Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/impworks/lens
Language for Embeddable .NET Scripting
https://github.com/impworks/lens
compiler dotnet dotnetcore embeddable language scripting-language
Last synced: 5 days ago
JSON representation
Language for Embeddable .NET Scripting
- Host: GitHub
- URL: https://github.com/impworks/lens
- Owner: impworks
- License: mit
- Created: 2012-09-18T16:28:01.000Z (about 12 years ago)
- Default Branch: master
- Last Pushed: 2020-09-21T17:01:11.000Z (about 4 years ago)
- Last Synced: 2024-10-02T21:04:51.730Z (about 1 month ago)
- Topics: compiler, dotnet, dotnetcore, embeddable, language, scripting-language
- Language: C#
- Size: 5.69 MB
- Stars: 89
- Watchers: 13
- Forks: 9
- Open Issues: 23
-
Metadata Files:
- Readme: README.RU.md
- License: LICENSE.md
Awesome Lists containing this project
README
# LENS
## 1. Коротко о главном
* Встраиваемый скриптовый язык
* Платформа .NET
* Функциональная парадигма, статическая типизация
* Функции - объекты первого рода, алгебраические типы
* Взаимодействие со сборками .NET## 2. Синтаксис и возможности
Блоки выделяются отступами, выражения разделяются переводом строки.
Размер отступов не важен, однако разрешается использование только пробелов.### 2.1. Типы данных
В интерпретатор встроена поддержка следующих типов, аналогичных C#:
* `unit` - он же `void`
* `object`
* `bool`
* `int`
* `long`
* `float`
* `double`
* `decimal`
* `string`
* `char`### 2.2. Объявление констант и переменных
Изменяемые переменные объявляются ключевым словом `var`, константы - `let`.
Значение констант не обязательно должно быть константой во время компиляции.
Константы нельзя использовать слева от знака присваивания или передавать по ссылке.```
var a = 1
let b = 2
```При объявлении переменной обычно ей задается начальное значение. Если оно неизвестно,
необходимо явно указать тип:```
var c, d: int
с = 3
```### 2.3. Операторы
В языке объявлены следующие операторы, перечисленные в порядке приоритета:
1. Возведение в степень (`**`)
2. Умножение (`*`), деление (`/`), получение остатка от деления (`%`)
3. Сложение (`+`), вычитание (`-`)
4. Сдвиг влево (`<:`), сдвиг вправо (`:>`), проверка на null (`??`)
5. Сравнение (`==`, `<>`, `<`, `>`, `<=`, `>=`)
6. Логические операции (`&&`, `||`, `^^`)
7. Битовые операции (`&`, `|`, `^`)Оператор сложения также используется для конкатенации строк. Если у объекта есть переопределенный
оператор, он будет использован.### 2.4. Записи
Запись - то же самое, что структура. Объект, имеющий только поля. Без методов
и модификаторов доступа. Объявляется ключевым словом `record`:```csharp
record Student
Name : string
Age : int
```
Все поля структуры являются публичными.Структуры могут быть рекурсивными, т.е. включать в себя элементы собственного
типа.### 2.5. Алгебраические типы
Объявляются ключевым словом `type` и перечислением возможных ярлыков типа. К
каждому ярлыку может быть прикреплена метка с помощью ключевого слова `of`:```csharp
type Suit
Hearts
Clubs
Spades
Diamondstype Card
Ace of Suit
King of Suit
Queen of Suit
Jack of Suit
ValueCard of Tuple
```Ярлыки должны быть глобально уникальными идентификаторами в контексте скрипта,
поскольку они же являются статическими конструкторами:```csharp
let jack = Jack Hearts
let two = ValueCard new (Diamonds; 2)
```### 2.6. Функции
Функции объявляются в теле программы ключевым словом `fun`:
```csharp
fun negate:int (x:int) -> -xfun hypo:int (a:int b:int) ->
let sq1 = a * a
let sq2 = b * b
Math::Sqrt (sq1 + sq2)
```
После названия функции идет ее тип, после - список параметров с типами в скобках.Каждая функция имеет свое пространство имен. Переменные, объявленные в глобальной
области видимости, _не доступны_ внутри функций.#### 2.61. Аргументы функции
После слова `fun` идет название функции и тип возвращаемого значения,
а потом ее аргументы с указанием типа. Если у функции не объявлено
ни одного параметра, она будет принимать тип `unit` для вызова.
Литералом `unit` является выражение `()`.Ключевое слово `unit` является внутренним именованием типа. Его нельзя использовать
для описания типа аргумента функции, в качестве generic-параметра другого типа и в
операторах `default` и `typeof`.#### 2.6.2. Возвращаемое значение функции
Любая функция должна возвращать значение. Возвращаемым значением является последнее
выражение тела функции. Если последнее выражение - управляющая конструкция или вызов
функции типа `void`, функция возвращает тип `unit`.Если функция не должна возвращать никакого значения, а ее последнее выражение не является
`void`, следует использоаать литерал `()`.#### 2.6.3 Вызов функции
Функция вызывается, когда ей передаются все требуемые параметры. Для того, чтобы
вызвать функцию без параметров, ей нужно передать параметр типа `unit` - пара скобок `()`.```csharp
fun sum:int (a:int b:int c:int) -> a + b + c
fun getTen:int -> 10let five = sum 1 1 3
let ten = getTen ()
```При вызове функции можно использовать только литералы и имена переменных. Любые более сложные
выражения должны быть взяты в скобки.```csharp
fun sum:double (a:double b:double) -> a + blet sum = sqrt sin 1 // sqrt(sin, 1) - wtf?
let sum = sqrt (sin 1) // компилируется
let someData = sum (sin 1) (cos 2)
```
#### 2.6.4. Передача аргумента по ссылкеАргумент в функцию можно передать по ссылке. Для этого как в объявлении, так и при вызове
следует использовать модификатор `ref`:```csharp
fun test:bool (str:ref string) ->
if str.Length > 100 then
str = str.Substring 0 100
true
else
false
var a = "hello world"
var b = "test"
println (test ref a) // true
println (test ref b) // false
```
После `ref` может использоваться:* Имя переменной, объявленной с помощью `var`
* Имя аргумента текущей функции
* Обращение к полю
* Обращение к индексу массива
Не может быть использовано:* Литерал, выражение или имя константы, объявленной с помощью `let`
* Обращение к свойству
* Обращение к индексу объекта с переопределенным индексатором#### 2.6.5. Анонимные функции
Анонимные функции могут быть объявлены (практически) в любом месте программы.
Помимо отсутствия имени они отличаются от именованных функций следующими моментами:1. Анонимная функция замыкает переменные и константы из внешней области видимости.
2. Тип анонимной функции выводится автоматически, поскольку она не может быть рекурсивной.Анонимная функция может быть описана следующим образом:
let sum = (a:int b:int) -> a + b
let getTen = -> sum 5 5
let addFive = (a:int) ->
let b = 5
sum a b // то же самое, что sum 5
Как видно из следующего примера, оператор `->` разделяет параметры функции и
ее тело. Даже если параметров нет, `->` все равно необходимо указывать.Типы аргументов анонимной функции можно не указывать, если они могут быть однозначно
выведены из места ее применения, например:* При передаче анонимной функции в качестве параметра метода или конструктора
* При присвоении в поле, свойство, элемента массива или уже существующую переменную
* При использовании оператора приведения типов
* При использовании оператора композиции функций#### 2.6.6. Чистые функции и мемоизация
При объявлении именованной функции ее можно пометить модификатором `pure`. Это
означает, что при равных входных параметрах ее результат всегда будет
одинаковым. В таком случае при первом вызове ее результат будет закеширован,
а при повторных вызовах будет использоваться именно этот кеш, а сама функция
не будет повторно вызвана.Чистота функции не проверяется компилятором. Фактическое наличие побочных
эффектов остается на совести программиста.#### 2.6.7. Порядок объявления и вызова функций
Порядок не играет роли. Рекурсивные вызовы допустимы без какого-либо явного
указания (например, в F# требуется модификатор `rec`), взаимная рекурсия
также допустима.#### 2.6.8. Оператор передачи значения
Для передачи значения в функцию может быть использован оператор `<|`.
Однако этот оператор будет полезен, если аргументы не умещаются на одной строке, или
если требуется передать многострочное выражение.Оператор `<|` требует увеличения отступа относительно выражения, к которому он применяется.
```csharp
somefx
<| value1
<| (a b) ->
let sum = a + b
sum * sum
<| s -> log s
```
#### 2.6.9. Оператор передачи контекстаДля вызова функций по цепочке, особенно со сложными аргументами, удобно использовать
оператор передачи контекста, аналогичный точке. Он позволяет размещать длинное
выражение на нескольких строках.```csharp
someData
|> Where (a -> a.Value > 10)
|> Select (a -> a.Value ** 2)
|> Sum ()
```#### 2.6.10 Переменное число аргументов в функции
Можно объявить функцию, которая будет принимать переменное число аргументов и
упаковывать их в массив. Для этого необходимо указать модификатор типа с троточием
у последнего аргумента:```csharp
fun count:int (x:object...) ->
x.Length
let three = count 1 2 3
let five = count true "test" 1.3 3.7 three
```Как и в C#, данный аргумент должен быть последним в списке.
### 2.7. Ключевые слова и конструкции
#### 2.7.1. Создание объектов
Новые объекты создаются с помощью ключевого слова `new`:
```csharp
let tuple = new Tuple "hello" 2
```#### 2.7.2. Условие
Условие записывается с помощью блока if / else:
```csharp
let a = if 1 > 2 then 3 else 4
```Выражение может также использоваться по правую сторону от знака присваивания,
если указаны обе ветки (`if` и `else`). Если блок `else` не используется, конструкция
`if` всегда возвращает тип `unit`.#### 2.7.3. Цикл
Цикл записывается с помощью блока `while`:
```csharp
var a = 0
while a < 10 do
Console::WriteLine "{0} loop iteration" a
a = a + 1
```Цикл `while` всегда возвращает значение последнего выражения в теле цикла.
Если цикл не был выполнен ни одного раза, будет возвращено выражение `default(T)`.#### 2.7.4. try-catch
Блоки `try-catch` записываются следующим образом:
```csharp
try
doSomethingHorrible()
catch ex:WebException
notify "web exception" ex.Message
catch ex:DivideByZeroException
notify "whoops!"
catch
notify "something weird has happened"
```После блока `try` также может идти блок `finally`, который выполняется всегда
при выходе из области видимости, возникало ли исключение или нет:```csharp
try
doSomething ()
catch
notify "something happened"
finally
freeResources ()
```Блок `try-catch` всегда возвращает `unit`.
#### 2.7.5. use
Ключевое слово `use` открывает пространство имен, добавляя объявленные в нем
классы в глобальное:```csharp
use System.Text.RegularExpressions
let rx = new Regex "[a-z]{2}"
```#### 2.7.6. using
Ключевое слово `using` позволяет объявить блок, которым ограничен интервал жизни
ресурса, реализуюшего интерфейс `IDisposable`:```csharp
using fs = (new FileStream "file.txt" FileMode::Create) do
fs.WriteByte 1
```#### 2.7.7. Приведение и проверка типов
Для приведения типов используется оператор `as`. В отличие от C#, он кидает
`InvalidCastException` в случае неудачи, а не возвращает `null`. Может быть
использован на любых типах, в том числе `int` / `string` / `bool` / `object`.Для проверки того, является ли объект экземпляром некоторого класса, используется
оператор `is`. Он возвращает `bool`.### 2.8. Создание структур данных
В языке есть поддержка для упрощенного создания заранее инициализированных
коллекций разного типа. Для этого используется специальный синтаксис оператора `new`.Данный синтаксис используется только в том случае, если количество элементов
заранее известно и оно отлично от нуля. Для объявления пустых структур данных
следует пользоваться их классическими конструкторами. Объявить пустой массив можно
с помощью `System.Array.CreateInstance(...)`. Возможно, следует добавить для этого
случая generic-метод.Тип коллекции выводится автоматически из типов аргументов. Для этого в коллекции должен
присутствовать хотя бы один элемент, отличный от null.Ключи `Dictionary` проверяются более строго - они не могут иметь значение `null` и их тип
должен совпадать в точности.#### 2.8.1. Массивы
```csharp
// int[]
let ints = new [1; 2; 3]
```#### 2.8.2. Списки
```csharp
// System.Collections.Generic.List
let ints = new [[1; 2; 3]]
```#### 2.8.3 Словари
```csharp
// System.Collections.Generic.Dictionary
let dict = new { "hello" => 1; "world" => 2 }
```#### 2.8.4 Кортежи
```csharp
// System.Tuple
let t = new (1, "hello world", new object())
```В кортеже должно быть от 1 до 7 элементов. Кортежи неограниченной длины, возможно,
будут поддерживаться в следующей версии.### 2.9 Функциональные возможности
#### 2.9.1 Приведение делегатов
Анонимные функции по умолчанию являются выражениями типа `Func<>` или `Action<>`, в зависимости
от того, возвращают ли они некое значение или их последнее выражение имеет тип `unit`.Для того, чтобы передать анонимную функцию в качестве параметра с иным типом, можно
использовать приведение типов. Для этого типы принимаемых и возвращаемых значений должны
в точности соответствовать:```csharp
let filter = (x:int) -> x % 2 == 0
let data = (Enumerable::Range 1 100).ToArray ()
let even = Array::FindAll data (filter as Predicate)
```#### 2.9.2 Частичное применение
На основе одной функции можно создать другую, передав ей часть параметров, а вместо недостающих
указав специальный идентификатор `_`. Результирующая функция будет принимать оставшиеся параметры:```csharp
fun add:int (x:int y:int) -> x + y
let add2 = add 2 _
let alsoAdd2 = add _ 2let three = add2 1 // int(3)
```При наличии перегруженных вариантов функции явно указанные аргументы должны однозначно
идентифицировать функцию, в противном случае возникнет ошибка неоднозначности:```csharp
fun repeat:str (value:int count:int) -> string::Join "" (new [value] * count)
fun repeat:str (value:string count:int) -> string::Join "" (new [value] * count)let repeat2 = repeat _ 2 // error: both functions match
```Частичное применение работает как с функциями, так и с конструкторами.
#### 2.9.3 Композиция функций
С помощью оператора композиции можно создавать новые функции из существующих, используя
результат одной функции в качестве аргумента для другой:```csharp
let parse = (x:string) -> Convert::ToInt32 x
let inc = (x:int) -> x + 1let compound = parse :> inc
println (compound "2") // 3
```Функция справа от оператора `:>` должна иметь строго 1 параметр, совпадающий с типом
возвращаемого значения функции слева. В этом случае удобно использовать частичное применение:```csharp
let add = (x:int y:int) -> x + y
let compound = parse :> add _ 1
```### 2.10. Сопоставление с образцом
Сопоставление с образцом позволяет разбирать произвольные структуры данных и извлекать из
них необходимые значения, наподобие того, как регулярные выражения работают со строками.
Для описания списка шаблонов используется блок `match`:```csharp
match x with
case 1 then "one"
case 2 then "two"
case _ then "other number"
```Правила применяются последовательно, пока не найдется удовлетворяющее - тогда будет возвращено
выражение результата, указанное после `then`. Возвращаемым типом является наиболее близкий общий тип,
подходящий ко всем указанным выражениям результата. Если ни одно правило не подошло, будет
возвращено значение по умолчанию для данного типа (`default T`).#### 2.10.1. Типы правил
##### 2.10.1.1. Литерал
В качестве образца можно использовать литералы встроенных типов - `int`, `string`, `bool` и т.д.
Также допустим литерал `null`.##### 2.10.1.2. Захват имени
Если в качестве образца указан идентификатор, значение сохраняется в переменную с таким названием,
которая может быть использована в дополнительных проверках и при возвращении результата.Идентификатор `_` (одно нижнее подчеркивание) не сохраняет значение и может быть использован
несколько раз.После идентификатора можно явно указать тип: тогда правило совпадет только в том случае, если объект
является экземпляром данного типа.```csharp
match getException () with
case ex:ArgumentException then "Invalid arg"
case ex:DivideByZeroException then "Division by zero"
case _ then "Something went wrong"
```##### 2.10.1.3. Диапазон
Числовое значение можно проверить на принадлежность к диапазону: `case 1..5`.
Обе границы диапазона включаются.##### 2.10.1.4. Кортежи
Кортежи можно разбить на индивидуальные значения, к каждому из которых применяется свое
вложенное правило:```csharp
let tuple = new (1; 2; "test"; "bla")
match tuple with
case (x; y; str; _) then fmt "{0} = {1}" str (x + y) // test = 3
```##### 2.10.1.5. Массивы и последовательности
Массивы (`T[]`), списки (`List`) и последовательности (`IEnumerable`) можно разбить
на элементы:```csharp
match array with
case [] then "empty"
case [x] then fmt "one item: {0}" x
case [x; y] then fmt "two items: {0} and {1}" x y
case [_; _; _] then "three items"
case _ then "more than 3 items"
```К одному из идентификаторов можно применить префикс-многоточие. Тогда этот идентификатор
захватит не один элемент, а вложенную последовательность из нуля и более элементов:```
fun length:int (array:object[]) ->
match array with
case [] then 0
case [_] then 1
case [_; ...x] then 1 + (length x)
```Для массива и `IList` подмножество может быть любым элементом, тип подмножества - `T[]`.
Для остальных случаем - только последний элемент, тип - `IEnumerable`.##### 2.10.1.6. Записи
Для объявленных в скрипте структур можно применить вложенные правила для каждого поля:
```
record Point
X : int
Y : intfun describe:string (pt:Point) ->
match pt with
case Point(X = 0; Y = 0) then "Zero"
case Point(X = 0) | Point(Y = 0) then "half-zero"
case _ then "Just a point"
```Поля, для которых проверки не указаны, могут иметь любые значения.
##### 2.10.1.7. Алгебраические типы
Для объявленных в скрипте типов можно проверить значение ярлыка:
```
type Expr
IntExpr of int
StringExpr of string
AddExpr of Tuple
SubExpr of Tuplefun describe:string (expr:Expr) ->
match expr with
case IntExpr of x then fmt "Int({0})" x
case StringExpr of x then fmt "Str({0})" x
case AddExpr of (x; y) then fmt "{0} + {1}" (describe x) (describe y)
case SubExpr of (x; y) then fmt "{0} - {1}" (describe x) (describe y)
```Для типов без ярлыка следует использовать явное указание типа (см. 2.10.1.1).
##### 2.10.1.8. KeyValue
Для элементов словаря можно использовать особый синтаксис: `case key => value`.
##### 2.10.1.9. Регулярные выражения
Строку можно сопоставить с регулярным выражением:
```
match "String" with
case #^[a-z]+$# then "lower"
case #^[A-Z]+$# then "upper"
case #^[a-z]+$#i then "mix"
```Допустимы следующующие модификаторы в любом порядке:
* `i = RegexOptions.IgnoreCase`
* `m = RegexOptions.Multiline`
* `s = RegexOptions.Singleline`
* `c = RegexOptions.CultureInvariant`Именованные группы автоматически извлекаются в одноименные переменные:
```
match "My name is John" with
case #^My name is (?\w+)$#i then fmt "Hello, {0}" name
```По умолчанию, тип извлеченных переменных - `string`. Для удобства значения можно
автоматически сконвертировать в любой тип `T`, если для него объявлен статический
метод: `bool T.TryParse(string value, out T result)`. Для этого тип указывается
через двоеточие после имени группы. Если метод `TryParse` возвращает `false`,
правило не применяется.```
match "I have 2 cookies" with
case #^I have (?\d+) cookies$# then fmt "Twice as much will be {0}" (count * 2)// Result: "Twice as much will be 4"
```#### 2.10.2. Альтернативные правила
Можно указать несколько правил, разделенных вертикальной чертой - тогда
достаточно совпасть хотя бы одному из них:```
match number with
case 1 | 2 | 3 then "one, two or three"
case _ then "other number"
```Если хотя бы одно правило захватывает какое-либо имя, такое же имя с таким же типом
должно быть захвачено во всех альтернативных правилах. Порядок захвата не важен.
Именованные группы в регулярных выражениях учитываются, а специальное имя `_` - нет.#### 2.10.3 Проверки `when`
Каждое выражение `case` может содержать дополнительную проверку - выражение, которое
должно вернуть `true`, чтобы правило совпало. Для этого применяется ключевое слово `when`:```
match x with
case y when y % 2 == 0 then "even"
case _ then "odd"
```## 3. Встраиваемость
Технически, интерпретатор реализован в виде сборки .NET, которую
программист может подключить к своей программе, чтобы добавить в нее
поддержку скриптового языка.Сборка содержит основной класс интерпретатора. Схема работы программиста с
интерпретатором следующая:1. Добавить в проект ссылку на сборки LENS
2. Создать объект интерпретатора
3. Зарегистрировать в интерпретаторе свои типы, функции и свойства
4. Передать интерпретатору текст исполняемой программыРезультатом является объект типа `Func`, позволяющий исполнять скрипт многократно
без необходимости перекомпиляции.
Примерный код этого взаимодействия на языке C# представлен ниже:```csharp
public void Run()
{
var source = "a = 1 + 2";
var a = 0;var compiler = new LensCompiler();
compiler.RegisterProperty("a", () => a, newA => a = newA);
try
{
var fx = compiler.Compile(source);
fx();Console.WriteLine("Success: {0}", a);
}
catch (LensCompilerException ex)
{
Console.WriteLine("Error: {0}", ex.FullMessage);
}
}
```## 4. Дополнительные возможности
* Поддержка переопределенных операторов
* Раскрутка констант во время компиляции
* Сохранение сгенерированной сборки в виде исполняемого файла
* Возможность отключать поиск extension-методов для ускорения компиляции## 5. Ограничения
### 5.1. Планы на будущее
Список планируемых возможностей для следующих версий доступен в виде задач на Github:
https://github.com/impworks/lens/issues### 5.2. Сознательные ограничения
Поскольку LENS является встраиваемым языком, в нем не будет вещей, присущих
классическим языкам программирования, как то:* Создание полноценных классов с методами
* Модификаторы доступа
* Объявление интерфейсов
* Управляющие конструкции, прерывающие поток выполнения: `return`, `break`, `continue`