You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Третий и самый крупный слой: 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 падений.
`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