Skip to content

Commit 51d1ff1

Browse files
Самоприменение: парсер flang, написанный на flang
Третий и самый крупный слой: 3887 строк, 372 функции, 147 тотальных. Побайтовое совпадение AST с эталонным src/parser.mjs: 38 файлов .flang из 38 (включая собственный исходник) и 51 файл .fts из 51. Второе не довесок: утилит, морфизмов, теорем, файлов-функторов и скобочной поверхности нет ни в одном .flang, а разбирает их тот же парсер, и отдельный тест требует, чтобы в корпусе встретились все семь видов legacy-узлов. Плюс 98 краевых случаев, 200 случайных склеек и 93 обрезки реальных исходников по случайной строке. Диагностики сверяются по коду, тексту, строке и столбцу. Расхождений нет. Рекурсивный спуск выбран сознательно, вопреки приёму ядра FTS. Поверхность flang не построчная: «если» живёт и в строку, и блоком, «пусть» вкладывает остаток блока, скобочный диалект отступов не видит вовсе. Свёртка в дерево заставила бы сверять не с эталоном, а с собственным представлением о том, как эталон группирует токены. Здесь порядок вызовов повторяет parser.mjs функция в функцию, и расхождение всегда локально. Цена — 225 обычных функций, и она названа честно, а не спрятана. Поток идёт пачками по 32 токена: «хвост» копирует список целиком, и поедание по одному дало бы порядка 4·10^8 копий на 28 000 токенах. AST строится как «Значение» из core/json.flang, а не своим типом: иначе не выразить разницу между «ключа нет» и «ключ со значением ничто», которой пользуется эталон, и порядок вставки ключей, на который он опирается при связывании вызовов без аргументов. 1224 теста flang, 0 падений.
1 parent 92db56c commit 51d1ff1

3 files changed

Lines changed: 4480 additions & 0 deletions

File tree

flang/self/SPEC.md

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,3 +213,125 @@ newer», потому что компилятор языка написан на
213213
интерпретатора; тест поднимает лимит до ста миллионов. Это не формальность:
214214
лимит ловит превращение печати в перебор. Компилятору такое разрешено
215215
(см. начало этого файла), факт-чекеру — нет.
216+
217+
### `self/parser.flang`
218+
219+
Состояние: 372 функции, 147 тотальных, 225 обычных. AST совпадает с эталоном
220+
(`src/parser.mjs`) **побайтово** — то есть `«Печать значения»` от построенного
221+
узла и `JSON.stringify(parse(исходник))` дают одну и ту же строку — на 38
222+
исходниках `.flang` репозитория (`flang/stdlib`, `flang/core`,
223+
`flang/examples/leetcode`, `flang/self`, включая собственный исходник) и на 51
224+
модели `.fts` (`examples`, `tools`, `web`, `editors`). Плюс 98 краевых случаев,
225+
200 случайных склеек и 93 обрезанных по случайной строке исходника
226+
репозитория — с зафиксированными сидами. Отказы сверяются так же: код, текст
227+
сообщения и место. Проверка: `flang/test/self-parser.test.mjs`.
228+
229+
Корпус `.fts` здесь не украшение: конструкций наследия FTS — утилита, морфизм,
230+
теорема, файл-функтор и старая скобочная поверхность `категория X { … }` — нет
231+
ни в одном `.flang` репозитория, а разбирает их тот же `parser.mjs`. Без `.fts`
232+
половина эталона осталась бы непроверенной; тест отдельно требует, чтобы в
233+
корпусе встретились все семь видов узла `ftsLegacy`.
234+
235+
**Почему рекурсивный спуск, а не свёртка потока в дерево.** Ядро FTS
236+
(`flang/core/parser.flang`) целиком тотально: поверхность `.fts` построчная, и
237+
разбор там сведён к свёртке по строкам и группам. Здесь этот путь выбран не
238+
был, и это решение, а не упущение. Поверхность flang не построчная: `если`
239+
живёт и в строку, и блоком; тело ветки бывает выражением на той же строке и
240+
отступным блоком; `пусть` вкладывает в себя весь остаток блока; скобочная
241+
поверхность отступов не замечает вовсе. Свести это к дереву отступов можно, но
242+
тогда сверять пришлось бы не с образцом, а с собственным представлением о том,
243+
как образец группирует токены, и каждое расхождение становилось бы отладкой
244+
двух разных разборов сразу. Здесь порядок вызовов повторяет `parser.mjs`
245+
функция в функцию, поэтому расхождение всегда локально — видно, в какой именно
246+
функции оно возникло. Цена решения названа прямо: нетотальность.
247+
248+
**Поток токенов лежит пачками.** Список flang — массив, и `хвост` копирует его
249+
целиком (`builtins.mjs`). Съедать токены по одному из плоского списка значило
250+
бы копировать хвост на каждом шаге: на самом большом исходнике репозитория
251+
(28 000 токенов) это 4·10⁸ копий указателей. Поэтому поток разложен пачками по
252+
32 токена, а три токена заглядывания (`peek(0..2)` образца) лежат отдельными
253+
полями. Снятие токена стоит копирования пачки, переход к следующей пачке —
254+
копирования списка пачек; итог линеен.
255+
256+
**Нетотальные функции (225) — три причины.**
257+
258+
| Что | Почему не доказывается |
259+
|---|---|
260+
| рекурсивный спуск: «Разобрать выражение», «Разобрать строки», «Разобрать если», «Разобрать разбор», «Разобрать образец», «Разобрать постфикс», «Разобрать первичное», «Разобрать тип», «Разобрать функцию», «Разобрать запись», «Разобрать утилиту», «Разобрать скобочный документ» и их спутники (около 200 функций) | убывает остаток потока токенов, а поток лежит внутри записи «Разборщик» и меняется целиком: анализ (`src/totality.mjs`) считает частью значения хвост списка, голову, поле записи и поле варианта, но не «состояние стало меньше». Взаимная рекурсия идёт через «Разобрать выражение» и охватывает почти весь файл, поэтому нетотальна вся компонента сразу |
261+
| циклы по потоку: «Пропустить переводы», «Пропустить разделители», «Войти в блок», «Выйти из блока», «Собрать домен», «Собрать кодомен», «Собрать части типа» | то же самое в чистом виде: рекурсия по остатку потока |
262+
| обход строки по символам и по подстрокам: «Основа имени» (через «Нижний регистр» лексера), «Повторная замена», «Обрезать слева», «Обрезать справа», «Собрать тип» и связывание имён, которое их зовёт («Подобрать основу», «Разрешить среди локальных», «Разрешить локальное», «Связать узел», «Разрешить узел», «Отказ имени») | убывает разность «длина минус позиция», то есть число. Та же нехватка «символов строки», что названа в долгах `self/lexer.flang`, `self/emit-c.flang` и `stdlib/strings.flang` |
263+
264+
Разложение потока пачками, поиск ключа в узле, подстановка ключа, связывание
265+
вызовов без аргументов и все таблицы ключевых слов — тотальны: там убывание
266+
структурное. Отсюда 147 доказанных функций из 372.
267+
268+
**AST — обобщённое значение, а не свой типизированный узел.** Узел AST здесь —
269+
это «Значение» из `flang/core/json.flang`, та же сумма «скаляр | список |
270+
запись», что читает `self/emit-c.flang`. Причины три, и все три обязательные,
271+
а не удобные:
272+
273+
1. печать в JSON берётся готовой и уже доказанной побайтово. Побайтовое
274+
совпадение — единственный критерий верности, а вторая печать означала бы
275+
второй набор решений про порядок ключей и экранирование;
276+
2. эталон различает «ключа нет» и «ключ со значением ничто»: `optional` поля,
277+
`legacy` программы, `arg` утверждения, `only` импорта. Запись flang этого
278+
различить не может — поле записи есть всегда;
279+
3. порядок ключей в JSON задаётся порядком вставки, и эталон им пользуется:
280+
`bindNullaryCalls` переписывает `var` в `call`, снимая и заново кладя `name`
281+
и `span`, чтобы получить порядок обычного вызова. Список пар ключ-значение
282+
это воспроизводит, запись — нет.
283+
284+
**Пометка узла вместо `WeakSet`.** Эталон помечает узлы `var`, ожидающие
285+
разрешения, слабым множеством, а связывание вызовов без аргументов переписывает
286+
их на месте. Здесь пометка живёт ключом самого узла — `?ждёт` и `?свободное`, —
287+
а «Связать вызовы» обходит готовый AST и снимает её. Ключи начинаются со знака
288+
вопроса, которого в AST быть не может, и до печати не доживают ни в одном из
289+
проверенных исходников.
290+
291+
**Расхождения с эталоном.** Первые три — те же, что уже записаны у лексера, и
292+
здесь они лишь наследуются; четвёртое своё.
293+
294+
1. **Нормализация NFC не выполняется.** Эталон приводит к NFC имя
295+
(`expectName`), имя поля записи, имя параметра, строку типа скобочной
296+
поверхности (`formatType`) и имена функторов композиции. Встроенной формы для
297+
этого в языке нет. Расхождение: имя, записанное разложенной формой, эталон
298+
отдаёт составленным, парсер на flang — разложенным.
299+
2. **`/^\p{Lu}/u` заменено явным алфавитом заглавных** — латиница и кириллица,
300+
таблица лексера. Этой проверкой различаются «тип, вариант или функция»
301+
(пишутся с прописной, не склоняются) и «локальное имя» — и в разрешении
302+
падежей, и в образце. Расхождение: имя, начинающееся с греческой или
303+
армянской прописной, эталон не склоняет, а парсер на flang попытается
304+
подобрать ему основу.
305+
3. **`toLowerCase()` в поиске основы сделан по той же таблице.** Окончания
306+
снимаются с имени, приведённого к нижнему регистру «Нижним регистром»
307+
лексера, а он знает латиницу и кириллицу. Расхождение видно только вместе с
308+
пунктом 2.
309+
4. **Регулярные выражения `formatType` выражены заменами.** Эталон приводит
310+
строку типа скобочной поверхности четырьмя `replace` с `\s+` и `\s*`;
311+
регулярных выражений в языке нет. Части склеены через `join(" ")`, поэтому
312+
пробельный пробег — это всегда пробелы, и повторная замена даёт то же самое.
313+
Остаётся один край: две вертикальные черты подряд (`A||B`) эталон разведёт
314+
как `A | | B`, а эта печать оставит два пробела между ними. В типах моделей
315+
репозитория такого нет, и `Map<string, number>[]`, `string | null`,
316+
`A & B` совпадают побайтово.
317+
318+
Столбцы токенов, таблица `\uXXXX` и алфавиты `ID_Start`/`ID_Continue` — долги
319+
лексера, парсер их только передаёт дальше: место диагностики он берёт из токена
320+
и сам ничего про символы не решает.
321+
322+
**Отказ теряет программу целиком.** Эталон бросает `flangError` из середины
323+
спуска и наружу не отдаёт ничего. Здесь беда едет значением, первая записанная
324+
побеждает, а все предикаты потока после неё отвечают так, чтобы любой цикл
325+
немедленно закончился: «Это вид», «Это слово» и «Знак сейчас» — «нет»,
326+
«В конце блока» — «да». Разбор после отказа доходит до конца вхолостую, и
327+
`«Разбор исходника»` отдаёт `ничто` вместо наполовину собранного AST: половина
328+
AST означала бы, что из неё можно что-то построить.
329+
330+
**Лимит шагов.** Разбор самого большого исходника репозитория (собственный,
331+
3300 строк, 28 000 токенов) вместе с лексером укладывается между 50 и 100
332+
миллионами шагов интерпретатора; тест поднимает лимит до ста пятидесяти
333+
миллионов, а глубину вызовов — до двадцати тысяч. Лимит здесь не формальность:
334+
он проверяет, что разбор остался линейным по числу токенов, а не свалился в
335+
квадрат — ради этого поток и разложен пачками. Глубина нужна потому, что спуск
336+
вкладывается на каждое `пусть` и на каждую ветку, а хвостовые вызовы (обходы
337+
объявлений, строк блока, аргументов) глубины не занимают.

0 commit comments

Comments
 (0)