RealV реализован в виде кастомного интерпретатора на F# (.NET 8). Архитектура классическая для функциональных интерпретаторов, но без сторонних комбинаторов — лексер и парсер написаны вручную методом рекурсивного спуска. Это даёт полный контроль над синтаксисом и сообщениями об ошибках.
Общая схема работы
Token listExpr (AST)Expr + Env → ValueProgram.fsчитает файл.rvи передаёт строку в интерпретатор.Lexer.tokenizeпроходит по строке посимвольно и возвращает список токенов.Parser.Expression.parseстроит AST методом рекурсивного спуска.Eval.evaluateзапускает корневой узел AST в глобальном окруженииEnv.- Результат последней инструкции (или явный
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 // мутация переменной
| Raise of Expr // выброс исключения (! expr)
| ArrowLoop of Expr * Expr // start -> end -> (...)
| Delay of Expr // ленивое создание (delay)
| Force of Expr // ленивое вычисление (force)
Лексер и парсер
В отличие от инструментов вроде FParsec, RealV использует ручной посимвольный лексер и ручной парсер методом рекурсивного спуска — без внешних зависимостей.
- Хранит позицию в строке, самостоятельно собирает идентификаторы, числа и строки.
- Поддерживает escape-последовательности
\n,\tвнутри строковых литералов. - Пропускает однострочные
//комментарии и пробельные символы.
- Читает токены слева направо с lookahead-проверками для разрешения конфликтов.
- Отличает объявление переменной от мутации — отслеживает, какие имена встретились впервые в блоке.
- Синтаксис блоков
{ a; b; c }собирается в узелSeq[a, b, c]. - Тернарный оператор
?:напрямую разворачивается вIf(_, _, _).
Вычислитель — Eval.fs
Eval.fs рекурсивно обходит AST, преобразуя Expr в 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> с цепочкой родительских окружений:
and Env(parent: Env option) =
let store = Dictionary<string, Value>()
- Get — ищет по локальному словарю, затем поднимается к
parent. - Define — добавляет переменную в текущий блок (узел
Let). - Set — перезаписывает существующую переменную в текущей или родительской области видимости.
= после инициализации.
Функции, замыкания и рекурсия
Именованные функции разворачиваются во время парсинга в узел
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:
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-подобный синтаксис поверх ФП-ядра.
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# компилирует файлы последовательно:
<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/Ast.fs
- Core/Tokens.fs
Алгебраические типы данных: узлы АСТ (Expr) и лексемы (Token). Фундамент проекта — все остальные модули зависят от этих типов в одностороннем порядке.
- Parser/Lexer.fs
- Parser/Parser.Expression.fs
Ручной лексер и парсер методом рекурсивного спуска без внешних зависимостей. Преобразует исходный текст сначала в список токенов, затем в дерево Expr.
- Runtime/Eval.fs
Рекурсивный интерпретатор дерева: тип Value, окружение Env, отложенные вычисления Thunk, встроенные функции (VPrim). Единственный модуль с мутациями и I/O.
- RealV.Cli/Program.fs
Читает .rv-файл, инициализирует глобальное окружение со встроенными функциями и передаёт исходный текст в интерпретатор. Выводит итоговое значение в консоль.