Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

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

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
Diamonds

type 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) -> -x

fun 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 -> 10

let five = sum 1 1 3
let ten = getTen ()
```

При вызове функции можно использовать только литералы и имена переменных. Любые более сложные
выражения должны быть взяты в скобки.

```csharp
fun sum:double (a:double b:double) -> a + b

let 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 _ 2

let 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 + 1

let 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 : int

fun 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 Tuple

fun 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`