From 2d3daa382157547efa5c1af645c4f3862e2bc2d1 Mon Sep 17 00:00:00 2001 From: cyberpsychoz Date: Wed, 28 Jan 2026 12:40:17 +0300 Subject: [PATCH 1/3] Add Python unit tests and fix Windows compatibility - Add Python compiler unit tests (30 tests) - Fix Windows path handling in fixtures test - Fix Windows CLI invocation in integration tests - Make test-python.sh configurable via PYTHON_CMD - Update README with Python documentation --- README.md | 11 +- test/acceptance/test-python.sh | 6 +- test/integration/cli.integration.test.ts | 4 +- test/integration/fixtures.integration.test.ts | 2 +- test/unit/compilers/python.unit.test.ts | 178 ++++++++++++++++++ 5 files changed, 193 insertions(+), 8 deletions(-) create mode 100644 test/unit/compilers/python.unit.test.ts diff --git a/README.md b/README.md index 43c77b6..fd12ee5 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![CI](https://github.com/enspirit/elo/actions/workflows/ci.yml/badge.svg)](https://github.com/enspirit/elo/actions/workflows/ci.yml) A simple, well-designed, portable and safe data expression language that -compiles to Ruby, Javascript and PostgreSQL. +compiles to Ruby, JavaScript, Python and PostgreSQL. **[Try Elo online](https://elo-lang.org/)** - Interactive playground and documentation @@ -55,6 +55,7 @@ See also the Related work section below. - **Multi-target compilation**: - Ruby (using `**` for power, `&&`/`||`/`!` for boolean logic, `Date.parse()`, `DateTime.parse()`, `ActiveSupport::Duration.parse()`) - JavaScript (using `Math.pow()` for power, `&&`/`||`/`!` for boolean logic, `new Date()`, `Duration.parse()`) + - Python (using `**` for power, `and`/`or`/`not` for boolean logic, `datetime` module) - PostgreSQL (using `POWER()` for power, `AND`/`OR`/`NOT` for boolean logic, `DATE`, `TIMESTAMP`, `INTERVAL` for temporals) ## Installation @@ -94,6 +95,9 @@ The compiler translates Elo expressions to Ruby, JavaScript, or SQL: # Compile expression to Ruby ./bin/eloc -e "2 + 3 * 4" -t ruby +# Compile expression to Python +./bin/eloc -e "2 + 3 * 4" -t python + # Compile expression to SQL ./bin/eloc -e "2 + 3 * 4" -t sql @@ -116,7 +120,7 @@ cat input.elo | ./bin/eloc - -t ruby Options: - `-e, --expression ` - Expression to compile -- `-t, --target ` - Target language: `ruby`, `js` (default), `sql` +- `-t, --target ` - Target language: `ruby`, `js` (default), `python`, `sql` - `-p, --prelude` - Include necessary library imports/requires - `--prelude-only` - Output only the prelude (no expression needed) - `-f, --file ` - Output to file instead of stdout @@ -197,7 +201,7 @@ The CLI and playground support multiple input/output formats (JSON, CSV). The fo For more control, you can use the lower-level parsing and compilation functions: ```typescript -import { parse, compileToRuby, compileToJavaScript, compileToSQL } from '@enspirit/elo'; +import { parse, compileToRuby, compileToJavaScript, compileToPython, compileToSQL } from '@enspirit/elo'; // Parse an expression const ast = parse(` @@ -211,6 +215,7 @@ const ast = parse(` // Compile to different targets console.log(compileToRuby(ast)); console.log(compileToJavaScript(ast)); +console.log(compileToPython(ast)); console.log(compileToSQL(ast)); ``` diff --git a/test/acceptance/test-python.sh b/test/acceptance/test-python.sh index 50b40ee..464c639 100755 --- a/test/acceptance/test-python.sh +++ b/test/acceptance/test-python.sh @@ -40,12 +40,14 @@ while IFS= read -r -d '' file; do relpath="${file#$TEST_DIR/}" # Fixtures contain self-executing code, run with prelude - if { echo "$PRELUDE"; cat "$file"; } | python3 2>/dev/null; then + # Use python3 on Unix, python on Windows + PYTHON_CMD="${PYTHON_CMD:-python3}" + if { echo "$PRELUDE"; cat "$file"; } | $PYTHON_CMD 2>/dev/null; then echo " ✓ $relpath" ((PASSED++)) || true else echo " ✗ $relpath" - { echo "$PRELUDE"; cat "$file"; } | python3 || true + { echo "$PRELUDE"; cat "$file"; } | $PYTHON_CMD || true ((FAILED++)) || true fi done < <(find "$TEST_DIR" -type f -name "*.expected.py" -print0 | sort -z) diff --git a/test/integration/cli.integration.test.ts b/test/integration/cli.integration.test.ts index beb651f..2842ddc 100644 --- a/test/integration/cli.integration.test.ts +++ b/test/integration/cli.integration.test.ts @@ -9,8 +9,8 @@ import { tmpdir } from 'os'; * CLI integration tests for the eloc and elo commands */ -const ELOC = './bin/eloc'; -const ELO = './bin/elo'; +const ELOC = process.platform === 'win32' ? 'node bin/eloc' : './bin/eloc'; +const ELO = process.platform === 'win32' ? 'node bin/elo' : './bin/elo'; function eloc(args: string): string { return execSync(`${ELOC} ${args}`, { encoding: 'utf-8' }).trim(); diff --git a/test/integration/fixtures.integration.test.ts b/test/integration/fixtures.integration.test.ts index 83dd565..be0c807 100644 --- a/test/integration/fixtures.integration.test.ts +++ b/test/integration/fixtures.integration.test.ts @@ -42,7 +42,7 @@ describe('Fixture validation', () => { const files = findEloFiles(TEST_DIR); for (const filePath of files) { - const fileName = filePath.split('/').pop()!; + const fileName = filePath.split(/[/\\]/).pop()!; const displayName = relative(TEST_DIR, filePath); if (EXEMPT_FILES.includes(fileName)) { diff --git a/test/unit/compilers/python.unit.test.ts b/test/unit/compilers/python.unit.test.ts new file mode 100644 index 0000000..d297a43 --- /dev/null +++ b/test/unit/compilers/python.unit.test.ts @@ -0,0 +1,178 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert'; +import { compileToPython } from '../../../src/compilers/python'; +import { literal, stringLiteral, variable, binary, unary, letExpr, memberAccess } from '../../../src/ast'; + +function wrapPy(code: string): string { + return `(lambda _: ${code})`; +} + +describe('Python Compiler - Literals', () => { + it('should compile numeric literals', () => { + assert.strictEqual(compileToPython(literal(42)), wrapPy('42')); + assert.strictEqual(compileToPython(literal(3.14)), wrapPy('3.14')); + assert.strictEqual(compileToPython(literal(0)), wrapPy('0')); + }); + + it('should compile boolean literals', () => { + assert.strictEqual(compileToPython(literal(true)), wrapPy('True')); + assert.strictEqual(compileToPython(literal(false)), wrapPy('False')); + }); +}); + +describe('Python Compiler - String Literals', () => { + it('should compile simple string', () => { + assert.strictEqual(compileToPython(stringLiteral('hello')), wrapPy('"hello"')); + }); + + it('should compile string with spaces', () => { + assert.strictEqual(compileToPython(stringLiteral('hello world')), wrapPy('"hello world"')); + }); + + it('should compile empty string', () => { + assert.strictEqual(compileToPython(stringLiteral('')), wrapPy('""')); + }); +}); + +describe('Python Compiler - Variables', () => { + it('should compile input variable _', () => { + assert.strictEqual(compileToPython(variable('_')), wrapPy('_')); + }); + + it('should compile member access on _ using .get()', () => { + assert.strictEqual(compileToPython(memberAccess(variable('_'), 'price')), wrapPy('_.get("price")')); + assert.strictEqual(compileToPython(memberAccess(variable('_'), 'userName')), wrapPy('_.get("userName")')); + }); +}); + +describe('Python Compiler - Arithmetic Operators', () => { + it('should compile addition', () => { + const ast = binary('+', literal(1), literal(2)); + assert.strictEqual(compileToPython(ast), wrapPy('1 + 2')); + }); + + it('should compile subtraction', () => { + const ast = binary('-', literal(5), literal(3)); + assert.strictEqual(compileToPython(ast), wrapPy('5 - 3')); + }); + + it('should compile multiplication', () => { + const ast = binary('*', literal(4), literal(3)); + assert.strictEqual(compileToPython(ast), wrapPy('4 * 3')); + }); + + it('should compile division', () => { + const ast = binary('/', literal(10), literal(2)); + assert.strictEqual(compileToPython(ast), wrapPy('10 / 2')); + }); + + it('should compile modulo', () => { + const ast = binary('%', literal(10), literal(3)); + assert.strictEqual(compileToPython(ast), wrapPy('10 % 3')); + }); + + it('should compile power to ** operator', () => { + const ast = binary('^', literal(2), literal(3)); + assert.strictEqual(compileToPython(ast), wrapPy('2 ** 3')); + }); +}); + +describe('Python Compiler - Comparison Operators', () => { + it('should compile less than', () => { + const ast = binary('<', literal(5), literal(10)); + assert.strictEqual(compileToPython(ast), wrapPy('5 < 10')); + }); + + it('should compile greater than', () => { + const ast = binary('>', literal(15), literal(10)); + assert.strictEqual(compileToPython(ast), wrapPy('15 > 10')); + }); + + it('should compile equality', () => { + const ast = binary('==', literal(10), literal(10)); + assert.strictEqual(compileToPython(ast), wrapPy('10 == 10')); + }); + + it('should compile inequality', () => { + const ast = binary('!=', literal(5), literal(10)); + assert.strictEqual(compileToPython(ast), wrapPy('5 != 10')); + }); +}); + +describe('Python Compiler - Logical Operators', () => { + it('should compile AND to "and"', () => { + const ast = binary('&&', literal(true), literal(false)); + assert.strictEqual(compileToPython(ast), wrapPy('True and False')); + }); + + it('should compile OR to "or"', () => { + const ast = binary('||', literal(true), literal(false)); + assert.strictEqual(compileToPython(ast), wrapPy('True or False')); + }); + + it('should compile NOT to "not"', () => { + const ast = unary('!', literal(true)); + assert.strictEqual(compileToPython(ast), wrapPy('not True')); + }); +}); + +describe('Python Compiler - Unary Operators', () => { + it('should compile unary minus', () => { + const ast = unary('-', literal(5)); + assert.strictEqual(compileToPython(ast), wrapPy('-5')); + }); + + it('should compile unary plus', () => { + const ast = unary('+', literal(5)); + assert.strictEqual(compileToPython(ast), wrapPy('+5')); + }); +}); + +describe('Python Compiler - Operator Precedence', () => { + it('should handle multiplication before addition', () => { + const ast = binary('+', literal(2), binary('*', literal(3), literal(4))); + assert.strictEqual(compileToPython(ast), wrapPy('2 + 3 * 4')); + }); + + it('should preserve precedence with nested expressions', () => { + const ast = binary('*', binary('+', literal(2), literal(3)), literal(4)); + assert.strictEqual(compileToPython(ast), wrapPy('(2 + 3) * 4')); + }); + + it('should handle power with addition', () => { + const ast = binary('+', binary('^', literal(2), literal(3)), literal(1)); + assert.strictEqual(compileToPython(ast), wrapPy('2 ** 3 + 1')); + }); +}); + +describe('Python Compiler - Let Expressions', () => { + it('should compile simple let with walrus operator', () => { + const ast = letExpr([{ name: 'x', value: literal(1) }], variable('x')); + assert.strictEqual(compileToPython(ast), wrapPy('(x := 1, x)[-1]')); + }); + + it('should compile let with multiple bindings', () => { + const ast = letExpr( + [{ name: 'x', value: literal(1) }, { name: 'y', value: literal(2) }], + binary('+', variable('x'), variable('y')) + ); + assert.strictEqual(compileToPython(ast), wrapPy('(x := 1, y := 2, x + y)[-1]')); + }); +}); + +describe('Python Compiler - Date Arithmetic', () => { + it('should compile date literal', () => { + const ast = { type: 'date', value: '2024-01-15' } as const; + assert.strictEqual(compileToPython(ast), wrapPy('_elo_dt(2024, 1, 15)')); + }); + + it('should compile datetime literal', () => { + const ast = { type: 'datetime', value: '2024-01-15T10:30:00Z' } as const; + assert.strictEqual(compileToPython(ast), wrapPy('_elo_dt(2024, 1, 15, 10, 30, 0)')); + }); + + it('should compile duration literal', () => { + const ast = { type: 'duration', value: 'P1D' } as const; + assert.strictEqual(compileToPython(ast), wrapPy('EloDuration.from_iso("P1D")')); + }); +}); From 4809bf44019e37e728416390b230040883a7283f Mon Sep 17 00:00:00 2001 From: cyberpsychoz Date: Wed, 28 Jan 2026 12:42:53 +0300 Subject: [PATCH 2/3] Add comprehensive Russian documentation for Python compiler --- PYTHON-COMPILER-RU.md | 649 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 649 insertions(+) create mode 100644 PYTHON-COMPILER-RU.md diff --git a/PYTHON-COMPILER-RU.md b/PYTHON-COMPILER-RU.md new file mode 100644 index 0000000..4e33cc9 --- /dev/null +++ b/PYTHON-COMPILER-RU.md @@ -0,0 +1,649 @@ +# Python компилятор Elo — Техническая документация + +## Обзор + +Elo теперь компилируется в Python, что делает его четвёртым целевым языком наряду с JavaScript, Ruby и PostgreSQL SQL. Компилятор преобразует выражения Elo в идиоматический Python-код, сохраняя семантическую эквивалентность со всеми остальными таргетами. + +--- + +## Архитектура компилятора + +### Файловая структура + +``` +src/ +├── compilers/ +│ └── python.ts # Основной компилятор Python +├── bindings/ +│ └── python.ts # Биндинги stdlib для Python +└── runtime.ts # Python helper-функции (PY_HELPERS) +``` + +### Pipeline компиляции + +1. **Parse**: Исходный код → AST (`parser.ts`) +2. **Transform**: AST → Typed IR (`transform.ts`) — выводит типы, переписывает операторы как вызовы функций +3. **Emit**: IR → Python код (`compilers/python.ts`) — использует `StdLib` для диспатчинга по типам + +--- + +## Основные конструкции + +### 1. Литералы + +| Elo | Python | Описание | +|-----|--------|----------| +| `42` | `42` | Целое число | +| `3.14` | `3.14` | Число с плавающей точкой | +| `true` | `True` | Логическое истина | +| `false` | `False` | Логическое ложь | +| `null` | `None` | Пустое значение | +| `'hello'` | `"hello"` | Строка | +| `[1, 2, 3]` | `[1, 2, 3]` | Массив | +| `{a: 1, b: 2}` | `{"a": 1, "b": 2}` | Объект (словарь) | + +**Пример компиляции:** +``` +Elo: true && false +Python: (lambda _: True and False) +``` + +### 2. Арифметические операторы + +| Elo | Python | Описание | +|-----|--------|----------| +| `+` | `+` | Сложение | +| `-` | `-` | Вычитание | +| `*` | `*` | Умножение | +| `/` | `/` | Деление | +| `%` | `%` | Остаток от деления | +| `^` | `**` | Возведение в степень | + +**Важно**: Elo использует `^` для степени, а Python использует `**`. Компилятор автоматически транслирует. + +``` +Elo: 2 ^ 10 +Python: (lambda _: 2 ** 10) +# Результат: 1024 +``` + +### 3. Логические операторы + +| Elo | Python | Описание | +|-----|--------|----------| +| `&&` | `and` | Логическое И | +| `\|\|` | `or` | Логическое ИЛИ | +| `!` | `not` | Логическое НЕ | + +``` +Elo: true && !false +Python: (lambda _: True and not False) +``` + +### 4. Операторы сравнения + +| Elo | Python | +|-----|--------| +| `<`, `>`, `<=`, `>=` | Идентично | +| `==`, `!=` | Идентично | + +--- + +## Let-выражения (Локальные переменные) + +Elo использует функциональные let-выражения для объявления локальных переменных. В Python это реализовано через **walrus operator** (`:=`), доступный с Python 3.8. + +**Синтаксис Elo:** +``` +let x = 10, y = 20 in x + y +``` + +**Компиляция в Python:** +```python +(lambda _: (x := 10, y := 20, x + y)[-1]) +``` + +**Как это работает:** +1. `(x := 10, y := 20, x + y)` — кортеж, где walrus operator присваивает значения +2. `[-1]` — берём последний элемент кортежа (результат выражения) +3. Всё обёрнуто в `lambda _` для соответствия паттерну Elo + +**Пример с вычислениями:** +``` +Elo: let price = 100, tax = 0.21 in price * (1 + tax) +Python: (lambda _: (price := 100, tax := 0.21, price * (1 + tax))[-1]) +# Результат: 121.0 +``` + +--- + +## Лямбда-функции + +Elo поддерживает анонимные функции с синтаксисом `fn(params ~> body)` или сокращённо `params ~> body`. + +**Полный синтаксис:** +``` +fn(x ~> x * 2) +``` + +**Сокращённый синтаксис:** +``` +x ~> x * 2 +``` + +**Компиляция в Python:** +```python +lambda x: x * 2 +``` + +**Пример с map:** +``` +Elo: map([1, 2, 3], fn(x ~> x * 2)) +Python: (lambda _: list(map(lambda x: x * 2, [1, 2, 3]))) +# Результат: [2, 4, 6] +``` + +--- + +## Доступ к данным + +### Переменная `_` (Input) + +Каждое выражение Elo получает входные данные через переменную `_`. В Python она передаётся как аргумент лямбды. + +``` +Elo: _.price * 1.21 +Python: (lambda _: _.get("price") * 1.21) +``` + +**Важно**: Компилятор использует `.get()` для безопасного доступа к полям словаря, что возвращает `None` для отсутствующих ключей вместо выброса исключения. + +### DataPath (Пути к данным) + +DataPath — это специальный синтаксис для навигации по структурам данных. + +``` +Elo: .user.address.city +Python: ["user", "address", "city"] +``` + +Используется с функцией `fetch()`: +``` +Elo: fetch(data, .user.name) +Python: (lambda _: kFetch(data, ["user", "name"])) +``` + +--- + +## Pipe-оператор (`|>`) + +Pipe-оператор позволяет писать цепочки преобразований слева направо (как в Elixir). + +``` +Elo: [1, 2, 3] |> map(x ~> x * 2) |> filter(x ~> x > 2) +``` + +**Компиляция в Python (вложенные вызовы):** +```python +(lambda _: list(filter(lambda x: x > 2, list(map(lambda x: x * 2, [1, 2, 3]))))) +``` + +**Результат:** `[4, 6]` + +--- + +## Alternative-оператор (`|`) + +Оператор альтернативы возвращает первое не-None значение из цепочки. + +``` +Elo: _.value | _.default | 0 +``` + +**Компиляция в Python:** +```python +(lambda _: (_alt0) if (_alt0 := _.get("value")) is not None else + ((_alt1) if (_alt1 := _.get("default")) is not None else (0))) +``` + +**Как это работает:** +1. Проверяем `_.value`, сохраняем в `_alt0` +2. Если `_alt0 is not None`, возвращаем его +3. Иначе проверяем `_.default`, сохраняем в `_alt1` +4. Если `_alt1 is not None`, возвращаем его +5. Иначе возвращаем `0` + +--- + +## Временные типы (Temporal) + +### Даты и DateTime + +| Elo | Python | Описание | +|-----|--------|----------| +| `TODAY` | `_elo_dt(now.year, now.month, now.day)` | Текущая дата | +| `NOW` | `_dt.datetime.now()` | Текущие дата и время | +| `D2024-01-15` | `_elo_dt(2024, 1, 15)` | Литерал даты | +| `D2024-01-15T10:30:00Z` | `_elo_dt(2024, 1, 15, 10, 30, 0)` | Литерал datetime | + +**Helper-функция `_elo_dt`:** +```python +def _elo_dt(y, mo, d, h=0, mi=0, s=0): + return _dt.datetime(y, mo, d, h, mi, s) +``` + +### Длительности (Duration) + +ISO8601 длительности компилируются в класс `EloDuration`: + +``` +Elo: P1D # 1 день +Python: EloDuration.from_iso("P1D") + +Elo: PT2H30M # 2 часа 30 минут +Python: EloDuration.from_iso("PT2H30M") + +Elo: P1Y2M3D # 1 год 2 месяца 3 дня +Python: EloDuration.from_iso("P1Y2M3D") +``` + +### Арифметика с датами + +``` +Elo: TODAY + P1D +Python: (lambda _: kAdd(_elo_dt(...), EloDuration.from_iso("P1D"))) +``` + +Helper `kAdd` обрабатывает сложение datetime + duration: +```python +def kAdd(l, r): + if isinstance(l, _dt.datetime) and isinstance(r, EloDuration): + return _elo_dt_plus(l, r) + if isinstance(l, EloDuration) and isinstance(r, _dt.datetime): + return _elo_dt_plus(r, l) + if isinstance(l, EloDuration) and isinstance(r, EloDuration): + return l.plus(r) + return l + r +``` + +### Функции извлечения + +| Elo | Python | +|-----|--------| +| `year(date)` | `date.year` | +| `month(date)` | `date.month` | +| `day(date)` | `date.day` | +| `hour(datetime)` | `datetime.hour` | +| `minute(datetime)` | `datetime.minute` | + +### Границы периодов + +| Elo | Python | +|-----|--------| +| `SOD(d)` | `_elo_start_of_day(d)` | +| `EOD(d)` | `_elo_end_of_day(d)` | +| `SOW(d)` | `_elo_start_of_week(d)` | +| `EOW(d)` | `_elo_end_of_week(d)` | +| `SOM(d)` | `_elo_start_of_month(d)` | +| `EOM(d)` | `_elo_end_of_month(d)` | +| `SOQ(d)` | `_elo_start_of_quarter(d)` | +| `EOQ(d)` | `_elo_end_of_quarter(d)` | +| `SOY(d)` | `_elo_start_of_year(d)` | +| `EOY(d)` | `_elo_end_of_year(d)` | + +--- + +## Функции стандартной библиотеки + +### Итерация по массивам + +| Elo | Python | +|-----|--------| +| `map(arr, fn)` | `list(map(fn, arr))` | +| `filter(arr, fn)` | `list(filter(fn, arr))` | +| `reduce(arr, init, fn)` | `functools.reduce(fn, arr, init)` | +| `any(arr, fn)` | `any(map(fn, arr))` | +| `all(arr, fn)` | `all(map(fn, arr))` | + +**Пример:** +``` +Elo: reduce([1, 2, 3, 4, 5], 0, fn(acc, x ~> acc + x)) +Python: (lambda _: functools.reduce(lambda acc, x: acc + x, [1, 2, 3, 4, 5], 0)) +# Результат: 15 +``` + +### Работа со строками + +| Elo | Python | +|-----|--------| +| `length(s)` | `len(s)` | +| `upper(s)` | `s.upper()` | +| `lower(s)` | `s.lower()` | +| `trim(s)` | `s.strip()` | +| `startsWith(s, prefix)` | `s.startswith(prefix)` | +| `endsWith(s, suffix)` | `s.endswith(suffix)` | +| `contains(s, sub)` | `sub in s` | +| `split(s, sep)` | `kSplit(s, sep)` | +| `join(arr, sep)` | `sep.join(arr)` | +| `replace(s, old, new)` | `s.replace(old, new, 1)` | +| `replaceAll(s, old, new)` | `s.replace(old, new)` | + +### Работа с массивами + +| Elo | Python | +|-----|--------| +| `first(arr)` | `kFirst(arr)` | +| `last(arr)` | `kLast(arr)` | +| `at(arr, idx)` | `kAt(arr, idx)` | +| `reverse(arr)` | `list(reversed(arr))` | +| `length(arr)` | `len(arr)` | +| `isEmpty(arr)` | `len(arr) == 0` | + +### Работа с данными + +| Elo | Python | +|-----|--------| +| `fetch(data, path)` | `kFetch(data, path)` | +| `patch(data, path, value)` | `kPatch(data, path, value)` | +| `merge(a, b)` | `{**a, **b}` | +| `deepMerge(a, b)` | `kDeepMerge(a, b)` | + +### Математические функции + +| Elo | Python | +|-----|--------| +| `abs(n)` | `abs(n)` | +| `round(n)` | `round(n)` | +| `floor(n)` | `math.floor(n)` | +| `ceil(n)` | `math.ceil(n)` | + +--- + +## Guard-выражения + +Guard позволяют добавлять проверки (assertions) в выражения. + +``` +Elo: guard x > 0 in x * 2 +``` + +**Компиляция в Python:** +```python +(lambda _: (kAssert(x > 0, "guard failed"), x * 2)[-1]) +``` + +**Helper kAssert:** +```python +def kAssert(cond, msg="Assertion failed"): + if not cond: raise Exception(msg) + return True +``` + +**С пользовательским сообщением:** +``` +Elo: guard price > 0 as 'Price must be positive' in price * quantity +``` + +--- + +## Type Selectors (Селекторы типов) + +Селекторы типов позволяют парсить и конвертировать значения. + +| Elo | Python | Описание | +|-----|--------|----------| +| `Int(x)` | `int(x)` | Преобразование в целое | +| `Float(x)` | `float(x)` | Преобразование в float | +| `String(x)` | `str(x)` или `kString(x)` | Преобразование в строку | +| `Bool(x)` | `kBool(x)` | Преобразование в boolean | +| `Date(x)` | `kParseDate(x)` | Парсинг даты | +| `Datetime(x)` | `kParseDatetime(x)` | Парсинг datetime | +| `Duration(x)` | `EloDuration.from_iso(x)` | Парсинг ISO8601 duration | +| `Data(x)` | `kData(x)` | Парсинг JSON | + +**Пример:** +``` +Elo: Int('42') + Float('3.14') +Python: (lambda _: int("42") + float("3.14")) +# Результат: 45.14 +``` + +--- + +## Type Definitions (Определения типов) + +Elo поддерживает Finitio-подобные схемы для валидации данных. + +### Базовый синтаксис + +``` +Elo: let Person = { name: String, age: Int } in data |> Person +``` + +**Компиляция в Python:** +```python +(lambda _: ( + _p_Person := pSchema([ + ("name", pString, False), # (ключ, парсер, optional?) + ("age", pInt, False) + ], "closed", None), + Person := lambda v: pUnwrap(_p_Person(v, '')), + Person(data) +)[-1]) +``` + +### Как работают комбинаторы + +Python lambdas ограничены одним выражением, поэтому используется **combinator-style**: + +1. **pSchema** — создаёт парсер для объектов +2. **pArray** — создаёт парсер для массивов +3. **pUnion** — создаёт парсер для union-типов +4. **pSubtype** — добавляет constraint к базовому типу + +**Пример pSchema:** +```python +def pSchema(props, extras_mode, extras_parser=None): + known_keys = [p[0] for p in props] + def parser(v, p): + if not isinstance(v, dict): + return pFail(p, "expected object, got " + type(v).__name__) + o = {} + for key, prop_parser, optional in props: + val = v.get(key) + if optional and val is None: + continue + r = prop_parser(val, p + "." + key) + if not r["success"]: + return pFail(p, None, [r]) + o[key] = r["value"] + # ... обработка extras ... + return pOk(o, p) + return parser +``` + +### Constraints (Ограничения) + +``` +Elo: let PositiveInt = Int(i | i > 0) in 42 |> PositiveInt +``` + +**Компиляция:** +```python +(lambda _: ( + _p_PositiveInt := pSubtype(pInt, [ + ("constraint 'i > 0' failed", lambda i: i > 0) + ]), + PositiveInt := lambda v: pUnwrap(_p_PositiveInt(v, '')), + PositiveInt(42) +)[-1]) +``` + +### Опциональные поля + +``` +Elo: let Config = { name: String, debug?: Bool } in data |> Config +``` + +Третий параметр в кортеже (`True`) указывает на опциональность: +```python +pSchema([ + ("name", pString, False), # обязательное + ("debug", pBool, True) # опциональное +], "closed", None) +``` + +### Union-типы + +``` +Elo: let StringOrInt = String | Int in value |> StringOrInt +``` + +```python +pUnion([pString, pInt]) +``` + +### Массивы + +``` +Elo: let Numbers = [Int] in data |> Numbers +``` + +```python +pArray(pInt) +``` + +--- + +## Prelude (Runtime-библиотека) + +Prelude содержит все helper-функции, необходимые для выполнения скомпилированного кода. + +**Получение prelude:** +```bash +./bin/eloc --prelude-only -t python +``` + +**Содержимое prelude:** +- `import math, functools, datetime as _dt, re` +- `class EloDuration` — класс для работы с длительностями +- Helper-функции (`kAssert`, `kEq`, `kFetch`, `kPatch`, etc.) +- Парсеры (`pOk`, `pFail`, `pString`, `pInt`, `pSchema`, etc.) +- Datetime helpers (`_elo_dt`, `_elo_start_of_day`, etc.) + +--- + +## Выполнение Python-кода + +### CLI + +```bash +# Компиляция +./bin/eloc -e "2 + 3 * 4" -t python +# Вывод: (lambda _: 2 + 3 * 4) + +# Компиляция с prelude +./bin/eloc -e "TODAY + P1D" -t python -p + +# Только prelude +./bin/eloc --prelude-only -t python +``` + +### Выполнение + +```bash +# Компиляция и выполнение +(./bin/eloc --prelude-only -t python; ./bin/eloc -e "2 ^ 10" -t python; echo "(None)") | python +# Вывод: 1024 +``` + +--- + +## Особенности реализации + +### 1. Walrus Operator (`:=`) + +Требует **Python 3.8+**. Позволяет делать присваивания внутри выражений. + +### 2. Все конструкции — выражения + +Python имеет ограничения на statements внутри lambda. Решение: +- `let` → walrus + tuple + `[-1]` +- `guard` → `kAssert()` helper +- type definitions → combinator parsers + +### 3. Safe Member Access + +Вместо `_.field` (который вызовет `KeyError`) используется `_.get("field")`, возвращающий `None`. + +### 4. Приоритеты операторов + +Компилятор автоматически добавляет скобки где нужно: + +``` +Elo: (2 + 3) * 4 +Python: (2 + 3) * 4 # скобки сохранены + +Elo: 2 + 3 * 4 +Python: 2 + 3 * 4 # скобки не нужны +``` + +--- + +## Тестирование + +### Unit-тесты + +```bash +npm run test:unit +# Включает 30 тестов для Python compiler +``` + +### Acceptance-тесты (fixtures) + +```bash +PYTHON_CMD=python bash test/acceptance/test-python.sh +``` + +44 fixture-файла проверяют семантическую эквивалентность Python-кода. + +--- + +## Ограничения + +1. **Python 3.8+** — требуется для walrus operator +2. **Single expression** — все конструкции должны быть выражениями +3. **No generators** — используем `list(map(...))` вместо list comprehensions для консистентности + +--- + +## Примеры + +### Вычисление налога +``` +Elo: let price = _.price, tax = 0.21 in price * (1 + tax) +Python: (lambda _: (price := _.get("price"), tax := 0.21, price * (1 + tax))[-1]) +``` + +### Фильтрация и преобразование +``` +Elo: _.items |> filter(x ~> x.active) |> map(x ~> x.name) +Python: (lambda _: list(map(lambda x: x.get("name"), + list(filter(lambda x: x.get("active"), _.get("items")))))) +``` + +### Валидация данных +``` +Elo: let User = { + name: String, + email: String(e | contains(e, '@')), + age: Int(a | a >= 18) + } in data |> User +``` + +### Работа с датами +``` +Elo: let due = _.dueDate |> Date in + guard due > TODAY in + due - TODAY |> inDays +``` From b82900b8ee7f74bcb914eb16a29414ec148da985 Mon Sep 17 00:00:00 2001 From: cyberpsychoz Date: Wed, 28 Jan 2026 13:16:01 +0300 Subject: [PATCH 3/3] Add Python compiler documentation (EN/RU) --- PYTHON-COMPILER.md | 653 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 653 insertions(+) create mode 100644 PYTHON-COMPILER.md diff --git a/PYTHON-COMPILER.md b/PYTHON-COMPILER.md new file mode 100644 index 0000000..380196e --- /dev/null +++ b/PYTHON-COMPILER.md @@ -0,0 +1,653 @@ +# Elo Python Compiler — Technical Documentation + +## Overview + +Elo now compiles to Python, making it the fourth target language alongside JavaScript, Ruby, and PostgreSQL SQL. The compiler transforms Elo expressions into idiomatic Python code while maintaining semantic equivalence with all other targets. + +--- + +## Compiler Architecture + +### File Structure + +``` +src/ +├── compilers/ +│ └── python.ts # Main Python compiler +├── bindings/ +│ └── python.ts # stdlib bindings for Python +└── runtime.ts # Python helper functions (PY_HELPERS) +``` + +### Compilation Pipeline + +1. **Parse**: Source code → AST (`parser.ts`) +2. **Transform**: AST → Typed IR (`transform.ts`) — infers types, rewrites operators as function calls +3. **Emit**: IR → Python code (`compilers/python.ts`) — uses `StdLib` for type-based dispatch + +--- + +## Core Constructs + +### 1. Literals + +| Elo | Python | Description | +|-----|--------|-------------| +| `42` | `42` | Integer | +| `3.14` | `3.14` | Float | +| `true` | `True` | Boolean true | +| `false` | `False` | Boolean false | +| `null` | `None` | Null value | +| `'hello'` | `"hello"` | String | +| `[1, 2, 3]` | `[1, 2, 3]` | Array | +| `{a: 1, b: 2}` | `{"a": 1, "b": 2}` | Object (dict) | + +**Compilation example:** +``` +Elo: true && false +Python: (lambda _: True and False) +``` + +### 2. Arithmetic Operators + +| Elo | Python | Description | +|-----|--------|-------------| +| `+` | `+` | Addition | +| `-` | `-` | Subtraction | +| `*` | `*` | Multiplication | +| `/` | `/` | Division | +| `%` | `%` | Modulo | +| `^` | `**` | Exponentiation | + +**Important**: Elo uses `^` for power, while Python uses `**`. The compiler translates automatically. + +``` +Elo: 2 ^ 10 +Python: (lambda _: 2 ** 10) +# Result: 1024 +``` + +### 3. Logical Operators + +| Elo | Python | Description | +|-----|--------|-------------| +| `&&` | `and` | Logical AND | +| `\|\|` | `or` | Logical OR | +| `!` | `not` | Logical NOT | + +``` +Elo: true && !false +Python: (lambda _: True and not False) +``` + +### 4. Comparison Operators + +| Elo | Python | +|-----|--------| +| `<`, `>`, `<=`, `>=` | Identical | +| `==`, `!=` | Identical | + +--- + +## Let Expressions (Local Variables) + +Elo uses functional let expressions for declaring local variables. In Python, this is implemented using the **walrus operator** (`:=`), available since Python 3.8. + +**Elo syntax:** +``` +let x = 10, y = 20 in x + y +``` + +**Python compilation:** +```python +(lambda _: (x := 10, y := 20, x + y)[-1]) +``` + +**How it works:** +1. `(x := 10, y := 20, x + y)` — tuple where walrus operator assigns values +2. `[-1]` — takes the last element of the tuple (the expression result) +3. Everything is wrapped in `lambda _` to match Elo's pattern + +**Example with calculations:** +``` +Elo: let price = 100, tax = 0.21 in price * (1 + tax) +Python: (lambda _: (price := 100, tax := 0.21, price * (1 + tax))[-1]) +# Result: 121.0 +``` + +--- + +## Lambda Functions + +Elo supports anonymous functions with syntax `fn(params ~> body)` or shorthand `params ~> body`. + +**Full syntax:** +``` +fn(x ~> x * 2) +``` + +**Shorthand syntax:** +``` +x ~> x * 2 +``` + +**Python compilation:** +```python +lambda x: x * 2 +``` + +**Example with map:** +``` +Elo: map([1, 2, 3], fn(x ~> x * 2)) +Python: (lambda _: list(map(lambda x: x * 2, [1, 2, 3]))) +# Result: [2, 4, 6] +``` + +--- + +## Data Access + +### The `_` Variable (Input) + +Every Elo expression receives input data through the `_` variable. In Python, it's passed as the lambda argument. + +``` +Elo: _.price * 1.21 +Python: (lambda _: _.get("price") * 1.21) +``` + +**Important**: The compiler uses `.get()` for safe dict access, returning `None` for missing keys instead of raising an exception. + +### DataPath (Data Paths) + +DataPath is a special syntax for navigating data structures. + +``` +Elo: .user.address.city +Python: ["user", "address", "city"] +``` + +Used with the `fetch()` function: +``` +Elo: fetch(data, .user.name) +Python: (lambda _: kFetch(data, ["user", "name"])) +``` + +--- + +## Pipe Operator (`|>`) + +The pipe operator allows writing transformation chains left-to-right (Elixir-style). + +``` +Elo: [1, 2, 3] |> map(x ~> x * 2) |> filter(x ~> x > 2) +``` + +**Python compilation (nested calls):** +```python +(lambda _: list(filter(lambda x: x > 2, list(map(lambda x: x * 2, [1, 2, 3]))))) +``` + +**Result:** `[4, 6]` + +--- + +## Alternative Operator (`|`) + +The alternative operator returns the first non-None value from a chain. + +``` +Elo: _.value | _.default | 0 +``` + +**Python compilation:** +```python +(lambda _: (_alt0) if (_alt0 := _.get("value")) is not None else + ((_alt1) if (_alt1 := _.get("default")) is not None else (0))) +``` + +**How it works:** +1. Check `_.value`, store in `_alt0` +2. If `_alt0 is not None`, return it +3. Otherwise check `_.default`, store in `_alt1` +4. If `_alt1 is not None`, return it +5. Otherwise return `0` + +--- + +## Temporal Types + +### Dates and DateTime + +| Elo | Python | Description | +|-----|--------|-------------| +| `TODAY` | `_elo_dt(now.year, now.month, now.day)` | Current date | +| `NOW` | `_dt.datetime.now()` | Current datetime | +| `D2024-01-15` | `_elo_dt(2024, 1, 15)` | Date literal | +| `D2024-01-15T10:30:00Z` | `_elo_dt(2024, 1, 15, 10, 30, 0)` | Datetime literal | + +**Helper function `_elo_dt`:** +```python +def _elo_dt(y, mo, d, h=0, mi=0, s=0): + return _dt.datetime(y, mo, d, h, mi, s) +``` + +### Durations + +ISO8601 durations compile to the `EloDuration` class: + +``` +Elo: P1D # 1 day +Python: EloDuration.from_iso("P1D") + +Elo: PT2H30M # 2 hours 30 minutes +Python: EloDuration.from_iso("PT2H30M") + +Elo: P1Y2M3D # 1 year 2 months 3 days +Python: EloDuration.from_iso("P1Y2M3D") +``` + +### Date Arithmetic + +``` +Elo: TODAY + P1D +Python: (lambda _: kAdd(_elo_dt(...), EloDuration.from_iso("P1D"))) +``` + +The `kAdd` helper handles datetime + duration addition: +```python +def kAdd(l, r): + if isinstance(l, _dt.datetime) and isinstance(r, EloDuration): + return _elo_dt_plus(l, r) + if isinstance(l, EloDuration) and isinstance(r, _dt.datetime): + return _elo_dt_plus(r, l) + if isinstance(l, EloDuration) and isinstance(r, EloDuration): + return l.plus(r) + return l + r +``` + +### Extraction Functions + +| Elo | Python | +|-----|--------| +| `year(date)` | `date.year` | +| `month(date)` | `date.month` | +| `day(date)` | `date.day` | +| `hour(datetime)` | `datetime.hour` | +| `minute(datetime)` | `datetime.minute` | + +### Period Boundaries + +| Elo | Python | +|-----|--------| +| `SOD(d)` | `_elo_start_of_day(d)` | +| `EOD(d)` | `_elo_end_of_day(d)` | +| `SOW(d)` | `_elo_start_of_week(d)` | +| `EOW(d)` | `_elo_end_of_week(d)` | +| `SOM(d)` | `_elo_start_of_month(d)` | +| `EOM(d)` | `_elo_end_of_month(d)` | +| `SOQ(d)` | `_elo_start_of_quarter(d)` | +| `EOQ(d)` | `_elo_end_of_quarter(d)` | +| `SOY(d)` | `_elo_start_of_year(d)` | +| `EOY(d)` | `_elo_end_of_year(d)` | + +--- + +## Standard Library Functions + +### Array Iteration + +| Elo | Python | +|-----|--------| +| `map(arr, fn)` | `list(map(fn, arr))` | +| `filter(arr, fn)` | `list(filter(fn, arr))` | +| `reduce(arr, init, fn)` | `functools.reduce(fn, arr, init)` | +| `any(arr, fn)` | `any(map(fn, arr))` | +| `all(arr, fn)` | `all(map(fn, arr))` | + +**Example:** +``` +Elo: reduce([1, 2, 3, 4, 5], 0, fn(acc, x ~> acc + x)) +Python: (lambda _: functools.reduce(lambda acc, x: acc + x, [1, 2, 3, 4, 5], 0)) +# Result: 15 +``` + +### String Functions + +| Elo | Python | +|-----|--------| +| `length(s)` | `len(s)` | +| `upper(s)` | `s.upper()` | +| `lower(s)` | `s.lower()` | +| `trim(s)` | `s.strip()` | +| `startsWith(s, prefix)` | `s.startswith(prefix)` | +| `endsWith(s, suffix)` | `s.endswith(suffix)` | +| `contains(s, sub)` | `sub in s` | +| `split(s, sep)` | `kSplit(s, sep)` | +| `join(arr, sep)` | `sep.join(arr)` | +| `replace(s, old, new)` | `s.replace(old, new, 1)` | +| `replaceAll(s, old, new)` | `s.replace(old, new)` | + +### Array Functions + +| Elo | Python | +|-----|--------| +| `first(arr)` | `kFirst(arr)` | +| `last(arr)` | `kLast(arr)` | +| `at(arr, idx)` | `kAt(arr, idx)` | +| `reverse(arr)` | `list(reversed(arr))` | +| `length(arr)` | `len(arr)` | +| `isEmpty(arr)` | `len(arr) == 0` | + +### Data Functions + +| Elo | Python | +|-----|--------| +| `fetch(data, path)` | `kFetch(data, path)` | +| `patch(data, path, value)` | `kPatch(data, path, value)` | +| `merge(a, b)` | `{**a, **b}` | +| `deepMerge(a, b)` | `kDeepMerge(a, b)` | + +### Math Functions + +| Elo | Python | +|-----|--------| +| `abs(n)` | `abs(n)` | +| `round(n)` | `round(n)` | +| `floor(n)` | `math.floor(n)` | +| `ceil(n)` | `math.ceil(n)` | + +--- + +## Guard Expressions + +Guards allow adding assertions to expressions. + +``` +Elo: guard x > 0 in x * 2 +``` + +**Python compilation:** +```python +(lambda _: (kAssert(x > 0, "guard failed"), x * 2)[-1]) +``` + +**Helper kAssert:** +```python +def kAssert(cond, msg="Assertion failed"): + if not cond: raise Exception(msg) + return True +``` + +**With custom message:** +``` +Elo: guard price > 0 as 'Price must be positive' in price * quantity +``` + +--- + +## Type Selectors + +Type selectors allow parsing and converting values. + +| Elo | Python | Description | +|-----|--------|-------------| +| `Int(x)` | `int(x)` | Convert to integer | +| `Float(x)` | `float(x)` | Convert to float | +| `String(x)` | `str(x)` or `kString(x)` | Convert to string | +| `Bool(x)` | `kBool(x)` | Convert to boolean | +| `Date(x)` | `kParseDate(x)` | Parse date | +| `Datetime(x)` | `kParseDatetime(x)` | Parse datetime | +| `Duration(x)` | `EloDuration.from_iso(x)` | Parse ISO8601 duration | +| `Data(x)` | `kData(x)` | Parse JSON | + +**Example:** +``` +Elo: Int('42') + Float('3.14') +Python: (lambda _: int("42") + float("3.14")) +# Result: 45.14 +``` + +--- + +## Type Definitions + +Elo supports Finitio-like schemas for data validation. + +### Basic Syntax + +``` +Elo: let Person = { name: String, age: Int } in data |> Person +``` + +**Python compilation:** +```python +(lambda _: ( + _p_Person := pSchema([ + ("name", pString, False), # (key, parser, optional?) + ("age", pInt, False) + ], "closed", None), + Person := lambda v: pUnwrap(_p_Person(v, '')), + Person(data) +)[-1]) +``` + +### How Combinators Work + +Python lambdas are limited to single expressions, so we use **combinator-style**: + +1. **pSchema** — creates parser for objects +2. **pArray** — creates parser for arrays +3. **pUnion** — creates parser for union types +4. **pSubtype** — adds constraint to base type + +**pSchema example:** +```python +def pSchema(props, extras_mode, extras_parser=None): + known_keys = [p[0] for p in props] + def parser(v, p): + if not isinstance(v, dict): + return pFail(p, "expected object, got " + type(v).__name__) + o = {} + for key, prop_parser, optional in props: + val = v.get(key) + if optional and val is None: + continue + r = prop_parser(val, p + "." + key) + if not r["success"]: + return pFail(p, None, [r]) + o[key] = r["value"] + # ... extras handling ... + return pOk(o, p) + return parser +``` + +### Constraints + +``` +Elo: let PositiveInt = Int(i | i > 0) in 42 |> PositiveInt +``` + +**Compilation:** +```python +(lambda _: ( + _p_PositiveInt := pSubtype(pInt, [ + ("constraint 'i > 0' failed", lambda i: i > 0) + ]), + PositiveInt := lambda v: pUnwrap(_p_PositiveInt(v, '')), + PositiveInt(42) +)[-1]) +``` + +### Optional Fields + +``` +Elo: let Config = { name: String, debug?: Bool } in data |> Config +``` + +The third parameter in the tuple (`True`) indicates optionality: +```python +pSchema([ + ("name", pString, False), # required + ("debug", pBool, True) # optional +], "closed", None) +``` + +### Union Types + +``` +Elo: let StringOrInt = String | Int in value |> StringOrInt +``` + +```python +pUnion([pString, pInt]) +``` + +### Arrays + +``` +Elo: let Numbers = [Int] in data |> Numbers +``` + +```python +pArray(pInt) +``` + +--- + +## Prelude (Runtime Library) + +The prelude contains all helper functions needed to execute compiled code. + +**Getting the prelude:** +```bash +./bin/eloc --prelude-only -t python +``` + +**Prelude contents:** +- `import math, functools, datetime as _dt, re` +- `class EloDuration` — class for working with durations +- Helper functions (`kAssert`, `kEq`, `kFetch`, `kPatch`, etc.) +- Parsers (`pOk`, `pFail`, `pString`, `pInt`, `pSchema`, etc.) +- Datetime helpers (`_elo_dt`, `_elo_start_of_day`, etc.) + +--- + +## Executing Python Code + +### CLI + +```bash +# Compilation +./bin/eloc -e "2 + 3 * 4" -t python +# Output: (lambda _: 2 + 3 * 4) + +# Compilation with prelude +./bin/eloc -e "TODAY + P1D" -t python -p + +# Prelude only +./bin/eloc --prelude-only -t python +``` + +### Execution + +```bash +# Compile and execute +(./bin/eloc --prelude-only -t python; ./bin/eloc -e "2 ^ 10" -t python; echo "(None)") | python3 +# Output: 1024 +``` + +--- + +## Implementation Details + +### 1. Walrus Operator (`:=`) + +Requires **Python 3.8+**. Allows assignments inside expressions. + +### 2. Everything is an Expression + +Python has restrictions on statements inside lambda. Solutions: +- `let` → walrus + tuple + `[-1]` +- `guard` → `kAssert()` helper +- type definitions → combinator parsers + +### 3. Safe Member Access + +Instead of `_.field` (which raises `KeyError`), we use `_.get("field")`, returning `None`. + +### 4. Operator Precedence + +The compiler automatically adds parentheses where needed: + +``` +Elo: (2 + 3) * 4 +Python: (2 + 3) * 4 # parentheses preserved + +Elo: 2 + 3 * 4 +Python: 2 + 3 * 4 # no parentheses needed +``` + +--- + +## Testing + +### Unit Tests + +```bash +npm run test:unit +# Includes 30 tests for Python compiler +``` + +### Acceptance Tests (fixtures) + +```bash +# Unix +python3 test/acceptance/test-python.sh + +# Windows +PYTHON_CMD=python bash test/acceptance/test-python.sh +``` + +44 fixture files verify semantic equivalence of Python code. + +--- + +## Limitations + +1. **Python 3.8+** — required for walrus operator +2. **Single expression** — all constructs must be expressions +3. **No generators** — we use `list(map(...))` instead of list comprehensions for consistency + +--- + +## Examples + +### Tax Calculation +``` +Elo: let price = _.price, tax = 0.21 in price * (1 + tax) +Python: (lambda _: (price := _.get("price"), tax := 0.21, price * (1 + tax))[-1]) +``` + +### Filter and Transform +``` +Elo: _.items |> filter(x ~> x.active) |> map(x ~> x.name) +Python: (lambda _: list(map(lambda x: x.get("name"), + list(filter(lambda x: x.get("active"), _.get("items")))))) +``` + +### Data Validation +``` +Elo: let User = { + name: String, + email: String(e | contains(e, '@')), + age: Int(a | a >= 18) + } in data |> User +``` + +### Working with Dates +``` +Elo: let due = _.dueDate |> Date in + guard due > TODAY in + due - TODAY |> inDays +```