Skip to content

RFC-019: Типовая гомоиконичность (Typed Homoiconicity) - Синтаксис как тип

⚠️ Постоянное экспериментальное уведомление: Это исследовательский эксперимент, направленный на проверку осуществимости языковой конструкции "синтаксис как тип". Данный RFC никогда не будет слит, независимо от результатов он не попадёт в ветку dev/main. Экспериментальная ветка будет удалена или архивирована после завершения.

  • Цель эксперимента: Оценить сложность реализации и потенциальную ценность типовой гомоиконичности
  • Стоп-сигнал: Отказ при отсутствии прогресса в течение 6 месяцев
  • Критерий успеха: Возможность запустить хотя бы одно пользовательское ключевое слово (полный цикл: парсинг → компиляция → выполнение)

Не гарантируется слияние в main ветку, в будущем RFC может быть отклонён или заброшен по различным причинам. Не используйте эту функциональность в продакшене.

⚠️ Пояснение к позиционированию: Данный RFC представляет собой мысленный эксперимент в области дизайна языка, не предоставляя инженерного решения. Для практического расширяемого режима парсинга см. Rust syn::Parse или Haskell parsec.


Аннотация

Данный RFC предлагает радикальный эксперимент в дизайне языка: сделать синтаксическую структуру языка частью системы типов.

Ключевая идея заимствована из Lisp — "код как данные" (гомоиконичность), но реализована через статическую систему типов:

  • Синтаксическое дерево (AST) является типом
  • Ключевые слова — это предопределённые экземпляры типов
  • Пользователи могут расширять синтаксис языка через определение типов

Это означает: язык сам становится набором composable, расширяемых "строительных блоков".


Мотивация

Зачем нужен этот эксперимент?

  1. Стремление к унификации: Устранить "ключевые слова" как особый синтаксический элемент, сделать всё типами и функциями
  2. Расширяемость языка: Пользователи могут определять новые синтаксические конструкции так же, как определяют функции
  3. Типобезопасные макросы: Традиционные макросы (текстовая подстановка) опасны, типовая гомоиконичность обеспечивает проверку на этапе компиляции
  4. Образовательные цели: Глубокое понимание сущности дизайна языков

Связь с Lisp

Lisp давно реализует "код как данные":

lisp
; Код на Lisp сам по себе является S-expression
(if (> x 0) "positive" "negative")

Отличие данного эксперимента: статическая система типов усиливает эту идею.


Предложение

Ключевые концепции

1. Синтаксическое дерево как тип (AST as Type)

yaoxiang
// Узлы синтаксического дерева являются типами
If: Type = { condition: Expr, then: Block, else: Block }
While: Type = { condition: Expr, body: Block }
Return: Type = { value: Expr }
Block: Type = { statements: Array[Expr] }
Let: Type = { name: String, value: Expr, body: Expr }
Function: Type = { params: Array[Param], body: Expr }
Call: Type = { func: Expr, args: Array[Expr] }

// Базовые типы
Literal: Type = { value: Int }
StringLiteral: Type = { value: String }
Variable: Type = { name: String }

2. Ключевые слова = функции, обрабатывающие типы

yaoxiang
//Evaluator — это функции, обрабатывающие эти типы
eval_if: (node: If, env: Env) -> Value = ...
eval_while: (node: While, env: Env) -> Value = ...
eval_return: (node: Return, env: Env) -> Value = ...
eval_block: (node: Block, env: Env) -> Value = ...

// Компилятор тоже может быть функцией
compile_if: (node: If, ctx: CompileContext) -> IR = ...
compile_while: (node: While, ctx: CompileContext) -> IR = ...

3. Типы несут правила парсинга (ключевая инновация)

Это ключевой момент эксперимента: тип не только описывает данные, но и несёт правила разбора кода.

yaoxiang
// Тип синтаксического правила
SyntaxRule: Type = {
    // Как разобрать код этого типа
    parse: (token_stream: TokenStream) -> (Self, remaining_tokens)

    // Как скомпилировать/вычислить экземпляр типа
    compile: (node: Self, ctx: CompileContext) -> IR
    eval: (node: Self, env: Env) -> Value
}

// Синтаксическое правило для типа IF
IF: SyntaxRule = {
    // Разбор "if (cond) { then } else { else }"
    parse: (tokens: TokenStream) -> (If, remaining) = {
        consume("if")
        cond = parse_expression(tokens)
        consume("{")
        then_block = parse_block(tokens)
        consume("}")
        consume("else")
        consume("{")
        else_block = parse_block(tokens)
        consume("}")
        return If(cond, then_block, else_block), tokens
    }

    eval: (node: If, env: Env) -> Value = {
        if eval(node.condition, env) != 0 {
            return eval(node.then, env)
        } else {
            return eval(node.else, env)
        }
    }
}

4. Расширение синтаксиса пользователем

Пользователи могут определять собственные "ключевые слова":

yaoxiang
// Пользователь определяет новую синтаксическую конструкцию: unless
Unless: SyntaxRule = {
    parse: (tokens: TokenStream) -> (If, remaining) = {
        consume("unless")
        cond = parse_expression(tokens)
        consume("{")
        body = parse_block(tokens)
        consume("}")
        // unless эквивалентен if (!cond) { body }
        return If(Not(cond), body, Block([])), tokens
    }
}

// Использование
unless x > 0 {
    print("x is not positive")
}

// Раскрывается в
if !(x > 0) {
    print("x is not positive")
}

Примеры

Полный пример: пользовательский синтаксис цикла

yaoxiang
// Определяем цикл "times": n.times { ... } выполняется n раз
TimesLoop: SyntaxRule = {
    parse: (tokens: TokenStream) -> (While, remaining) = {
        receiver = parse_expression(tokens)  // Получаем число
        consume(".times")
        consume("{")
        body = parse_block(tokens)
        consume("}")
        // Преобразуем в while-цикл
        counter_var = gensym("i")
        return While(
            Less(Variable(counter_var), receiver),
            Block([
                body,
                Assign(counter_var, Add(Variable(counter_var), Literal(1)))
            ])
        ), tokens
    }
}

// Использование
5.times {
    print("Hello!")
}

// Раскрывается в
i = 0
while i < 5 {
    print("Hello!")
    i = i + 1
}

Пример: Синтаксис сопоставления с образцом

yaoxiang
// Пользователь определяет сопоставление с образцом
Match: SyntaxRule = {
    parse: (tokens: TokenStream) -> (MatchNode, remaining) = {
        subject = parse_expression(tokens)
        consume("{")
        cases = []
        while !check("}") {
            pattern = parse_pattern(tokens)
            consume("=>")
            body = parse_expression(tokens)
            cases.push((pattern, body))
        }
        consume("}")
        return MatchNode(subject, cases), tokens
    }
}

// Использование
match x {
    0 => "zero",
    1 => "one",
    n if n > 10 => "big",
    _ => "other"
}

Детальный дизайн

Архитектура системы

┌─────────────────────────────────────────────────────┐
│                    Исходный код                      │
└─────────────────┬───────────────────────────────────┘


┌─────────────────────────────────────────────────────┐
│              Синтаксический анализатор (Parser)     │
│  - Распознавание ключевых слов                      │
│  - Поиск соответствующего типа SyntaxRule           │
│  - Вызов метода parse типа                         │
└─────────────────┬───────────────────────────────────┘


┌─────────────────────────────────────────────────────┐
│              AST (экземпляры типов)                  │
│  If, While, Match, TimesLoop...                     │
└─────────────────┬───────────────────────────────────┘


┌─────────────────────────────────────────────────────┐
│              Компилятор/Интерпретатор               │
│  - Вызов методов compile/eval типа                 │
│  - Генерация целевого кода или выполнение           │
└─────────────────────────────────────────────────────┘

Ключевые технические проблемы

1. Функционализация потока управления

Проблема: if должен вычислять только одну ветку, нельзя использовать обычный вызов функции.

Решение: Передавать thunk (отложенное вычисление)

yaoxiang
// Внутреннее представление после компиляции
If: Type = {
    condition: Expr,
    then: () -> Value,  // thunk, отложенное вычисление
    else: () -> Value
}

2. Нелокальный возврат return

Проблема: return должен выходить из нескольких уровней функций.

Решения:

  • Вариант A: CPS-преобразование на этапе компиляции
  • Вариант B: Использование Result/Either монады
  • Вариант C: Ограничение области видимости return

3. Синтаксическая неоднозначность

Проблема: Как различить if(x > 0) { 1 } — вызов функции или ключевое слово?

Решения:

  • Ключевые слова используют специальный синтаксис (например, if ... { } else { })
  • Или ограничение через систему типов

4. Бесконечная рекурсия

Проблема: Пользователь может определить самореферентные синтаксические правила.

Решение: Обнаружение циклических зависимостей на этапе компиляции


Связь с существующими системами

Связь с RFC-010 (Унифицированный синтаксис типов)

RFC-010 реализовал унифицированный синтаксис name: type = value, данный RFC является его развитием:

RFC-010Данный RFC
Переменные, функции, типы — всё name: type = valueКлючевые слова тоже name: type = value
Типы являются значениямиСинтаксические правила являются значениями
Type — метатипSyntaxRule — метатип синтаксиса

Сравнение с Lisp/макросами

ХарактеристикаМакросы LispДанный эксперимент
Представление кодаS-expression (списки)Экземпляры типов
Способ расширенияdefmacroОпределение типа SyntaxRule
ТипобезопасностьСлабая (текстовая подстановка)Сильная (проверка типов)
Время парсингаruntime/compile-timeЭтап компиляции
Поддержка IDEСлабаяСильная (информация о типах)

План работы с ветками

Экспериментальная ветка

Имя ветки: exp/typed-homoiconicity
Создаётся от ветки dev

Важно:

  • Это экспериментальная ветка, не будет часто сливаться с dev
  • Может долгое время разрабатываться независимо
  • Не гарантируется слияние в main
  • В случае неудачи эксперимента ветка будет удалена

Этапы разработки

⚠️ Верхний временной лимит эксперимента: 6 месяцев

ФазаЦельОжидаемое времяПримечания
Phase 1Proof of concept: AST-типы на существующем синтаксисе2 недели
Phase 2Реализация базового evaluator2 неделиКлючевая проблема: управление потоком if/return
Phase 3Реализация правил парсинга для типа SyntaxRule3 недели
Phase 4Расширение синтаксиса пользователем3 неделиОсновная цель: запустить хотя бы одно пользовательское ключевое слово
Phase 5Оптимизация и документация2 неделиЗавершение эксперимента

Обработка таймаута: Если Phase 2 (реализация управления потоком) не показывает прогресса более 4 недель, следует рассмотреть отказ.


Компромиссы

Преимущества

  • Предельная унификация: Устранение границы между ключевыми словами и обычным кодом
  • Расширяемость языка: Пользователи могут определять собственный синтаксис
  • Типобезопасность: Безопаснее традиционных макросов
  • Образовательная ценность: Глубокое понимание сущности языков

Недостатки

  • Сложность реализации: Требуется значительная переработка компилятора
  • Опасения по производительности: Интерпретация на runtime может быть медленной
  • Крутая кривая обучения: Абстрактные концепции, требуется понимание системы типов
  • Сомнительная практичность: Возможно избыточная инженерия

Риски

  • Эксперимент может провалиться, не найдя практического применения
  • Сложность реализации может превысить ожидания
  • Конфликт с существующими возможностями

Открытые вопросы

  • [ ] Как обрабатывать синтаксические конфликты (конфликт пользовательских правил со встроенными)?
  • [ ] Планы оптимизации производительности?
  • [ ] Нужен ли механизм импорта/экспорта синтаксиса?
  • [ ] Как интегрировать с существующей модульной системой?

Приложение

Глоссарий терминов

ТерминОпределение
Гомоиконичность (Homoiconicity)Код и данные используют одинаковое представление
AST (Синтаксическое дерево)Абстрактное синтаксическое представление программы
SyntaxRuleТип, несущий правила синтаксического разбора
ThunkОбёртка функции для отложенного вычисления
CPSContinuation Passing Style, стиль передачи продолжений

Библиография


Жизненный цикл и судьба

┌─────────────┐
│   Черновик  │  ← Текущее состояние
└──────┬──────┘


       ⚠️ Постоянная экспериментальная ветка (exp/typed-homoiconicity)

       Возможные результаты:
       ├─► Успешная проверка → Архивировать, никогда не сливать
       ├─► Неудача → Удалить ветку
       └─► Таймаут → Отказ и удаление

       ⚠️ При любом исходе данный RFC никогда не будет слит

⚠️ Важное напоминание: Это исследовательский эксперимент, никогда не будет слит. Не полагайтесь на эту функциональность в продакшен-коде.