RealV
RealV

RealV реализован в виде кастомного интерпретатора на F# (.NET 8). Архитектура классическая для функциональных интерпретаторов, но без сторонних комбинаторов — лексер и парсер написаны вручную методом рекурсивного спуска. Это даёт полный контроль над синтаксисом и сообщениями об ошибках.

Общая схема работы

.rv файл
Исходный файл
Текст программы на RealV
Lexer.fs
Лексер
строка → Token list
Parser.Expression.fs
Парсер
токены → Expr (AST)
Eval.fs
Вычислитель
Expr + EnvValue
Program.fs
Вывод
результат → консоль
  1. Program.fs читает файл .rv и передаёт строку в интерпретатор.
  2. Lexer.tokenize проходит по строке посимвольно и возвращает список токенов.
  3. Parser.Expression.parse строит AST методом рекурсивного спуска.
  4. Eval.evaluate запускает корневой узел AST в глобальном окружении Env.
  5. Результат последней инструкции (или явный return) выводится в консоль.

AST — модуль Ast.fs

Грамматика языка описана в виде алгебраического типа данных (Discriminated Union) Expr. Каждый узел дерева соответствует конкретной синтаксической конструкции:

Базовые типы и литералы
| Number of int
| Bool   of bool
| String of string
| List   of Expr list
Переменные и ветвления
| Var of string
| If  of Expr * Expr * Expr     // условие ? true : false
| Seq of Expr list              // блок инструкций { ... }
Связывание и функции
| Lambda  of string * Expr                      // (x) => expr
| App     of Expr * Expr                        // f(x)
| Let     of string * Expr * Expr               // первое объявление
| LetRec  of string * string * Expr * Expr      // рекурсивное объявление
| Set     of string * Expr                      // мутация переменной
Особые конструкции RealV
| Raise     of Expr           // выброс исключения (! expr)
| ArrowLoop of Expr * Expr    // start -> end -> (...)
| Delay     of Expr           // ленивое создание (delay)
| Force     of Expr           // ленивое вычисление (force)

Лексер и парсер

В отличие от инструментов вроде FParsec, RealV использует ручной посимвольный лексер и ручной парсер методом рекурсивного спуска — без внешних зависимостей.

Lexer.fs — Лексер
  • Хранит позицию в строке, самостоятельно собирает идентификаторы, числа и строки.
  • Поддерживает escape-последовательности \n, \t внутри строковых литералов.
  • Пропускает однострочные // комментарии и пробельные символы.
Parser.Expression.fs — Парсер
  • Читает токены слева направо с lookahead-проверками для разрешения конфликтов.
  • Отличает объявление переменной от мутации — отслеживает, какие имена встретились впервые в блоке.
  • Синтаксис блоков { a; b; c } собирается в узел Seq[a, b, c].
  • Тернарный оператор ?: напрямую разворачивается в If(_, _, _).

Вычислитель — Eval.fs

Eval.fs рекурсивно обходит AST, преобразуя Expr в Value. Система типов значений:

Runtime/Eval.fs — тип Value
type Value =
    | VNumber  of int
    | VBool    of bool
    | VUnit
    | VString  of string
    | VList    of Value list
    | VClosure of string * Expr * Env   // пользовательская функция
    | VPrim    of (Value -> Value)      // встроенная функция среды
    | VThunk   of Thunk                 // отложенное вычисление

RealV допускает локальную мутабельность и сайд-эффекты (файловый I/O) — в этом его принципиальное отличие от чисто функциональных языков.

Окружение и мутабельность (Env)

Окружение исполнения (Env) реализовано поверх изменяемого .NET словаря Dictionary<string, Value> с цепочкой родительских окружений:

Runtime/Eval.fs — тип Env
and Env(parent: Env option) =
    let store = Dictionary<string, Value>()
  • Get — ищет по локальному словарю, затем поднимается к parent.
  • Define — добавляет переменную в текущий блок (узел Let).
  • Set — перезаписывает существующую переменную в текущей или родительской области видимости.
Такое устройство позволяет языку вести себя привычно для программистов из C/JS: значения можно переназначать через = после инициализации.

Функции, замыкания и рекурсия

Именованные функции разворачиваются во время парсинга в узел LetRec(name, arg, body, ...). В Eval имя связывается с фиктивным замыканием до вычисления тела — это решает проблему самоссылки при рекурсии без дополнительных ключевых слов.

VClosure(argName, bodyExpr, env) «запоминает» ссылку на тот Env, в котором функция была создана — это обеспечивает полноценную работу лексических замыканий.

Пример: замыкание захватывает переменную из внешнего окружения
makeAdder(x) = {
  return (y) => x + y  // лямбда захватывает x из Env makeAdder
}

add10 = makeAdder(10)  // создаётся VClosure("y", x+y, Env{x=10})
add10(3)               // → 13  (x берётся из замкнутого Env)

Ленивые вычисления (Thunks)

Отложенные вычисления с кэшированием управляются типом Thunk:

Runtime/Eval.fs — тип Thunk
and Thunk = { mutable Cell: Value option; Compute: unit -> Value }
  • delay(expr) оборачивает выражение в Thunk с Cell = None.
  • При force(thunk) с Cell = None вызывается Compute(), результат кладётся в Cell = Some(val).
  • При повторном force — мгновенно возвращается кэшированное значение.
x = delay(!123)  // не упадёт
force(x)         // разворачивается и падает с ошибкой

Циклы и диапазоны (ArrowLoop)

Конструкция val1 -> val2 -> (i) => body под капотом вычисляет стартовое и конечное значения, затем в цикле while вызывает правую лямбду, передавая текущий индекс. Диапазон включает обе границы — 1 -> 5 итерирует i = 1, 2, 3, 4, 5. Это позволяет писать короткие итераторы без явной рекурсии — идеально вписывается в C-подобный синтаксис поверх ФП-ядра.

Пример: сумма чисел от 1 до n через ArrowLoop
sumTo(n) = {
  result = 0
  1 -> n -> (i) => result = result + i
  return result
}

sumTo(10) // 55

Сборка и точка входа

Проект организован в две папки исходного кода: библиотека RealV и CLI RealV.Cli.

Файл проекта src/RealV.Cli/RealV.Cli.fsproj подключает ядро по порядку зависимостей — важен именно этот порядок, так как F# компилирует файлы последовательно:

src/RealV.Cli/RealV.Cli.fsproj — порядок компиляции
<ItemGroup>
  <Compile Include="..\RealV\Core\Ast.fs" />
  <Compile Include="..\RealV\Core\Tokens.fs" />
  <Compile Include="..\RealV\Parser\Lexer.fs" />
  <Compile Include="..\RealV\Parser\Parser.Expression.fs" />
  <Compile Include="..\RealV\Runtime\Eval.fs" />
  <Compile Include="Program.fs" />
</ItemGroup>

Запуск примера:

dotnet run --project src/RealV.Cli/RealV.Cli.fsproj -- examples/factorial.rv

При старте инициализируется глобальное окружение со встроенными VPrim-функциями: length, head, tail, append, slice, readFile, writeFile, appendFile и другими.

Структура проекта

Проект разделён на три корня: библиотека интерпретатора src/RealV/, CLI-обёртка src/RealV.Cli/ и расширение VS Code. Исходные файлы упорядочены по слоям — от типов до точки входа.

src/
├── RealV/                          библиотека интерпретатора
│   ├── Core/
│   │   ├── Ast.fs                  тип Expr — все узлы АСТ
│   │   └── Tokens.fs               тип Token — лексемы
│   ├── Parser/
│   │   ├── Lexer.fs                посимвольный лексер
│   │   └── Parser.Expression.fs    рекурсивный спуск → Expr
│   └── Runtime/
│       └── Eval.fs                 интерпретатор: Value, Env, Thunk
└── RealV.Cli/                      CLI-обёртка
    └── Program.fs                  точка входа, запуск интерпретатора

vscode-extension/                   расширение для VS Code
├── package.json                    манифест расширения
├── extension.js                    логика активации и кнопка запуска
├── language-configuration.json     скобки, комментарии, отступы
└── syntaxes/
    └── realv.tmLanguage.json       TextMate-грамматика для подсветки

examples/                           примеры программ на RealV
└── *.rv                            factorial, closures, lazy, io…

Слои архитектуры

Core — Типы
  • Core/Ast.fs
  • Core/Tokens.fs

Алгебраические типы данных: узлы АСТ (Expr) и лексемы (Token). Фундамент проекта — все остальные модули зависят от этих типов в одностороннем порядке.

Parser — Разбор
  • Parser/Lexer.fs
  • Parser/Parser.Expression.fs

Ручной лексер и парсер методом рекурсивного спуска без внешних зависимостей. Преобразует исходный текст сначала в список токенов, затем в дерево Expr.

Runtime — Вычислитель
  • Runtime/Eval.fs

Рекурсивный интерпретатор дерева: тип Value, окружение Env, отложенные вычисления Thunk, встроенные функции (VPrim). Единственный модуль с мутациями и I/O.

CLI — Точка входа
  • RealV.Cli/Program.fs

Читает .rv-файл, инициализирует глобальное окружение со встроенными функциями и передаёт исходный текст в интерпретатор. Выводит итоговое значение в консоль.