impworks / lens Goto Github PK
View Code? Open in Web Editor NEWLanguage for Embeddable .NET Scripting
License: MIT License
Language for Embeddable .NET Scripting
License: MIT License
Посмотрел в *MemberNode
, а там только название member'а и присваиваемое значение в set-варианте. Когда разбирается выражение типа (a + b).mbr
, куда записывается (a + b)
?
В тестах возникла задача сравнивать узлы синтаксического дерева. Для этого у них, наверное, придётся перегрузить метод Equals
.
Нормально ли это?
Не пригодится ли где-нибудь ещё?
Если не пригодится (или будет мешать) - можно для тестов написать какую-нибудь фигню, которая бы сравнивала объекты рефлекшоном.
При написании парсера следует учесть, что ключевые слова формально являются идентификаторами. Нужно поставить проверку на то, чтобы они таковыми не считались.
Также в языке будут разрешены классические object, string, int, float, long, double - но в качестве алиасов для имен типов, а не в качестве ключевых слов.
Ключевые слова являются регистро-зависимыми. Синтаксиса типа @keyword
не предполагается. Идентификатор может начинаться с ключевого слова, т.е. types
- валидное имя для переменной.
Полагаю, можно уже понемножку начинать работы в ветке parser.
Непонятно, зачем нужны узлы NewDictionaryNode
, NewListNode
- в грамматике ничего такого нет (по крайней мере, я не нашёл).
Если и правда не нужны - предлагаю пока удалить.
Только мозг дошел до того, чтобы обдумать конкретным образом частичное применение функций в LENS. Пока ничего путного не приходит.
Основная проблема в сочетании ЧПФ и полиморфизма. Допустим, есть код:
fun test a:int b:int -> print a+b
fun test a:int b:long -> print a+b
fun test a:string b:string -> print a+b
var a1 = test
var a2 = test 1
Должна ли такая ситуация обрабатываться? Если да, то какого типа будут эти переменные и можно ли будет на их основе делать дальнейшее ЧПФ?
Пока есть идея, как сделать ЧПФ со следующими ограничениями:
@ForNeVeR: Стоит ли оно того при таком раскладе? Или пока отложить весь механизм до следующих версий?
Вопрос возник из следующей проблемы: допустим, есть выражение:
alpha.beta.gamma.delta.eps = 1
В данном случае, из-за рекурсивного подхода оно будет воспринято парсером так:
(((alpha.beta).gamma).delta).eps = 1
Что верно, если это всё объекты, но с другой стороны, alpha.beta.gamma
может вполне оказаться неймспейсом, а delta
- именем класса. Или же alpha.beta
- неймспейс, gamma
- класс, delta
- имя свойства типа X, у которого в свою очередь есть свойство eps
.
Разрешение таких идентификаторов становится вопросом нетривиальным. Причем разрешение должно начинаться с верхнего уровня, где выражение выглядит как X.Y = Z
и мы пытаемся узнать, что такое X
, спускаясь постепенно вниз и вниз.
Насколько я знаю, неймспейс - штука неделимая. Объявление namespace alpha.beta.gamma
не гарантирует создания базовых неймспейсов alpha
и alpha.beta
, поэтому как нода, представляющая собой alpha
будет разруливать, что она - часть неймспейса - одному богу ведомо. Даже если я и ошибаюсь, алгоритм все равно получается жутко рекурсивный.
Отсюда вариант: записывать доступ к неймспейсам так:
alpha::beta::gamma::delta.eps = 1
alpha::beta::gamma.delta.eps = 1
Плюсы:
Минусы:
@ForNeVeR, что скажешь? Я пока что за.
Строка ()
не парсится, хотя должна выдавать UnitNode
. Заведен тест в ParserTest.cs
.
Не нашёл в документации, как правильно формируются имена переменных и типов. Считаю, это нужно обязательно где-то описать, а также сообщить мне для парсера.
Сейчас я использую такое определение:
identifierRef := regex "[a-zA-Z_][0-9a-zA-Z_]*" >>=?
fun s -> if List.exists (fun k -> k = s) keywords then
pzero
else
preturn s
Что означает:
_
._
.Здесь буду описывать изменения в грамматике, которые требуется синхронизировать с кодом парсера. Не хочется плодить массу тикетов.
Нужно подумать на тему того, как лучше реализовать замыкания. Пока видится два варианта:
Первый более общий и более правильный - иметь гибкие механизмы перестраивания дерева кода открыло бы огромные возможности для всяких штук типа yield return
или async
, второй же вариант проще, хотя класс, отвечающий за обращение к переменным сильно замусорится.
Использовать готовый, или снова писать парсер ручками?
Сейчас класс Context
представляет собой сборную солянку, которая чуть-чуть повышает уровень абстракции:
Такой подход прост, но по мере того, как Lens обрастает функционалом, в нем будет все проще и проще запутаться. Например, существуют следующие проблемы:
Dictionary
, как полагается, а поля и методы нетДля исправления ситуации предлагается создать честную модель системы типов, описанных в скрипте, заменяющую собой неработающие методы Reflection'а у классов, получаемых через Emit.
Костяк будет состоять примерно из следующих классов:
Context
TypeReference (name, parent)
MethodReference (name, args, return)
ConstructorReference (args)
FieldReference (name, type)
PropertyReference (name, type)
Соответственно, Context теперь будет работать следующим образом:
TypeNode
, RecordNode
и TypeLabel
превращаются в TypeReference
RecordEntry
превращаются в FieldReference
FunctionNode
превращаются в MethodReference
LambdaNode
превращаются в хитроназванные TypeReference
и MethodReference
в нихFieldReference
, в MethodReference.Context
отмечаются замкнутые переменныеMethodReference
.Используя такой подход, мы получаем следующие преимущества:
Ничего я не упустил? Может быть, осталась какая-то неоднозначность?
Внезапно выяснилось несколько неоднозначных моментов, связанных с совместимостью с фреймворком .NET:
out
и ref
-параметрами?A<B
- выражение "а меньше б", или же часть подписи типа A<B>
?Класс Lens.SyntaxTree.SyntaxTree.Literals.UnitNode
почему-то оказался internal
.
Непонятно, куда записывать имя record'а в узел RecordDefinitionNode
.
Нужно утвердить ряд вопросов, связанных со средой разработки.
Я бы предпочёл использование VS2012, .NET4 с unit-тестами и интеграцией. Для интеграции можно использовать Jenkins, у меня есть минимальный опыт его использования с .NET-проектами.
Вообще, вопросы 3, 4 и 5 у меня возникли после того, как я понаблюдал за разработкой scala - там у ребят все серьёзно, и, как мне кажется, такой подход приносит ощутимые результаты как в плане развития проекта, так и в плане общей осведомлённости разработчиков о том, как идут дела в соседних компонентах.
Нигде в грамматике не обнаружил мест, в которых разрешены (или запрещены) пробелы, табуляции и прочий вайтспейс. Нужно обсудить этот вопрос. Вижу возможность ввести обработку двумя способами:
И ещё - что мы считаем вайтспейсом? Есть какие-то формальные определения?
Парсер уже скоро будет готов. Для того, чтобы побыстрее его допинать, я отложил написание unit-тестов. Но написать их будет нужно обязательно, потому что есть много мелочей, которые я мог не заметить (например, перепутать узлы синтаксического дерева).
Один я буду слишком долго писать тесты, так что, @impworks, мне потребуется твоя помощь.
Когда закончу парсер - залью его в ветку master
, и отдельно уведомлю о начале этапа активной разработки тестов.
Многие методы в ходе своей работы требуют обращения к "контексту компиляции" - некой структуре, хранящей в себе различную информацию, в частности:
В проекте Mirelle это называлось Emitter и передавалось в каждый метод Compile
и GetExpressionType
, что не очень удобно. Есть вариант хранить его статически, но он накрывает возможность создания двух экземпляров интерпретатора и использование его в многопоточной среде. Нужно подумать о том, насколько это важно.
Нашего препода безумно интересует адекватность сообщений об ошибках. В этой связи нужно сделать так, чтобы выдавался не список возможных лексем, а какие-то человекопонятные сообщения.
Желательно сделать этот способ расширяемым и легко дописываемым, потому что по мере тестирования вылезет много случаев, в которых было бы логично видеть кастомизированное сообщение об ошибке.
Есть такой тест:
test
<| true
<| (a:double) ->
logger.log a
a ** 2
<| false
Он не проходит из-за того, что аргументы функции должны быть в скобках (обсуждалось в #37).
Взял лямбду в скобки - парсинг стал проходить. Результат таков:
test
<| true
<| ((a:double) ->
logger.log a
a ** 2)
<| false
Является ли это поведение правильным? Прошу учесть, что на текущий момент любого рода сложные выражения (например, 2 + 2
) тоже не будут работать без скобок даже при многострочной передаче параметров.
Скрипт должен иметь возможность взаимодействовать с вызывающей его стороной. Для этого интерпретатору перед исполнением должны скармливаться типы и методы, которыми скрипт сможет манипулировать.
Необходимо продумать то, как сгенерированная с помощью Reflection.Emit сборка сможет получать к ним доступ.
Хотелось бы, например, сделать вот так:
script.AddType(typeof(System.Math));
script.AddMethod("log", (string s) => Console.WriteLine(s));
На мой взгляд, тестовый код какой-то кривой:
test 1337 true "hello" new(13.37; new [1; 2])
Я предлагаю такое исправление:
test 1337 true "hello" (new(13.37; new [1; 2]))
(после такого фикса тест начинает худо-бедно работать)
Должно ли быть возможным использование аргумента функции в таком контексте без скобок? F#, например, этого не позволяет, и грамматика у нас составлена соответствующим образом.
Решил завести одну задачу для грамматики, и все проблемы писать сюда.
Итак, номер первый. Есть такой вот пример кода:
a = (1 + 2).someShit
В соответствии с текущей грамматикой, он не может быть распарсен. .someShit
- это accessor_expr
, а в соответствии с грамматикой, accessor_expr
идёт только после статических или локальных идентификаторов:
lvalue = ( type "::" identifier | identifier ) { accessor_expr }
accessor_expr = "." identifier | "[" line_expr "]"
Сходу не могу придумать, как решить эту проблему.
Хотелось бы иметь корректное сообщение о том, что в скрипте поехал identing.
Эти классы нужно переименовать в *Node
, я думаю.
Нужны ли нам эти операторы?
Если typeof нужен, то придется также допускать специальный формат записи типа - generic-тип без опциональных параметров, как то:
var tupleGeneric = typeof(Tuple<,,,>); // кортеж из 4 элементов
Нашёл в Grammar.txt
потенциально опасную конструкцию:
invoke_list = { value_expr } | ( { NL "<|" value_expr } NL )
Она опасна тем, что под форму { value_expr }
подходит пустая строка - следовательно, эта форма будет матчиться вечно (на самом деле, fparsec просто бросит исключение, обнаружив такой парсер). Необходимо чем-то укрепить эту ветвь, чтобы она не могла сматчиться на пустом месте.
Здесь буду отписываться по мере появления багов в парсере, ради которых не хочется создавать отдельные тикеты.
NewObjectDeclaration
проходится, если 13.37
заменить на 13
.Сейчас у нас в языке предусмотрено, что if
или while
могут стоять по правую сторону от знака присваивания и результатом такого действия будет последняя выполненная команда \ строка. Однако такой подход сопряжен с определенными неудобствами.
Во-первых, если каждая строка будет оставлять что-то на стеке в надежде, что кто-то этим воспользуется, байткод получится неоптимальным и будет изобиловать операторами pop
.
Во-вторых, система вывода типов не позволит сделать такое условие:
if(a)
do_something 1 2
else
do_nothing
Если у функций do_something
и do_nothing
разный возвращаемый тип. В данном случае очевидно, что мы не присваиваем ничего и нам нахрен не нужно их возвращаемое значение.
Видимо, в метод Compile()
помимо указателя на CompilerContext
придется передавать еще и флаг, оставлять ли значение на стеке или нет.
По тексту задачи #9 я заметил, что ты планируешь использовать LINQ в Lens. Для этого придётся поддерживать Extension Methods. В курсе ли ты, как это вообще реализуется?
Никак не могу придумать, что делать с алгебраическими типами данных. @ForNeVeR, может быть, у тебя есть какие-то мысли?
Возьмем, например, такой тип:
type Multitude
| Integer of int
| Fractional of double
| None
А потом мы делаем так:
var some1 = Integer 1
var some2 = Fractional 1.337
Но что теперь с ними делать? Какие поля будут у переменных some1
и some2
? Как проверить их тип?
Варианта я вижу два:
IEnumerable
и Tuple
. Для него понадобится дополнительный синтаксис, ноды, правки в парсере и тесты.SomeType
будет прилагаться enum
вида SomeTypeKind
.SomeType
будет поле Kind
типа SomeTypeKind
для проверки типа.Label
, имеющего тег, будет доступно поле типа LabelTag
.Что скажешь?
Нет узла синтаксического дерева для обозначения значения null
.
В CatchNode
нужно хранить тип исключения и имя объекта, под которым это исключение будет доступно внутреннему коду.
Требуется дополнительный обход дерева, предшествующий компиляции. Этот обход должен выполнять следующий действия:
Scope
.Нужно что-то придумать для того, чтобы заполнить свойства StartLocation
, EndLocation
объектов NodeBase
.
Написать рабочую версию TypeSignature.resolveType
и тесты к ней.
Для выражения using
на данный момент не существует узла синтаксического дерева. Вероятно, нужно его добавить.
Нужно написать тесты, покрывающие все возможности LENS:
Эти тесты можно будет впоследствие представить в дипломе в качестве доказательства работоспособности программы: там есть отдельный раздел "отладка и тестирование".
Поскольку параметры в функции передаются без скобок, LINQ-вызов на LENS получится довольно громоздким и неудобным:
(((Enumerable.Range 1 100).Where x:int -> x > 10).Select x:int -> x * 2
В F# для этого используется оператор |>
, и код выглядит так:
let data = Emumerable.Range 1 100
let res = data
|> IEnumerable.filter (fun x -> x > 10)
|> IEnumerable.map (fun x -> x * 2)
У нас же оператор |>
сейчас используется для обратной цели - перед ним стоит функция, а после него - аргумент, не поместившийся на строку.
Возможно, есть смысл пересмотреть эту концепцию. Для передачи многих параметров в функцию использовать <|
, а для передачи значения по цепочке функций использовать оператор |>
? Тогда linq-вызов выглядел бы так:
var data = Enumerable.Range 1 100
let res = data
|> IEnumerable.Where x:int -> x > 10
|> IEnumerable.Select x:int -> x * 2
А передача - так:
var result = somefx
<| param1
<| param 2
default(T)
Все это необходимо не забыть учесть в парсере.
Расстояние между типами - это метрика, которая позволяет определить наиболее подходящий оверрайд метод для вызова. Чем меньше расстояние - тем больше метод "подходит".
Для вычисления расстояний используются следующие правила:
dist(T, T) = 0
dist(T1, T2) = 1 + dist(T1, T2.Parent)
dist(object, T(value)) = 1
dist(IType, Type(IType)) = 1
dist(X<in T1>, X<in T2>) = dist(T2, T1)
dist(T1[], T2[]) = dist(T1, T2)
dist(X<out T1>, X<out T2>) = dist(T1, T2)
Есть также особые случаи:
byte -> short -> int -> long
-> decimal
, single -> double
. Переходы с int
на single
и с long
на double
также считаются за единицу.Классы пространства имён Lens.SyntaxTree.Literals
названы как-то непоследовательно: у нас тут, например, и DoubleLiteral
, и IntNode
. Нужно назвать как-нибудь одинаково: или везде Node
, или везде Literal
. Лучше Node
- так будет больше походить на соседние пространства имён (все имена из Lens.SyntaxTree.*
заканчиваются на Node
).
В документации приведён такой пример для record:
record Student =
Name : string
Age : int
Однако в файле Grammar.txt
нет упоминаний о знаке равенства:
recorddef = "record" identifier NL recorddef_stmt { recorddef_stmt }
Нужно или исправить документацию, или исправить грамматику.
Для большей наглядности я написал в тестах код таким образом:
var src = @"
type hello
| tag1
| tag2";
И все тесты моментально попапали.
Недавно ввел в интерфейс NodeBase.Compile()
флаг mustReturn
. По сути, он нужен был для такого случая:
fun log str:string ->
Console.WriteLine str
str
fun log val:int ->
Console.WriteLine val
val
if (Math.RandomBool ())
log "hello"
else
log 1
По сути, разные ветки выражения if
будут иметь разный тип, и если бы его результат присваивался в переменную, это было бы недопустимо. Однако в теле метода такой код вполне имеет право на жизнь и не хотелось бы уродовать его таким образом:
if (Math.RandomBool ())
log "hello"
()
else
log 1
()
В таком случае CodeBlockNode
вызывал бы все ноды, кроме последней, со значением false
, а например LetNode
вызывала бы Value.Compile()
со значением true
.
Вроде выглядит удобно и понятно, но меня смущает тот факт, что у механизма, проходящего через все без исключения ноды, есть только одна точка применения - и все. Не является ли это оверкиллом?
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.