# Архитектура движка Лексика

Лексус — собственный язык, его грамматика, морфология и целевая модель исполнения.
Текущий рабочий рантайм всё ещё использует Python 3: внутренние файлы
`лексика/*.lek` пока написаны на синтаксисе Python и запускаются переходным
загрузчиком. Нативный слой C11 в `ядро/` — только загрузочная реализация
лексера и части морфологии, а не полный интерпретатор.

Цель — не заменить Python на постоянный C, Rust или Go-рантайм. Цель —
собственный движок с собственными значениями, арифметикой, командами,
виртуальной машиной и стандартной средой. C11 допускается лишь как временная
ступень первой сборки; в конечной поставке пользователю не нужен ни Python,
ни C-компилятор. Нормативные правила описаны в
[контракте самостоятельного движка](КОНТРАКТ_ДВИЖКА.md).

Исходники самого движка тоже должны в конечном счёте быть обычными программами
Лексики, без маскировки синтаксиса чужого языка расширением `.lek`.

```
исходник .lek
      │
      ▼
[ лексер ]            слова, числа, речь, знаки . , ! ? ( )
      │
      ▼
[ морфология ]        склонение, спряжение, числительные, разбор
      │
      ▼
[ грамматика ]        падежный разбор → дерево смысла
      │
      ▼
[ исполнитель ]       области видимости, значения, правила
      │
      ▼
[ заплатки ] [ разум языка ] [ установщик ]
```

## 1. Что где лежит

| файл | строк | дело |
|---|---|---|
| `лексика/лексер.lek` | 214 | правила письма, разрешённые знаки, сборка речи |
| `лексика/морфология.lek` | 439 | склонение существительных, спряжение глаголов, числительные |
| `лексика/словарь.lek` | 215 | служебные слова и их формы, команды, функции, предлоги |
| `лексика/грамматика.lek` | 890 | падежный разбор, дерево смысла, операторы |
| `лексика/исполнитель.lek` | 630 | значения, области видимости, правила, встроенные действия |
| `лексика/модель.lek` | 294 | крошечная встроенная модель и внешние модели |
| `лексика/патчи.lek` | 176 | заплатки: сборка, наложение, откат |
| `лексика/упаковка.lek` | 122 | свой контейнер `LEKSUSPK`, отпечаток, удаление дерева |
| `лексика/хранение.lek` | 192 | своё чтение и запись данных, значения по-русски |
| `лексика/форматы.lek` | 105 | русские расширения файлов |
| `лексика/загрузка.lek` | 137 | свой загрузчик частей вместо importlib |
| `лексика/ошибки.lek` | 40 | ошибки письма, грамматики, смысла |
| `лексус.lek` | 449 | дела движка, беседа, справка |
| `установить.lek` | 173 | установщик, ярлык `лексус`, значок в меню |
| `проверка/тесты.lek` | 449 | 100 проверок языка |
| `проверка/проверить_документы.lek` | 58 | прогон всех примеров из документов |
| `сборка/установщик.c` | 546 | установщик для Windows на чистом WinAPI |
| `лексика/разум.lek` | 195 | своя нейронная сеть: признаки, веса, падеж и род |
| `лексика/байткод.lek` | 156 | сборка дерева смысла в команды |
| `лексика/машина.lek` | 247 | стековая машина: замыкания замыслов, кадры правил |
| `лексика/доводы.lek` | 119 | свой разбор командной строки |
| `лексика/файлы.lek` | 187 | свой слой файлов и путей |
| `сборка/обучить_разум.lek` | 236 | обучение сети на склонениях движка |
| `словари/разум.веса` | — | веса сети, 584 КБ |
| `значки/` | — | знак проекта: png для документов, ico для ярлыков и `.exe` |

Всего около четырёх тысяч восьмисот строк.

## 2. Как работает падежный разбор

Главная мысль языка: **падеж — это роль слова в предложении**, значит его
можно использовать вместо символов и префиксов.

1. Лексер отдаёт словоформу как есть: `сумму`.
2. Морфология склоняет имя `сумма` и получает таблицу форм:
   `сумма, суммы, сумме, сумму, суммой, сумме, суммы, сумм, …`
3. Движок ищет словоформу в таблицах всех заведённых имён — от ближней
   области видимости к дальней.
4. Команда заранее знает, какой падеж ей нужен:

```python
КОМАНДЫ = {
    "длина":  {"шаблон": [(None, "рд", "перечень")]},
    "взять":  {"шаблон": [("из", "рд", "перечень"), (None, None, "номер")]},
    "добавить": {"шаблон": [("в", "вн", "перечень"), (None, None, "значение")]},
}
```

Шаблон — это и есть падежное управление команды. Отсюда свободный порядок
слов там, где он естественен: `прибавить два к счёту` и `к счёту прибавить
два` дают одно и то же.

### Беглые гласные и множественное число

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

* `перечень → перечня`, `день → дня`, `конец → конца` (беглая гласная);
* `покупка → покупок`, `ручка → ручек`, `строка → строк` (родительный
  множественного);
* имя, заведённое во множественном числе (`покупки`), узнаётся и в
  родительном (`покупок`), и в единственном (`покупка`);
* имя, заведённое в косвенном падеже (`для каждого числа`), узнаётся и в
  начальной форме (`число`).

Последнее сделано так: к имени добавляются ближайшие родственные начальные
формы, и таблица форм получается объединением. Это дешевле и точнее, чем
угадывать одну «правильную» лемму.

## 3. Дерево смысла

Узлы — простые классы с русскими именами: `Число`, `Речь`, `Имя`, `Вызов`,
`Действие`, `Отрицание`, `Команда`, `Объявить`, `Присвоить`, `Изменить`,
`Сказать`, `Спросить`, `Если`, `Пока`, `Повторить`, `ДляКаждого`, `Правило`,
`Вернуть`, `Прервать`, `Продолжить`, `Подключить`.

Разбор — рекурсивный спуск с уровнями старшинства:

```
или → и → сравнение → сумма → произведение → степень → унарный → первичное
```

Терминаторы передаются множеством слов: условие ветвления останавливается на
слове `то`, условие цикла — на `повторять`, перебор — на `повторять` или
`то`. Поэтому запятые и «лишние» слова не ломают разбор.

## 4. Исполнитель

* Области видимости — цепочка `Область`; поиск имени идёт от ближней к
  дальней, поэтому правило видит свои значения, потом значения внешние.
* Правила собираются до исполнения: порядок объявления не важен,
  вложенность и возврат значения поддержаны.
* Особые исключения `Вернуться`, `ПрерватьЦикл`, `ПродолжитьЦикл`
  разматывают стек без ручных счётчиков.
* Все сообщения об ошибках — с местом в тексте и подсказкой.

### Чем поведение отличается от Питона

Логика бралась как ориентир, но правила свои:

| Питон | Лексика |
|---|---|
| блоки отступом | блоки до слова `конец` |
| `if` / `else` / `while` | `если … то` / `иначе` / `пока … повторять` |
| `def f(a, b)` | `правило f принимает a, b … конец правила` |
| `print(x)` | `сказать x.` |
| `x = 5` | `пусть x равно пяти.` или `положить пять в x.` |
| строки в кавычках | `речь … конец речи` с вставкой `(значение)` |
| `float("3.5")` | `три запятая пять` |
| истина только `True/False` | пустая речь и ноль тоже ложь |
| числа только цифрами | цифрами или словами: `сорок два` |

## 5. Разум языка: как вшита крошечная модель

`лексика/модель.lek` задаёт три состояния и один общий договор:

```python
class ЛюбаяМодель:
    def выбрать(self, слово, кандидаты, контекст): ...   # падеж, число, род
    def разобрать(self, слово, контекст): ...            # полный разбор
    def уверенность(self, слово, падеж, число): ...      # вес решения
    def толковать(self, запрос): ...                     # творческий режим
```

| состояние | кто работает |
|---|---|
| `встроенная` | `ВстроеннаяМодель` — таблицы окончаний и частот, ≈ 180 КБ, без сети |
| `выключена` | `ВыключеннаяМодель` — только правила склонения |
| `внешняя` | `ВнешняяМодель` — GGUF через llama.cpp или ONNX-энкодер |

Точки подключения в движке ровно две:

1. **Спорный падеж.** Когда одна словоформа подходит к нескольким именам,
   `Исполнитель.найти_имя` спрашивает модель, какой разбор вероятнее.
   Встроенная модель считает вес по таблице частотных окончаний; внешняя
   получает короткую подсказку и отвечает одним словом.
2. **Толкование.** `лексус толковать …` и настройка `разрешить_толкование`
   дают творческий режим. Он никогда не участвует в разборе программ: только
   человек спрашивает модель.

Отказоустойчивость: если внешняя модель не скачана, не собрана или упала,
движок молча возвращается к встроенной. Язык не зависит от сети.

### Какие образы подходят

| модель | вес | вид | зачем |
|---|---|---|---|
| `cointegrated/rubert-tiny2` | ≈ 45 МБ, 12 млн параметров | ONNX-энкодер, MIT | разбор падежа, числа, рода; проверка согласования |
| `Vikhrmodels/Vikhr-Qwen-2.5-0.5b-instruct-GGUF` | ≈ 400 МБ (Q4_K_M) | GGUF, qwen2 | русское толкование, подсказки для незнакомых слов |
| `qwen2.5:0.5b` в ollama | ≈ 398 МБ | образ для ollama | быстрый запуск без сборки llama.cpp |

Список печатает `лексус модели`. Подключение:

```sh
лексус настройка умная_модель внешняя
лексус настройка путь_к_модели ~/модели/vikhr-0.5b-Q4_K_M.gguf
лексус настройка движок_модели llama.cpp
```

## 6. Заплатки

Файл `.патч` — свой контейнер `LEKSUSPK`, ни zip, ни его библиотек:

```
LEKSUSPK          знак контейнера
u32               число записей
записи            u16 длина имени, имя в utf-8, u64 размер, u64 смещение
данные            подряд
```

Записи внутри:

| имя | что внутри |
|---|---|
| `манифест.настр` | язык, версия языка, имя, версия, автор, описание, точка входа, время сборки, отпечатки всех файлов |
| `код/…` | исходники `.lek` |
| `запуск` | оболочка-ярлык: `exec лексус запустить "имя" "$@"` |
| `читай.lek` | образец обращения к заплатке |

Манифест пишется своим хранилищем: значения по-русски (`истина`, `ложь`,
`ничто`), читается своим разбором.

* `собрать` — пакует папку программы, считает свой отпечаток FNV-1a 64 бита;
* `наложить` — распаковывает в `~/.лексус/библиотека/имя`, сверяет каждый
  файл по отпечатку, ведёт список `~/.лексус/патчи.список`;
* `откатить` — снимает заплатку целиком;
* после наложения заплатка видна как словарь правил (`подключить имя`) и
  как программа (`лексус запустить имя`).

Так «добавить программу в патч» — это одна команда, а не ручная сборка.

## 6а. Загрузчик частей

Своего механизма импорта в питоне нет, а брать чужой проект больше не хочет:
`лексика/загрузка.lek` делает эту работу сам.

* `Искатель` стоит первым в `sys.meta_path` и отвечает только за имена, у
  которых рядом лежит файл `.lek` (или каталог с `__init__.lek`);
* `ЗагрузчикЧасти` читает файл как utf-8, собирает через `compile` и исполняет
  в пространстве имён части;
* `Описание` — своё описание части: имя, файл, места подчастей;
* `загрузить` и `загрузить_каталог` грузят часть напрямую, вообще без импорта;
* всё остальное — питон, сторонние пакеты, стандартная библиотеку — искатель
  молча уступает встроенному импорту.

Точки входа (`лексус.lek`, проверки, сборщик словаря) поднимают загрузчик
восемью строками: читают файл и исполняют его в отдельном пространстве имён.
Кириллическое расширение `.лек` тут и упёрлось: искатель питона смотрит только
латиницу, поэтому расширение `.lek`.

## 7. Установщик

Установок две, и обе сделаны своими руками.

### Windows: `сборка/установщик.c` → `Лексус-установщик.exe`

Программа на чистом WinAPI, без внешних библиотек. Один файл около 22 МБ
содержит и самого установщика, и груз.

**Формат груза** — свой, дописан в конец `.exe`:

```
LEKSUS01                        знак груза
u32                             число записей
записи                          u16 длина имени, имя в utf-8,
                                u64 размер, u64 смещение от начала груза
данные                          подряд, без сжатия
LEKSUSZZ                        знак хвоста
u64 смещение груза, u64 размер  чтобы найти груз с конца файла
```

Установщик читает хвост, по нему находит груз, отдельным проходом читает
оглавление, а потом раскладывает файлы. Смещения считаются от начала
**груза**, а не файла — на этом всё и сломалось в первый раз.

Что делает:

* разбирает доводы командной строки (`--тихо`, `--удалить`);
* кладёт файлы в `%LOCALAPPDATA%\Лексус`;
* пишет `лексус.cmd` с `chcp 65001`, `PYTHONUTF8=1` и `PYTHONIOENCODING=utf-8`,
  чтобы русский вывод не превращался в `\u043b…`;
* зовёт встроенный `питон\python.exe`, а если его нет — `py -3`;
* ставит ярлыки `Лексика` на рабочий стол и в «Пуск» со значком проекта
  (`значки/Лексика.ico` лежит в грузе);
* прописывает каталог в `HKCU\Environment` и рассылает `WM_SETTINGCHANGE`;
* сам себя проверяет: запускает `лексус.cmd проверить` и печатает итог;
* `--удалить` сносит каталог установки.

Сам установщик собирается так:

```sh
sudo apt-get install mingw-w64
python3 сборка/собрать_exe.lek
```

`собрать_exe.lek` готовит груз, собирает значок через `windres`, скачивает
`python-3.12.7-embed-amd64.zip`, зовёт `x86_64-w64-mingw32-gcc`, склеивает и
проверяет оглавление обратно.

### Linux и macOS: `установить.sh` → `установить.lek`

* проверяет `python3 ≥ 3.8`;
* раскладывает `лексус.lek`, `лексика`, `примеры`, `словари`, `документы`;
* ставит ярлык `лексус` в `~/.local/bin` с подсказкой про `PATH`;
* создаёт `~/.лексус/библиотека` для заплаток;
* прогоняет проверочную программу и печатает итог;
* `--снести` убирает движок и ярлык.

### Почему `.lek`, а не `.лек`

Хотелось кириллическое расширение, и оно работало бы, но `importlib` из
питона ищет расширения только латиницей: `SourceFileLoader` с `.лек` не
подхватывается. Чтобы движок оставался набором отдельных файлов, пришлось
взять `.lek`. Это единственное место, где пришлось отступить от «всё
по-русски».

## 8. Витрина проекта

Описание репозитория, темы, ключевые слова и мета-теги страницы собраны в
`ПРОДВИЖЕНИЕ.md`. Текстовая часть витрины: `README.md`, `index.html`
(JSON-LD и карточки ссылок), `llms.txt`, `robots.txt`, `sitemap.xml`.

## 7а. Разум языка: своя сеть

Встроенная модель — не таблица, а маленькая нейронная сеть, написанная и
обученная здесь же.

* признаки: окончания в 1–5 знаков, начало слова, последняя гласная и число
  знаков после неё, длина, разряд последнего согласного, отдельные подсказки
  вроде «ость» и «ние»;
* признаки хешируются в 8192 клетки своим отпечатком FNV‑1a, поэтому словарь
  весов остаётся маленьким;
* голова на 12 классов «падеж и число» и отдельная голова на род;
* обучение — `сборка/обучить_разум.lek`: склонения движка для настоящих и
  выдуманных основ, мягкие цели для спорных форм, 60 эпох;
* веса занимают около 584 КБ; обучение и применение весов не требуют `numpy`.
  Число точности из обучающего скрипта относится только к его внутренней
  проверочной выборке и не гарантирует качество на произвольных текстах;
* `лексус разум` показывает состояние, `умная_модель табличная` возвращает
  старые таблицы, `выключена` отключает разум совсем.

## 7б. Байткод и переходная машина

```
лексус байткод примеры/факториал.lek
```

`лексика/байткод.lek` уже строит команды для части операторов, однако
некоторые операнды всё ещё содержат узлы дерева грамматики.
`лексика/машина.lek` пока является переходным исполнителем: она умеет часть
переходов и циклов, но значения выражений вычисляет старый исполнитель,
а некоторые вызовы правил проходят через узлы дерева грамматики. Поэтому
это ещё не независимая VM.

Следующий шаг — полностью собирать выражения в команды, не помещать AST-объекты
в байткод и исполнять программу только собственной моделью значений.
Подробности и критерии приёмки находятся в
[контракте движка](КОНТРАКТ_ДВИЖКА.md). Отладочный путь старого дерева:
`лексус запустить --дерево файл.lek`.

## 8а. Что уже своё

Движок постепенно перестаёт опираться на чужое. Сейчас своё:

| было | стало |
|---|---|
| `importlib.machinery` для файлов `.lek` | `лексика/загрузка.lek` |
| `json` для настроек и манифеста | `лексика/хранение.lek` |
| `zipfile` для заплаток | `лексика/упаковка.lek`, контейнер `LEKSUSPK` |
| `hashlib.sha256` | свой отпечаток FNV-1a 64 бита |
| `shutil.rmtree` | `упаковка.удалить_дерево` |
| `shutil.which` | `упаковка.где` |
| расширение `.заплатка` | `.патч` |
| настройки `настройки.json` | `настройки.настр` |
| `argparse` | `лексика/доводы.lek` |
| `os` по всему движку | `лексика/файлы.lek` |
| таблицы окончаний как разум | `лексика/разум.lek`, своя сеть на 584 КБ |
| обход дерева смысла | `лексика/байткод.lek` + `лексика/машина.lek` |

Что ещё берётся из питона и почему:

* `os` — остался только внутри `лексика/файлы.lek`, дальше его вытеснит
  свой слой системных вызовов;
* `sys` — точки входа и вывод;
* `re` — только в проверке документов;
* `math`, `random`, `time` — счёт, случайность, время выполнения;
* `onnxruntime`, `numpy`, `llama_cpp` — только если включена внешняя модель.

## 9. Короткие вопросы в беседе

`лексус беседа` распознаёт несколько коротких вопросов до передачи строки
грамматическому разборщику. Встроенные ответы работают без модели:

* `кто президент` → `Путин`;
* `кто самый богатый` → `Маск`;
* `кто мета` или `кто фейсбук` → `Цукерберг`;
* `цена рубль доллар газ нефть` → демонстрационные резервные ориентиры.

Префикс `самый` направляет запрос во внешнюю LLM, если она подключена. Перед
вызовом модели Лексус пробует получить сниппеты DuckDuckGo стандартными
средствами Python. При сетевой ошибке модель отвечает только по своим знаниям;
такой ответ не выдаётся за проверенную котировку.

## 10. Что дальше

Актуальный порядок переноса и критерии приёмки ведутся в
[ПЛАН_ПЕРЕНОСА_РАНТАЙМА.md](../ПЛАН_ПЕРЕНОСА_РАНТАЙМА.md):

1. собственная модель значений и арифметики;
2. полная сборка выражений в независимые команды;
3. виртуальная машина без вызовов старого исполнителя;
4. перенос лексера, морфологии, грамматики и стандартной среды;
5. самохостинг — компилятор и части движка на обычном синтаксисе Лексики;
6. удаление Python из рабочего пути и отказ от C-компилятора в пользовательской сборке.
