@algosail/lang 0.2.12 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/bin/sail.mjs +4 -0
  2. package/cli/run-cli.js +176 -0
  3. package/index.js +11 -2
  4. package/lib/codegen/README.md +230 -0
  5. package/lib/codegen/codegen-diagnostics.js +164 -0
  6. package/lib/codegen/compile-graph.js +107 -0
  7. package/lib/codegen/emit-adt.js +177 -0
  8. package/lib/codegen/emit-body.js +1265 -0
  9. package/lib/codegen/emit-builtin.js +371 -0
  10. package/lib/codegen/emit-jsdoc-sail.js +383 -0
  11. package/lib/codegen/emit-module.js +498 -0
  12. package/lib/codegen/esm-imports.js +26 -0
  13. package/lib/codegen/index.js +69 -0
  14. package/lib/codegen/out-layout.js +102 -0
  15. package/lib/ffi/extract-jsdoc-sail.js +34 -0
  16. package/lib/io-node/index.js +4 -0
  17. package/lib/io-node/package-root.js +18 -0
  18. package/lib/io-node/read-file.js +12 -0
  19. package/lib/io-node/resolve-package.js +24 -0
  20. package/lib/io-node/resolve-sail-names-from-disk.js +21 -0
  21. package/lib/ir/assert-json-serializable.js +30 -0
  22. package/lib/ir/attach-call-effects.js +108 -0
  23. package/lib/ir/bind-values.js +594 -0
  24. package/lib/ir/build-module-ir.js +290 -0
  25. package/lib/ir/index.js +31 -0
  26. package/lib/ir/lower-body-steps.js +170 -0
  27. package/lib/ir/module-metadata.js +65 -0
  28. package/lib/ir/schema-version.js +15 -0
  29. package/lib/ir/serialize.js +202 -0
  30. package/lib/ir/stitch-types.js +92 -0
  31. package/lib/names/adt-autogen.js +22 -0
  32. package/lib/names/import-path.js +28 -0
  33. package/lib/names/index.js +1 -0
  34. package/lib/names/local-declarations.js +127 -0
  35. package/lib/names/lower-first.js +6 -0
  36. package/lib/names/module-scope.js +120 -0
  37. package/lib/names/resolve-sail.js +365 -0
  38. package/lib/names/walk-ast-refs.js +91 -0
  39. package/lib/parse/ast-build.js +51 -0
  40. package/lib/parse/ast-spec.js +212 -0
  41. package/lib/parse/builtins-set.js +12 -0
  42. package/lib/parse/diagnostics.js +180 -0
  43. package/lib/parse/index.js +46 -0
  44. package/lib/parse/lexer.js +390 -0
  45. package/lib/parse/parse-source.js +912 -0
  46. package/lib/typecheck/adt-autogen-sigs.js +345 -0
  47. package/lib/typecheck/build-type-env.js +148 -0
  48. package/lib/typecheck/builtin-signatures.js +183 -0
  49. package/lib/typecheck/check-word-body.js +1021 -0
  50. package/lib/typecheck/effect-decl.js +124 -0
  51. package/lib/typecheck/index.js +55 -0
  52. package/lib/typecheck/normalize-sig.js +369 -0
  53. package/lib/typecheck/stack-step-snapshots.js +56 -0
  54. package/lib/typecheck/unify-type.js +665 -0
  55. package/lib/typecheck/validate-adt.js +201 -0
  56. package/package.json +4 -9
  57. package/scripts/regen-demo-full-syntax-ast.mjs +22 -0
  58. package/test/cli/sail-cli.test.js +64 -0
  59. package/test/codegen/compile-bracket-ffi-e2e.test.js +64 -0
  60. package/test/codegen/compile-stage0.test.js +128 -0
  61. package/test/codegen/compile-stage4-layout.test.js +124 -0
  62. package/test/codegen/e2e-prelude-ffi-adt/app/extra.sail +6 -0
  63. package/test/codegen/e2e-prelude-ffi-adt/app/lib.sail +33 -0
  64. package/test/codegen/e2e-prelude-ffi-adt/app/main.sail +28 -0
  65. package/test/codegen/e2e-prelude-ffi-adt/artifacts/.gitignore +2 -0
  66. package/test/codegen/e2e-prelude-ffi-adt/ffi/helpers.js +27 -0
  67. package/test/codegen/e2e-prelude-ffi-adt.test.js +100 -0
  68. package/test/codegen/emit-adt-stage6.test.js +168 -0
  69. package/test/codegen/emit-async-stage5.test.js +164 -0
  70. package/test/codegen/emit-body-stage2.test.js +139 -0
  71. package/test/codegen/emit-body.test.js +163 -0
  72. package/test/codegen/emit-builtins-stage7.test.js +258 -0
  73. package/test/codegen/emit-diagnostics-stage9.test.js +90 -0
  74. package/test/codegen/emit-jsdoc-stage8.test.js +113 -0
  75. package/test/codegen/emit-module-stage3.test.js +78 -0
  76. package/test/conformance/conformance-ir-l4.test.js +38 -0
  77. package/test/conformance/conformance-l5-codegen.test.js +111 -0
  78. package/test/conformance/conformance-runner.js +91 -0
  79. package/test/conformance/conformance-suite-l3.test.js +32 -0
  80. package/test/ffi/prelude-jsdoc.test.js +49 -0
  81. package/test/fixtures/demo-full-syntax.ast.json +1471 -0
  82. package/test/fixtures/demo-full-syntax.sail +35 -0
  83. package/test/fixtures/io-node-ffi-adt/ffi.js +7 -0
  84. package/test/fixtures/io-node-ffi-adt/use.sail +4 -0
  85. package/test/fixtures/io-node-mini/dep.sail +2 -0
  86. package/test/fixtures/io-node-mini/entry.sail +4 -0
  87. package/test/fixtures/io-node-prelude/entry.sail +4 -0
  88. package/test/fixtures/io-node-reexport-chain/a.sail +4 -0
  89. package/test/fixtures/io-node-reexport-chain/b.sail +2 -0
  90. package/test/fixtures/io-node-reexport-chain/c.sail +2 -0
  91. package/test/io-node/resolve-disk.test.js +59 -0
  92. package/test/ir/bind-values.test.js +84 -0
  93. package/test/ir/build-module-ir.test.js +100 -0
  94. package/test/ir/call-effects.test.js +97 -0
  95. package/test/ir/ffi-bracket-ir.test.js +59 -0
  96. package/test/ir/full-ir-document.test.js +51 -0
  97. package/test/ir/ir-document-assert.js +67 -0
  98. package/test/ir/lower-body-steps.test.js +90 -0
  99. package/test/ir/module-metadata.test.js +42 -0
  100. package/test/ir/serialization-model.test.js +172 -0
  101. package/test/ir/stitch-types.test.js +74 -0
  102. package/test/names/l2-resolve-adt-autogen.test.js +155 -0
  103. package/test/names/l2-resolve-bracket-ffi.test.js +108 -0
  104. package/test/names/l2-resolve-declaration-and-bracket-errors.test.js +276 -0
  105. package/test/names/l2-resolve-graph.test.js +105 -0
  106. package/test/names/l2-resolve-single-file.test.js +79 -0
  107. package/test/parse/ast-spec.test.js +56 -0
  108. package/test/parse/ast.test.js +476 -0
  109. package/test/parse/contract.test.js +37 -0
  110. package/test/parse/fixtures-full-syntax.test.js +24 -0
  111. package/test/parse/helpers.js +27 -0
  112. package/test/parse/l0-lex-diagnostics-matrix.test.js +59 -0
  113. package/test/parse/l0-lex.test.js +40 -0
  114. package/test/parse/l1-diagnostics.test.js +77 -0
  115. package/test/parse/l1-import.test.js +28 -0
  116. package/test/parse/l1-parse-diagnostics-matrix.test.js +32 -0
  117. package/test/parse/l1-top-level.test.js +47 -0
  118. package/test/parse/l1-types.test.js +31 -0
  119. package/test/parse/l1-words.test.js +49 -0
  120. package/test/parse/l2-diagnostics-contract.test.js +67 -0
  121. package/test/parse/l3-diagnostics-contract.test.js +66 -0
  122. package/test/typecheck/adt-decl-stage2.test.js +83 -0
  123. package/test/typecheck/container-contract-e1309.test.js +258 -0
  124. package/test/typecheck/ffi-bracket-l3.test.js +61 -0
  125. package/test/typecheck/l3-diagnostics-matrix.test.js +248 -0
  126. package/test/typecheck/l3-partial-pipeline.test.js +74 -0
  127. package/test/typecheck/opaque-ffi-type.test.js +78 -0
  128. package/test/typecheck/sig-type-stage3.test.js +190 -0
  129. package/test/typecheck/stack-check-stage4.test.js +149 -0
  130. package/test/typecheck/stack-check-stage5.test.js +74 -0
  131. package/test/typecheck/stack-check-stage6.test.js +56 -0
  132. package/test/typecheck/stack-check-stage7.test.js +160 -0
  133. package/test/typecheck/stack-check-stage8.test.js +146 -0
  134. package/test/typecheck/stack-check-stage9.test.js +105 -0
  135. package/test/typecheck/typecheck-env.test.js +53 -0
  136. package/test/typecheck/typecheck-pipeline.test.js +37 -0
  137. package/README.md +0 -37
  138. package/cli/sail.js +0 -151
  139. package/cli/typecheck.js +0 -39
  140. package/docs/ARCHITECTURE.md +0 -50
  141. package/docs/CHANGELOG.md +0 -18
  142. package/docs/FFI-GUIDE.md +0 -65
  143. package/docs/RELEASE.md +0 -36
  144. package/docs/TESTING.md +0 -86
  145. package/test/integration.test.js +0 -61
package/bin/sail.mjs ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { main } from '../cli/run-cli.js'
3
+
4
+ await main()
package/cli/run-cli.js ADDED
@@ -0,0 +1,176 @@
1
+ /**
2
+ * CLI @algosail/lang: подкоманды typecheck и compile (RFC-compile §9.1).
3
+ */
4
+ import { parseArgs } from 'node:util'
5
+ import path from 'node:path'
6
+ import {
7
+ compileSailToOutDir,
8
+ createReadFileUtf8,
9
+ createResolvePackage,
10
+ findPackageRoot,
11
+ typecheckSail
12
+ } from '../index.js'
13
+
14
+ /**
15
+ * @param {object} d
16
+ * @returns {string}
17
+ */
18
+ export function formatDiagnosticLine (d) {
19
+ if (d == null || typeof d !== 'object') return String(d)
20
+ const code = d.code != null ? String(d.code) : ''
21
+ const msg = d.message != null ? String(d.message) : ''
22
+ const bits = [code, msg].filter(Boolean)
23
+ if (typeof d.path === 'string' && d.path.length > 0) bits.push(`path ${d.path}`)
24
+ if (d.line != null) bits.push(`line ${d.line}`)
25
+ if (d.column != null) bits.push(`col ${d.column}`)
26
+ return bits.join(' — ')
27
+ }
28
+
29
+ /**
30
+ * @param {string} entryPath
31
+ * @param {string | undefined} projectRootFlag
32
+ * @returns {(spec: string, fromPath: string) => string | null}
33
+ */
34
+ export function resolvePackageForCli (entryPath, projectRootFlag) {
35
+ const resolvedFlag =
36
+ projectRootFlag != null && projectRootFlag !== ''
37
+ ? path.resolve(projectRootFlag)
38
+ : null
39
+ const root = resolvedFlag ?? findPackageRoot(entryPath)
40
+ if (root != null) {
41
+ return createResolvePackage({ projectRoot: root })
42
+ }
43
+ return createResolvePackage()
44
+ }
45
+
46
+ function printUsage () {
47
+ process.stderr.write(`Usage:
48
+ sail-lang typecheck [--project-root <dir>] <entry.sail>
49
+ sail-lang compile [--out-dir|-o <dir>] [--source-root <dir>] [--project-root <dir>] <entry.sail>
50
+
51
+ Options:
52
+ --project-root Корень package.json для resolve npm (иначе ищется вверх от entry)
53
+ --source-root RFC-compile §9.1 (по умолчанию dirname(entry))
54
+ -h, --help Справка
55
+
56
+ Диагностики печатаются в stderr; код выхода 0 при успехе, 1 при ошибке.
57
+ `)
58
+ }
59
+
60
+ /**
61
+ * @param {string[]} argv
62
+ * @param {{ stdout?: NodeJS.WriteStream, stderr?: NodeJS.WriteStream }} [io]
63
+ * @returns {Promise<number>} exit code
64
+ */
65
+ export async function runCli (argv, io = {}) {
66
+ const out = io.stdout ?? process.stdout
67
+ const err = io.stderr ?? process.stderr
68
+
69
+ let tokens
70
+ try {
71
+ tokens = parseArgs({
72
+ args: argv,
73
+ allowPositionals: true,
74
+ strict: true,
75
+ options: {
76
+ help: { type: 'boolean', short: 'h' },
77
+ 'project-root': { type: 'string' },
78
+ 'out-dir': { type: 'string', short: 'o' },
79
+ 'source-root': { type: 'string' }
80
+ }
81
+ })
82
+ } catch (e) {
83
+ err.write(String(e?.message ?? e) + '\n')
84
+ printUsage()
85
+ return 1
86
+ }
87
+
88
+ const { values, positionals } = tokens
89
+
90
+ if (values.help === true) {
91
+ printUsage()
92
+ return 0
93
+ }
94
+ if (positionals.length === 0) {
95
+ printUsage()
96
+ return 1
97
+ }
98
+
99
+ const cmd = positionals[0]
100
+ const entryRaw = positionals[1]
101
+
102
+ if (entryRaw == null || entryRaw === '') {
103
+ err.write('Missing <entry.sail>\n')
104
+ printUsage()
105
+ return 1
106
+ }
107
+
108
+ const entryPath = path.resolve(process.cwd(), entryRaw)
109
+ const readFile = createReadFileUtf8()
110
+ const resolvePackage = resolvePackageForCli(
111
+ entryPath,
112
+ values['project-root']
113
+ )
114
+
115
+ if (cmd === 'typecheck') {
116
+ if (positionals.length > 2) {
117
+ err.write('Too many arguments for typecheck\n')
118
+ return 1
119
+ }
120
+ const r = typecheckSail({ entryPath, readFile, resolvePackage })
121
+ if (!r.ok) {
122
+ for (const d of r.diagnostics ?? []) {
123
+ err.write(formatDiagnosticLine(d) + '\n')
124
+ }
125
+ return 1
126
+ }
127
+ out.write('ok\n')
128
+ return 0
129
+ }
130
+
131
+ if (cmd === 'compile') {
132
+ if (positionals.length > 2) {
133
+ err.write('Too many arguments for compile\n')
134
+ return 1
135
+ }
136
+ const outDir = values['out-dir']
137
+ if (outDir == null || outDir === '') {
138
+ err.write('compile requires --out-dir (-o)\n')
139
+ printUsage()
140
+ return 1
141
+ }
142
+ const outDirAbs = path.resolve(process.cwd(), outDir)
143
+ const sourceRoot =
144
+ values['source-root'] != null && values['source-root'] !== ''
145
+ ? path.resolve(process.cwd(), values['source-root'])
146
+ : undefined
147
+
148
+ const r = await compileSailToOutDir({
149
+ entryPath,
150
+ outDir: outDirAbs,
151
+ sourceRoot,
152
+ readFile,
153
+ resolvePackage
154
+ })
155
+ if (!r.ok) {
156
+ for (const d of r.diagnostics ?? []) {
157
+ err.write(formatDiagnosticLine(d) + '\n')
158
+ }
159
+ return 1
160
+ }
161
+ for (const p of r.emitted ?? []) {
162
+ out.write(`${p}\n`)
163
+ }
164
+ return 0
165
+ }
166
+
167
+ err.write(`Unknown command: ${cmd}\n`)
168
+ printUsage()
169
+ return 1
170
+ }
171
+
172
+ /** Запуск из bin: динамический import index по file URL (cwd может быть любым). */
173
+ export async function main () {
174
+ const code = await runCli(process.argv.slice(2))
175
+ process.exitCode = code
176
+ }
package/index.js CHANGED
@@ -1,2 +1,11 @@
1
- export { typecheck } from '@algosail/typecheck'
2
- export { createParser } from '@algosail/parser'
1
+ export { parseSource } from './lib/parse/index.js'
2
+ export { isBuiltinWord, BUILTIN_WORDS } from './lib/parse/builtins-set.js'
3
+ export { resolveSailNames } from './lib/names/index.js'
4
+ export { typecheckSail } from './lib/typecheck/index.js'
5
+ export { compileSailToOutDir } from './lib/codegen/index.js'
6
+ export {
7
+ createReadFileUtf8,
8
+ createResolvePackage,
9
+ findPackageRoot,
10
+ resolveSailNamesFromDisk
11
+ } from './lib/io-node/index.js'
@@ -0,0 +1,230 @@
1
+ # L5 Codegen (`lang/lib/codegen`)
2
+
3
+ Цель каталога: **полная** реализация генерации JavaScript (уровень **L5** по [RFC-conformance-0.1.md](../../../RFC-conformance-0.1.md)) в профиле lowering v0.1 [RFC-compile-0.1.md](../../../RFC-compile-0.1.md).
4
+
5
+ Нормативные документы (при конфликте с кодом побеждает RFC, см. правила репозитория):
6
+
7
+ | Документ | Роль для codegen |
8
+ |----------|------------------|
9
+ | [RFC-compile-0.1.md](../../../RFC-compile-0.1.md) | Пайплайн §4, профиль §1.1, стек §6, слова/quotation/builtins §7, ADT/литералы §8, ESM §9, `+Async` §10, `@sail` §11, диагностики §12, цель ES2025 §13 |
10
+ | [RFC-IR-0.1.md](../../../RFC-IR-0.1.md) | Входная модель IR; для пути codegen — полнота §§2–7 при `ok`, обязательно **привязки значений §4** там, где нужен JS (согласовано с RFC-compile §5–§6.2) |
11
+ | [RFC-0.1.md](../../../RFC-0.1.md) | Семантика, FFI §10.1, модули §4.2 |
12
+ | [RFC-builtins-0.1.md](../../../RFC-builtins-0.1.md) | Семантика `builtin_word` |
13
+ | [RFC-typecheck-0.1.md](../../../RFC-typecheck-0.1.md) | Уже отработанные инварианты до IR; codegen их не пересчитывает |
14
+ | [RFC-conformance-0.1.md](../../../RFC-conformance-0.1.md) | Эталонные кейсы L5 в `conformance/` и поля манифеста §5 |
15
+
16
+ **Граница до L5:** codegen **не** запускается, пока агрегатный итог L3 (и статус IR для рабочего набора модулей) не **`ok`** (RFC-compile §4, RFC-IR §1.1).
17
+
18
+ **Вне нормы v0.1** (не блокируют заявление L5 по RFC-compile): bundler, минификация, обязательные source maps, TypeScript — см. RFC-compile §2.
19
+
20
+ ---
21
+
22
+ ## Входные данные реализации
23
+
24
+ - Успешный проход **Parse → Names → Typecheck** для замыкания модулей от `entry`.
25
+ - Построение IR: `buildModuleIr` / `buildSerializedIrDocumentFromEnv` из [lang/lib/ir/](../ir/) с **`valueBindings: true`** на пути «компиляция → JS».
26
+ - Параметры компиляции (нормативная модель): **`entry`**, **`out-dir`**, опционально **`source-root`** (RFC-compile §9.2).
27
+
28
+ ---
29
+
30
+ ## Предлагаемая раскладка модулей (после начала кодирования)
31
+
32
+ Имена ориентировочные; группировать по сущности, без «один экспорт — один файл» без нужды (см. `AGENTS.md`).
33
+
34
+ | Файл / область | Назначение |
35
+ |----------------|------------|
36
+ | `index.js` | Публичный API (например `compileSailToOutDir`, типы опций) |
37
+ | `compile-graph.js` | Обход графа модулей от `entry`, порядок эмиссии, вызов IR + emit на модуль |
38
+ | `emit-module.js` | Один `.sail` → один ESM: импорты, тела слов, экспорты |
39
+ | `emit-body.js` | Обход `irSteps` + `valueBindings`: виртуальный стек → выражения / последовательность операторов; ветвление quotation: инлайн при `call`, `const qN = () => …` в точке передачи как значение (RFC-compile §6.3) |
40
+ | `emit-builtin.js` | Отображение узлов builtin в JS (инлайн или вызовы рантайма — по стратегии) |
41
+ | `emit-adt.js` | Тела автоген-слов ADT (RFC-0.1 §7): sum/product, eliminator по `arguments` |
42
+ | `emit-async.js` | `async` / `await` по `asyncDefinition`, `calleeAsync` (RFC-compile §10) |
43
+ | `esm-imports.js` | Построение статических `import` и спецификаторов §9.2 |
44
+ | `out-layout.js` | Пути `O(S)`, копирование FFI `.js`, зеркало дерева (§9.1) |
45
+ | `emit-jsdoc-sail.js` | JSDoc с `@sail` и печать Sail-фрагментов (ADT из AST, сигнатуры слова из AST или wire IR); контракт как у FFI ([extract-jsdoc-sail.js](../ffi/extract-jsdoc-sail.js)) |
46
+ | `codegen-diagnostics.js` | Реестр кодов **E51xx**–**E53xx** и фабрики сообщений (поле `path` для привязки к файлу; RFC-compile §12) |
47
+
48
+ Тесты: `lang/test/codegen/**/*.test.js` (brittle), плюс расширение conformance-раннера под поля `requireJs` / ожидаемый JS по [RFC-conformance-0.1.md](../../../RFC-conformance-0.1.md) §5.
49
+
50
+ ---
51
+
52
+ ## Этапы реализации (полная программа)
53
+
54
+ Каждый этап завершается **критерием приёмки**: brittle-тесты и/или кейсы conformance, **без** ослабления профиля RFC-compile §1.1.
55
+
56
+ ### Этап 0 — Каркас драйвера и контракт вызова
57
+
58
+ **Цель:** единая точка входа «после L3»: проверка `ok`, отказ с диагностиками без записи JS; при `ok` — передача управления графу модулей.
59
+
60
+ **RFC:** RFC-compile §4, RFC-IR §1.1.
61
+
62
+ **Результат:** функция верхнего уровня (например в `index.js` или `compile-graph.js`), принимающая `entry`, `out-dir`, `source-root`, колбэки чтения файлов; явное ветвление «не ok → только diagnostics».
63
+
64
+ **Критерий приёмки:** тест: при намеренной ошибке L3 — ни одного `.js` в `out-dir`; при успешном минимальном модуле — допускается заглушка «пустой или минимальный ESM» до этапа 3.
65
+
66
+ ---
67
+
68
+ ### Этап 1 — Ядро тела слова: `irSteps` + `valueBindings`
69
+
70
+ **Цель:** из IR узлов §5 и привязок §4 строить эквивалент потока значений в JS **без** массива как модели стека (RFC-compile §1.1 п.4, §6.2).
71
+
72
+ **RFC:** RFC-IR §§3–5, RFC-compile §6–§7 (фрагменты без модуля).
73
+
74
+ **Результат:** для одного слова (или внутреннего фрагмента) генерация последовательности присваиваний / выражений, корректная перестановка идентичностей для shuffle-слов через `argIds` / `resultIds`.
75
+
76
+ **Подэтапы (логические):**
77
+
78
+ 1. Литералы и простые builtin с фиксированной семантикой.
79
+ 2. Вызов локального `@word` (тот же модуль) как функции с аргументами в порядке RFC-0.1 §10.1 (вершина — последний параметр).
80
+ 3. Остальные виды узлов IR по мере необходимости (включая `Slot` и т.д. по RFC-IR §5).
81
+
82
+ **Критерий приёмки:** тесты на цепочки вроде `dup` / `swap` / `drop` + вызов; снимок или assert на отсутствие «единого стек-массива» в сгенерированном тексте (эвристика или структурная проверка AST выхода — на усмотрение).
83
+
84
+ ---
85
+
86
+ ### Этап 2 — Quotation: `call` и значения первого класса
87
+
88
+ **Цель:** согласовать эмиссию quotation с профилем v0.1 (RFC-compile §1.1 п.3, **§6.3**): отдельная JS-функция для тела quotation **не** появляется «сама по себе» при проходе по IR.
89
+
90
+ **Правила:**
91
+
92
+ 1. До **`call`** и до шага, где quotation нужна как **значение** для слова/FFI (в т.ч. со стека), **не** эмитить лишние обёртки-функции.
93
+ 2. На **`call`** — тело соответствующей quotation **инлайнится** в текущую генерируемую функцию слова (тот же контур, что и остальные шаги тела), без обязательного вызова отдельной функции только ради quotation.
94
+ 3. Когда quotation **передаётся** как аргумент (функция высшего порядка) — в точке использования эмитится **стрелочная функция**; **рекомендуется** именованная привязка `const qN = () => { … }`, чтобы в будущем (вне скоупа текущей реализации) можно было добавить метапрограммирование / методы на представлении quotation без смены общей схемы codegen.
95
+
96
+ **RFC:** RFC-IR узлы `quote` / `named_quote`; RFC-compile §6.3, §7 п.2.
97
+
98
+ **Критерий приёмки:** сценарии только с `call` — в сгенерированном тексте нет обязательной отдельной функции на каждую литеральную quotation; сценарий передачи quotation в слово высшей порядки — есть локальная функция (стрелочная) в точке передачи; исполнение под Node даёт семантику, эквивалентную ядру.
99
+
100
+ ---
101
+
102
+ ### Этап 3 — Оболочка одного ESM-файла
103
+
104
+ **Цель:** один модуль `.sail` → один `.js` с статическими `import` и `export` (RFC-compile §9 п.1–3, §1.1 п.2).
105
+
106
+ **RFC:** RFC-IR §7 (импорты с `resolvedPath` где есть), RFC-0.1 §4.2.
107
+
108
+ **Результат:**
109
+
110
+ - Верх файла: только статические `import` (без `import()` для sail-зависимостей).
111
+ - Экспорт всех символов, которые модуль экспортирует по правилам Sail; внутренние хелперы без экспорта.
112
+
113
+ **Критерий приёмки:** двухмодульный проект (entry + один импорт `.sail`), Node ESM разрешает модули; символы доступны снаружи согласно исходному экспорту.
114
+
115
+ ---
116
+
117
+ ### Этап 4 — Граф модулей и раскладка `out-dir`
118
+
119
+ **Цель:** транзитивное замыкание от `entry`, зеркало путей, копирование проектных FFI `.js` (RFC-compile §9.1–§9.2).
120
+
121
+ **RFC:** ацикличность sail→sail уже проверена на L2 (`E1112`).
122
+
123
+ **Результат:**
124
+
125
+ - Вычисление `O(S)` для каждого `.sail` и FFI `.js` в замыкании.
126
+ - Относительные спецификаторы между артефактами под `out-dir` с `./` / `../` и суффиксом `.js`.
127
+ - npm / bare спецификаторы в `import` как в исходном `path`, без копирования пакетов в `out-dir`.
128
+
129
+ **Критерий приёмки:** интеграционный тест с вложенными каталогами + FFI-файлом; импорты разрешаются из `out-dir`.
130
+
131
+ ---
132
+
133
+ ### Этап 5 — `+Async` и `await`
134
+
135
+ **Цель:** слова с `+Async` → `async function`; вызовы с `calleeAsync` → `await` (RFC-compile §10, маркировка IR RFC-IR §6).
136
+
137
+ **Результат:** согласованность с типизацией (`E1310` уже до codegen); корректный возврат `Promise` / кортежа для нескольких выходов (§10 п.3–4).
138
+
139
+ **Критерий приёмки:** тест на async-цепочку; отсутствие «висящих» `Promise` на стеке после `await`.
140
+
141
+ ---
142
+
143
+ ### Этап 6 — ADT, литералы, eliminator-ы
144
+
145
+ **Цель:** конструкторы, распознаватели и автоген в соответствии с RFC-0.1 §7–§8 и RFC-compile §7–§8.
146
+
147
+ **Результат:** согласованные runtime-формы; eliminator-ы не обязаны быть отдельными builtin-вызовами, если семантика совпадает (RFC-compile §7 п.4).
148
+
149
+ **Реализация:** [emit-adt.js](emit-adt.js) (тела автоген-`function` по ADT); [emit-module.js](emit-module.js) — только используемые автогены без `export`, `~M/w` → `__adt_M__w`; [emit-body.js](emit-body.js) — `ListLiteral`, `Slot`, вызов sum-eliminator с префиксом стека; [bind-values.js](../ir/bind-values.js) — `ListLiteral`, исправление `slice(-0)` при `nIn===0`; list literal в [check-word-body.js](../typecheck/check-word-body.js).
150
+
151
+ **Критерий приёмки:** [lang/test/codegen/emit-adt-stage6.test.js](../../test/codegen/emit-adt-stage6.test.js); `npm test` в `lang/`.
152
+
153
+ ---
154
+
155
+ ### Этап 7 — Полное покрытие builtins
156
+
157
+ **Цель:** каждый `builtin_word` из RFC-builtins либо инлайнится, либо маппится на корректный JS с той же семантикой (RFC-compile §7 п.3).
158
+
159
+ **Результат:** перестановки §4.1–4.2 — таблицы shuffle в [emit-body.js](emit-body.js) и [bind-values.js](../ir/bind-values.js); `call` — инлайн/invoke в [emit-body.js](emit-body.js); комбинаторы `dip`, `keep`, `bi`, `tri`, `spread`, `both` — [emit-builtin.js](emit-builtin.js) и согласованные `valueBindings` + `combinatorInlineMeta` в [bind-values.js](../ir/bind-values.js) (ветка `kind: 'inline'` из `quoteRegistry`, иначе `kind: 'invoke'` по типу `Quote` в `preTypes`, когда значение quotation пришло из вызова слова). Рекурсивный обход `+Async` / вариант B — [emit-body.js](emit-body.js) (`irStepsContainsCalleeAsync`, `irStepsCallsPropagatingAsyncCallee`) для вложенных шагов только у частей `kind: 'inline'`.
160
+
161
+ **Критерий приёмки:** [lang/test/codegen/emit-builtins-stage7.test.js](../../test/codegen/emit-builtins-stage7.test.js); полный `npm test` в `lang/`; при новом `builtin_word` в RFC — `builtins-set.js`, `builtin-signatures.js`, при необходимости `bind-values.js`, shuffle-таблица или [emit-builtin.js](emit-builtin.js).
162
+
163
+ ---
164
+
165
+ ### Этап 8 — `@sail` в сгенерированном JS
166
+
167
+ **Цель:** извлекаемый контракт для потребителей FFI (RFC-compile §11, RFC-0.1 §10.1).
168
+
169
+ **Результат:** [emit-jsdoc-sail.js](emit-jsdoc-sail.js) строит JSDoc (префикс до `@sail` из `doc` AST, затем только Sail); [emit-module.js](emit-module.js) вставляет верхний блок с объявлениями экспортируемых `&` (порядок `items` снимка) сразу после `import` и перед ADT-autogen, и отдельный блок перед каждым `export function`. Внутренние `function` autogen без `@sail`. Сбой формирования блока: `E5206`. `+Async` в контракте слова согласован с `export async` (включая всплытие от async-callee по тому же правилу, что эмиттер тела).
170
+
171
+ **Критерий приёмки:** [lang/test/codegen/emit-jsdoc-stage8.test.js](../../test/codegen/emit-jsdoc-stage8.test.js) (`extractSailFragmentsFromJs`, разбор фрагментов через `parseSource`); полный `npm test` в `lang/`.
172
+
173
+ ---
174
+
175
+ ### Этап 9 — Диагностики codegen и полировка
176
+
177
+ **Цель:** ошибки конфигурации (`source-root`, выход за пределы дерева), невозможность эмиссии или копии FFI — с **стабильными кодами** и привязкой к путям где уместно (RFC-compile §12; span из парсера для codegen в v0.1 не обязателен).
178
+
179
+ **Результат:** [codegen-diagnostics.js](codegen-diagnostics.js) — фабрики для **E5101**–**E5105** (раскладка путей и `entryPath` ⊂ `sourceRoot` в [out-layout.js](out-layout.js)), **E5201**–**E5206** ([emit-module.js](emit-module.js)), **E5301**–**E5302** ([compile-graph.js](compile-graph.js)). `E5301` только если в замыкании есть проектный FFI `.js` под `source-root` и не передан `readFile`. Эмиссия по графу — **best-effort**: при ошибке после части записей часть файлов в `out-dir` уже может существовать; до codegen по-прежнему «не ok L3 → без записи».
180
+
181
+ **Критерий приёмки:** [lang/test/codegen/emit-diagnostics-stage9.test.js](../../test/codegen/emit-diagnostics-stage9.test.js); полный `npm test` в `lang/`.
182
+
183
+ ---
184
+
185
+ ### Этап 10 — Conformance L5 и регрессия
186
+
187
+ **Цель:** подключить эталонный набор к codegen и зафиксировать `conformanceSuiteVersion` по [RFC-conformance-0.1.md](../../../RFC-conformance-0.1.md) §7.
188
+
189
+ **Результат:** [lang/test/conformance/conformance-l5-codegen.test.js](../../test/conformance/conformance-l5-codegen.test.js) — загрузка [conformance/suite.json](../../../conformance/suite.json), фильтр `expect === 'accept'` и **`requireJs === true`** (см. также `filterCasesForL5CodegenRunner` в [conformance-runner.js](../../test/conformance/conformance-runner.js)). Для каждого кейса: **`sourceRoot` = корень каталога `conformance/`** на диске, **`readFile`** через `buildConformanceReadFile`, **`outDir`** — временная директория (`os.tmpdir` + `mkdtemp`), в **`finally`** — `rmSync(..., { recursive: true })` (политика этапа 9: при падении после части шагов tmp всё равно убирать). Вызов `compileSailToOutDir({ entryPath, outDir, sourceRoot, readFile })`; проверки: `ok`, непустой `emitted`, файлы на диске и непустые, подстрока `export function`, отсутствие динамического `import()`; для `accept-l5-cross-module-codegen` — статический `import`/`from` в сгенерированном entry. Перед codegen повторный проход L3 тем же `readFile`; при **`requireIr`** — `buildSerializedIrDocumentFromEnv` и [assertSerializedIrDocumentOk](../../test/ir/ir-document-assert.js), поле **`expectIrSchema`** сверяется с корнем документа.
190
+
191
+ **Как обновлять ожидания при изменении набора:** поднять **`conformanceSuiteVersion`** в `conformance/suite.json` (RFC-conformance §7) и ту же строку в **`EXPECTED_CONFORMANCE_SUITE_VERSION`** в `conformance-runner.js`; при новых полях кейса — расширить фильтр/ассерты в этом тесте осознанно. Золотой текст JS в манифесте в объёме этапа 10 не используется.
192
+
193
+ **Критерий приёмки:** `npm test` в `lang/` зелёный; все accept-кейсы с `requireJs: true` проходят раннер; версия в JSON и константе раннера совпадают.
194
+
195
+ ---
196
+
197
+ ## Зависимости между этапами (сводка)
198
+
199
+ ```mermaid
200
+ flowchart LR
201
+ E0[0 Драйвер] --> E1[1 Тело irSteps]
202
+ E1 --> E2[2 Quotation call/HO]
203
+ E2 --> E3[3 Один ESM]
204
+ E3 --> E4[4 Граф out-dir]
205
+ E4 --> E5[5 Async]
206
+ E1 --> E6[6 ADT]
207
+ E6 --> E7[7 Builtins полные]
208
+ E3 --> E7
209
+ E5 --> E8[8 JSDoc sail]
210
+ E4 --> E8
211
+ E7 --> E9[9 Диагностики]
212
+ E8 --> E9
213
+ E9 --> E10[10 Conformance L5]
214
+ ```
215
+
216
+ Этапы 5 и 6 можно развивать параллельно после стабилизации этапа 3; этап 7 логично вести итеративно с этапом 1, но **закрывать** только после прохождения сценариев этапа 6.
217
+
218
+ ---
219
+
220
+ ## Чеклист готовности L5 (для заявления соответствия)
221
+
222
+ - [ ] Профиль RFC-compile §1.1 соблюдён на всём эталонном наборе L5 (в т.ч. quotation: §6.3 — инлайн при `call`, функция в точке передачи при HO). *(Текущие кейсы L5 в suite с `requireJs`: одномодульная композиция и кросс-модульный импорт — раннер этапа 10 не доказывает весь §1.1.)*
223
+ - [x] Статические `import` для sail-графа на кросс-модульном conformance-кейсе; в эмитах раннера L5 нет динамического `import()`; спецификаторы с эталоном §9.2 не сравниваются.
224
+ - [ ] FFI-граница §6.1 / RFC-0.1 §10.1 на экспортируемых словах *(нет эталонного FFI-кейса L5 в suite).*
225
+ - [ ] `+Async` / `await` по §10 там, где IR маркирует async.
226
+ - [x] `@sail` на сгенерированных экспортах §11 *(покрыто тестами этапа 8; раннер этапа 10 отдельно не проверяет).*
227
+ - [ ] Выход синтаксически валиден как ESM **ES2025** (RFC-compile §13).
228
+ - [ ] Драйвер не эмитит JS при не-`ok` L3/IR для целевого набора модулей; при не-`ok` codegen после успешного L3 возвращаются диагностики **E51xx**–**E53xx** без гарантии чистого `out-dir` *(поведение драйвера и коды ошибок покрыты `compile-stage0.test.js` и `emit-diagnostics-stage9.test.js`; эталонный L5 conformance негативы codegen не гоняет — для строки чеклиста как «полного» L5-пакета пункт остаётся открытым).*
229
+
230
+ После выполнения — обновить при необходимости описание уровня L5 в документации пакета `lang` (вне этого файла — по отдельному запросу).
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Диагностики чистого codegen (RFC-compile-0.1 §12): коды E51xx–E53xx, сообщения, поле `path` где уместно.
3
+ * Коды E1xxx–E14xx в RFC-0.1 §13 относятся к parse/names/typecheck/runtime; блок E5xxx — контракт репозитория для L5.
4
+ */
5
+ import path from 'node:path'
6
+
7
+ /**
8
+ * @param {{ code: string, message: string, path?: string }} d
9
+ * @returns {{ code: string, message: string, path?: string }}
10
+ */
11
+ export function codegenDiagnostic (d) {
12
+ const out = { code: d.code, message: d.message }
13
+ if (d.path != null) out.path = path.normalize(path.resolve(d.path))
14
+ return out
15
+ }
16
+
17
+ /** @param {string} sailPath */
18
+ export function e5101SailOutsideSourceRoot (sailPath) {
19
+ return codegenDiagnostic({
20
+ code: 'E5101',
21
+ message:
22
+ 'Sail module path is outside source-root (RFC-compile §9.2)',
23
+ path: sailPath
24
+ })
25
+ }
26
+
27
+ /** @param {string} sailPath */
28
+ export function e5101SailInvalidRelative (sailPath) {
29
+ return codegenDiagnostic({
30
+ code: 'E5101',
31
+ message: 'Invalid relative path from source-root to module',
32
+ path: sailPath
33
+ })
34
+ }
35
+
36
+ /** @param {string} sailPath */
37
+ export function e5102SailExpectedExtension (sailPath) {
38
+ return codegenDiagnostic({
39
+ code: 'E5102',
40
+ message: 'Expected .sail module path for codegen output mapping',
41
+ path: sailPath
42
+ })
43
+ }
44
+
45
+ /** @param {string} jsPath */
46
+ export function e5103FfiOutsideSourceRoot (jsPath) {
47
+ return codegenDiagnostic({
48
+ code: 'E5103',
49
+ message: 'FFI .js path is outside source-root (RFC-compile §9.2)',
50
+ path: jsPath
51
+ })
52
+ }
53
+
54
+ /** @param {string} jsPath */
55
+ export function e5103FfiInvalidRelative (jsPath) {
56
+ return codegenDiagnostic({
57
+ code: 'E5103',
58
+ message: 'Invalid relative path from source-root to FFI .js',
59
+ path: jsPath
60
+ })
61
+ }
62
+
63
+ /** @param {string} jsPath */
64
+ export function e5104FfiExpectedExtension (jsPath) {
65
+ return codegenDiagnostic({
66
+ code: 'E5104',
67
+ message: 'Expected .js path for project FFI output mapping',
68
+ path: jsPath
69
+ })
70
+ }
71
+
72
+ /**
73
+ * Точка входа компиляции не лежит в дереве source-root (явный sourceRoot).
74
+ *
75
+ * @param {{ entryPath: string, sourceRoot: string }} opts
76
+ */
77
+ export function e5105EntryOutsideSourceRoot ({ entryPath, sourceRoot }) {
78
+ return codegenDiagnostic({
79
+ code: 'E5105',
80
+ message: `entryPath is outside source-root (RFC-compile §9.2): ${JSON.stringify(path.normalize(path.resolve(entryPath)))} not under ${JSON.stringify(path.normalize(path.resolve(sourceRoot)))}`,
81
+ path: entryPath
82
+ })
83
+ }
84
+
85
+ /** @param {string} alias */
86
+ export function e5201BadImportAlias (alias) {
87
+ return codegenDiagnostic({
88
+ code: 'E5201',
89
+ message: `Недопустимый идентификатор алиаса модуля для import: ${JSON.stringify(alias)}`
90
+ })
91
+ }
92
+
93
+ /** @param {string} alias */
94
+ export function e5202ImportEmptySpecifier (alias) {
95
+ return codegenDiagnostic({
96
+ code: 'E5202',
97
+ message: `Импорт ${alias}: нет resolvedPath к .sail и пустой path (npm ожидает непустой спецификатор)`
98
+ })
99
+ }
100
+
101
+ /** @param {string} w */
102
+ export function e5203BadBracketWord (w) {
103
+ return codegenDiagnostic({
104
+ code: 'E5203',
105
+ message: `Недопустимое имя слова в скобках импорта: ${JSON.stringify(w)}`
106
+ })
107
+ }
108
+
109
+ /** @param {string} wname */
110
+ export function e5204BadJsWordName (wname) {
111
+ return codegenDiagnostic({
112
+ code: 'E5204',
113
+ message: `Недопустимое имя слова для JS: ${JSON.stringify(wname)}`
114
+ })
115
+ }
116
+
117
+ /** @param {string} detail */
118
+ export function e5205EmitAdtAutogen (detail) {
119
+ return codegenDiagnostic({
120
+ code: 'E5205',
121
+ message: `emit ADT autogen: ${detail}`
122
+ })
123
+ }
124
+
125
+ /** @param {string} wname */
126
+ export function e5205EmitWord (wname, detail) {
127
+ return codegenDiagnostic({
128
+ code: 'E5205',
129
+ message: `emit ${wname}: ${detail}`
130
+ })
131
+ }
132
+
133
+ /** @param {string} detail */
134
+ export function e5206JSDocModuleHeader (detail) {
135
+ return codegenDiagnostic({
136
+ code: 'E5206',
137
+ message: `JSDoc @sail (заголовок модуля): ${detail}`
138
+ })
139
+ }
140
+
141
+ /** @param {string} wname */
142
+ export function e5206JSDocWord (wname, detail) {
143
+ return codegenDiagnostic({
144
+ code: 'E5206',
145
+ message: `JSDoc @sail (${wname}): ${detail}`
146
+ })
147
+ }
148
+
149
+ export function e5301ReadFileRequired () {
150
+ return codegenDiagnostic({
151
+ code: 'E5301',
152
+ message:
153
+ 'readFile is required for codegen emit when project FFI .js modules are in the closure (RFC-compile §9.1)'
154
+ })
155
+ }
156
+
157
+ /** @param {string} jsPath */
158
+ export function e5302CannotReadFfi (jsPath) {
159
+ return codegenDiagnostic({
160
+ code: 'E5302',
161
+ message: `Cannot read FFI file for copy: ${jsPath}`,
162
+ path: jsPath
163
+ })
164
+ }