@algosail/lang 0.2.12 → 0.5.1

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 +12 -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 +34 -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
@@ -0,0 +1,1265 @@
1
+ /**
2
+ * L5 этап 1–2: эмиссия фрагмента тела слова по `irSteps` + `valueBindings`
3
+ * (RFC-compile-0.1 §6.2–6.3, RFC-IR-0.1 §4, RFC-0.1 §10.1).
4
+ *
5
+ * Без массива как модели стека: только `const` и выражения.
6
+ * Quotation: отложенная эмиссия; `call` — инлайн тела; значение Quote — `const qN = (…) => { … }`.
7
+ */
8
+
9
+ import {
10
+ COMBINATOR_BUILTIN_NAMES,
11
+ tryEmitCombinatorBuiltin
12
+ } from './emit-builtin.js'
13
+
14
+ /**
15
+ * @typedef {import('../typecheck/normalize-sig.js').NormalizedSignature} NormalizedSignature
16
+ */
17
+
18
+ /**
19
+ * @typedef {{
20
+ * localPropagatesAsync: (name: string) => boolean
21
+ * qualifiedPropagatesAsync: (moduleAlias: string, wordName: string) => boolean
22
+ * }} CalleeAsyncResolver
23
+ */
24
+
25
+ /**
26
+ * @typedef {{
27
+ * innerSteps: Record<string, unknown>[]
28
+ * innerEntryStackIds: string[]
29
+ * }} QuoteEmitMeta
30
+ */
31
+
32
+ /**
33
+ * Выражение shuffle — уже имя `const`/параметра из idToJs (без побочных эффектов);
34
+ * тогда новый `const` не нужен: это перестановка/dup на уровне имён (RFC-0.1, lowering §6.2).
35
+ *
36
+ * @param {string} expr
37
+ * @returns {boolean}
38
+ */
39
+ function isShuffleReuseExpr (expr) {
40
+ return /^[_$a-zA-Z][_$a-zA-Z0-9]*$/.test(expr)
41
+ }
42
+
43
+ /** @type {Record<string, (argExprs: string[]) => string[]>} */
44
+ const BUILTIN_SHUFFLE_EXPR = {
45
+ dup: (a) => [a[0], a[0]],
46
+ drop: () => [],
47
+ swap: (a) => [a[1], a[0]],
48
+ over: (a) => [a[0], a[1], a[0]],
49
+ dup2: (a) => [a[0], a[1], a[0], a[1]],
50
+ drop2: () => [],
51
+ rot: (a) => [a[1], a[2], a[0]],
52
+ reverse: (a) => [a[2], a[0], a[1]],
53
+ nip: (a) => [a[1]],
54
+ tuck: (a) => [a[1], a[0], a[1]],
55
+ swap2: (a) => [...a.slice(2, 4), ...a.slice(0, 2)]
56
+ }
57
+
58
+ /**
59
+ * @param {number} nIn
60
+ * @param {number} nOut
61
+ * @returns {NormalizedSignature}
62
+ */
63
+ function sigWireFromStackLens (nIn, nOut) {
64
+ const slot = { kind: 'prim', name: '_' }
65
+ return {
66
+ left: Array.from({ length: nIn }, () => slot),
67
+ right: Array.from({ length: nOut }, () => slot),
68
+ effectsAdd: [],
69
+ effectsRemove: []
70
+ }
71
+ }
72
+
73
+ /**
74
+ * @param {Record<string, unknown>[]} innerSteps
75
+ * @returns {number}
76
+ */
77
+ function inferInnerOutLen (innerSteps) {
78
+ if (!Array.isArray(innerSteps)) return 0
79
+ for (let i = innerSteps.length - 1; i >= 0; i--) {
80
+ const s = innerSteps[i]
81
+ if (s != null && typeof s === 'object' && Array.isArray(s.postTypes)) {
82
+ return s.postTypes.length
83
+ }
84
+ }
85
+ return 0
86
+ }
87
+
88
+ /**
89
+ * @param {string[]} lines
90
+ * @param {string[]} currentStackIds
91
+ * @param {Map<string, string>} idToJs
92
+ * @param {NormalizedSignature} sig
93
+ * @param {string} wordLabel
94
+ */
95
+ function appendReturnForLines (lines, currentStackIds, idToJs, sig, wordLabel) {
96
+ const right = Array.isArray(sig.right) ? sig.right : []
97
+ if (right.length !== currentStackIds.length) {
98
+ throw new Error(
99
+ `emit-body[${wordLabel}]: длина финального стека (${currentStackIds.length}) ` +
100
+ `не совпадает с sig.right (${right.length})`
101
+ )
102
+ }
103
+ if (right.length === 0) {
104
+ lines.push('return;')
105
+ } else if (right.length === 1) {
106
+ const id = currentStackIds[0]
107
+ const j = idToJs.get(id)
108
+ if (j == null) {
109
+ throw new Error(`emit-body[${wordLabel}]: нет привязки для финального id ${id}`)
110
+ }
111
+ lines.push(`return ${j};`)
112
+ } else {
113
+ const parts = currentStackIds.map((id) => {
114
+ const j = idToJs.get(id)
115
+ if (j == null) {
116
+ throw new Error(`emit-body[${wordLabel}]: нет привязки для финального id ${id}`)
117
+ }
118
+ return j
119
+ })
120
+ lines.push(`return [${parts.join(', ')}];`)
121
+ }
122
+ }
123
+
124
+ /**
125
+ * @param {Record<string, unknown>} node
126
+ * @returns {string}
127
+ */
128
+ function literalToJsExpr (node) {
129
+ const litKind = /** @type {string | undefined} */ (node.litKind)
130
+ const raw = node.raw != null ? String(node.raw) : ''
131
+ switch (litKind) {
132
+ case 'number': {
133
+ if (raw === '' || Number.isNaN(Number(raw))) {
134
+ throw new Error(`emit-body: некорректный number literal ${JSON.stringify(raw)}`)
135
+ }
136
+ return raw
137
+ }
138
+ case 'bigint':
139
+ return `BigInt(${JSON.stringify(raw)})`
140
+ case 'bool':
141
+ return raw === 'true' ? 'true' : 'false'
142
+ case 'nil':
143
+ return 'null'
144
+ case 'string':
145
+ return JSON.stringify(raw)
146
+ case 'regexp':
147
+ throw new Error('emit-body: regexp literal пока не поддержан')
148
+ default:
149
+ throw new Error(`emit-body: неизвестный litKind ${String(litKind)}`)
150
+ }
151
+ }
152
+
153
+ /**
154
+ * @param {string} name
155
+ */
156
+ function assertValidJsCallee (name) {
157
+ if (!/^[A-Za-z_$][\w$]*$/.test(name)) {
158
+ throw new Error(
159
+ `emit-body: имя слова не является допустимым идентификатором JS: ${JSON.stringify(name)}`
160
+ )
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Имя JS для входного слота слова: `named_quote.prefix` или очередное `pK`.
166
+ *
167
+ * @param {unknown} slot
168
+ * @param {() => string} allocP
169
+ * @returns {string}
170
+ */
171
+ function jsParamNameForEntrySlot (slot, allocP) {
172
+ if (
173
+ slot != null &&
174
+ typeof slot === 'object' &&
175
+ /** @type {{ kind?: string, prefix?: string }} */ (slot).kind === 'named_quote' &&
176
+ typeof /** @type {{ prefix?: string }} */ (slot).prefix === 'string'
177
+ ) {
178
+ const px = /** @type {{ prefix: string }} */ (slot).prefix
179
+ try {
180
+ assertValidJsCallee(px)
181
+ return px
182
+ } catch {
183
+ /* fall through */
184
+ }
185
+ }
186
+ return allocP()
187
+ }
188
+
189
+ /**
190
+ * Список формальных параметров JS для обёртки `function` (wire `normalizedSig` + `entryStackIds` из bind-values).
191
+ *
192
+ * @param {NormalizedSignature | null | undefined} normalizedSig
193
+ * @param {string[] | null | undefined} entryStackIds
194
+ * @returns {string}
195
+ */
196
+ export function formatFormalParametersForWireSig (normalizedSig, entryStackIds) {
197
+ if (normalizedSig == null || typeof normalizedSig !== 'object') return ''
198
+ const left = Array.isArray(normalizedSig.left) ? normalizedSig.left : []
199
+ if (left.length === 0) return ''
200
+ if (Array.isArray(entryStackIds) && entryStackIds.length === left.length) {
201
+ let pAcc = 0
202
+ const allocP = () => `p${pAcc++}`
203
+ return left.map((slot) => jsParamNameForEntrySlot(slot, allocP)).join(', ')
204
+ }
205
+ return left.map((_, i) => `p${i}`).join(', ')
206
+ }
207
+
208
+ /**
209
+ * Рекурсивно: есть ли в шагах узел с полем `calleeAsync === true` (маркер L3/IR).
210
+ *
211
+ * @param {unknown} steps
212
+ * @returns {boolean}
213
+ */
214
+ export function irStepsContainsCalleeAsync (steps) {
215
+ if (!Array.isArray(steps)) return false
216
+ for (const raw of steps) {
217
+ if (raw == null || typeof raw !== 'object') continue
218
+ const n = /** @type {Record<string, unknown>} */ (raw)
219
+ if (n.calleeAsync === true) return true
220
+ const kind = String(n.kind ?? '')
221
+ if (kind === 'Builtin' && n.name === 'call') {
222
+ const meta = n.callInlineMeta
223
+ if (meta != null && typeof meta === 'object') {
224
+ const m = /** @type {{ innerSteps?: unknown }} */ (meta)
225
+ if (irStepsContainsCalleeAsync(m.innerSteps)) return true
226
+ }
227
+ }
228
+ if (
229
+ kind === 'Builtin' &&
230
+ COMBINATOR_BUILTIN_NAMES.has(String(n.name ?? ''))
231
+ ) {
232
+ const cmeta = n.combinatorInlineMeta
233
+ if (cmeta != null && typeof cmeta === 'object') {
234
+ const parts = /** @type {{ parts?: unknown }} */ (cmeta).parts
235
+ if (Array.isArray(parts)) {
236
+ for (const p of parts) {
237
+ if (
238
+ p != null &&
239
+ typeof p === 'object' &&
240
+ /** @type {{ kind?: string }} */ (p).kind === 'inline' &&
241
+ irStepsContainsCalleeAsync(
242
+ /** @type {{ innerSteps?: unknown }} */ (p).innerSteps
243
+ )
244
+ ) {
245
+ return true
246
+ }
247
+ }
248
+ }
249
+ }
250
+ }
251
+ if (kind === 'Quotation') {
252
+ const qm = n.quoteEmitMeta
253
+ if (qm != null && typeof qm === 'object') {
254
+ const inner = /** @type {{ innerSteps?: unknown }} */ (qm).innerSteps
255
+ if (irStepsContainsCalleeAsync(inner)) return true
256
+ }
257
+ }
258
+ }
259
+ return false
260
+ }
261
+
262
+ /**
263
+ * Рекурсивно: есть ли вызов слова, у которого по сигнатуре всплывает `+Async` (RFC-compile §10, вариант B).
264
+ * Не опирается на `node.calleeAsync` в IR.
265
+ *
266
+ * @param {unknown} steps
267
+ * @param {CalleeAsyncResolver | null | undefined} resolver
268
+ * @returns {boolean}
269
+ */
270
+ export function irStepsCallsPropagatingAsyncCallee (steps, resolver) {
271
+ if (resolver == null || !Array.isArray(steps)) return false
272
+ for (const raw of steps) {
273
+ if (raw == null || typeof raw !== 'object') continue
274
+ const n = /** @type {Record<string, unknown>} */ (raw)
275
+ const kind = String(n.kind ?? '')
276
+ if (kind === 'Word') {
277
+ const ref = n.ref
278
+ if (ref === 'local') {
279
+ if (resolver.localPropagatesAsync(String(n.name ?? ''))) return true
280
+ } else if (ref === 'qualified') {
281
+ if (
282
+ resolver.qualifiedPropagatesAsync(
283
+ String(n.module ?? ''),
284
+ String(n.word ?? n.name ?? '')
285
+ )
286
+ ) {
287
+ return true
288
+ }
289
+ }
290
+ }
291
+ if (kind === 'Builtin' && n.name === 'call') {
292
+ const meta = n.callInlineMeta
293
+ if (meta != null && typeof meta === 'object') {
294
+ const m = /** @type {{ innerSteps?: unknown }} */ (meta)
295
+ if (irStepsCallsPropagatingAsyncCallee(m.innerSteps, resolver)) return true
296
+ }
297
+ }
298
+ if (
299
+ kind === 'Builtin' &&
300
+ COMBINATOR_BUILTIN_NAMES.has(String(n.name ?? ''))
301
+ ) {
302
+ const cmeta = n.combinatorInlineMeta
303
+ if (cmeta != null && typeof cmeta === 'object') {
304
+ const parts = /** @type {{ parts?: unknown }} */ (cmeta).parts
305
+ if (Array.isArray(parts)) {
306
+ for (const p of parts) {
307
+ if (
308
+ p != null &&
309
+ typeof p === 'object' &&
310
+ /** @type {{ kind?: string }} */ (p).kind === 'inline' &&
311
+ irStepsCallsPropagatingAsyncCallee(
312
+ /** @type {{ innerSteps?: unknown }} */ (p).innerSteps,
313
+ resolver
314
+ )
315
+ ) {
316
+ return true
317
+ }
318
+ }
319
+ }
320
+ }
321
+ }
322
+ if (kind === 'Quotation') {
323
+ const qm = n.quoteEmitMeta
324
+ if (qm != null && typeof qm === 'object') {
325
+ const inner = /** @type {{ innerSteps?: unknown }} */ (qm).innerSteps
326
+ if (irStepsCallsPropagatingAsyncCallee(inner, resolver)) return true
327
+ }
328
+ }
329
+ }
330
+ return false
331
+ }
332
+
333
+ /**
334
+ * @param {Record<string, unknown>} node
335
+ * @param {CalleeAsyncResolver | null | undefined} resolver
336
+ * @returns {boolean}
337
+ */
338
+ function calleeStepNeedsAwaitFromSig (node, resolver) {
339
+ if (node.calleeAsync === true) return true
340
+ if (resolver == null) return false
341
+ const kind = String(node.kind ?? '')
342
+ if (kind !== 'Word') return false
343
+ const ref = node.ref
344
+ if (ref === 'local') {
345
+ return resolver.localPropagatesAsync(String(node.name ?? ''))
346
+ }
347
+ if (ref === 'qualified') {
348
+ return resolver.qualifiedPropagatesAsync(
349
+ String(node.module ?? ''),
350
+ String(node.word ?? node.name ?? '')
351
+ )
352
+ }
353
+ return false
354
+ }
355
+
356
+ /**
357
+ * @param {Record<string, unknown>} node
358
+ * @param {{ argIds: string[], resultIds: string[] }} vb
359
+ * @param {Map<string, string>} idToJs
360
+ * @param {() => string} genName
361
+ * @param {string[]} lines
362
+ */
363
+ /**
364
+ * @param {Record<string, unknown>} node
365
+ * @param {{ argIds: string[], resultIds: string[] }} vb
366
+ * @param {number} L
367
+ * @param {Map<string, string>} idToJs
368
+ * @param {() => string} genName
369
+ * @param {string[]} lines
370
+ */
371
+ function emitListLiteralStep (node, vb, L, idToJs, genName, lines) {
372
+ const nIn = vb.argIds.length
373
+ const baseLen = L - nIn
374
+ const topOutIds = vb.resultIds.slice(baseLen)
375
+ if (topOutIds.length !== 1) {
376
+ throw new Error('emit-body: ListLiteral ожидает ровно один новый id на вершине')
377
+ }
378
+ const elements = /** @type {Record<string, unknown>[] | undefined} */ (node.elements)
379
+ if (!Array.isArray(elements)) {
380
+ throw new Error('emit-body: ListLiteral без elements')
381
+ }
382
+ const exprs = elements.map((el) => literalToJsExpr(el))
383
+ const litId = topOutIds[0]
384
+ const nm = genName()
385
+ lines.push(`const ${nm} = [${exprs.join(', ')}];`)
386
+ idToJs.set(litId, nm)
387
+ }
388
+
389
+ /**
390
+ * @param {'read'|'write'} direction
391
+ * @param {string} slotName
392
+ * @param {{ argIds: string[], resultIds: string[] }} vb
393
+ * @param {number} L
394
+ * @param {Map<string, string>} idToJs
395
+ * @param {Map<string, string>} slotExprByName
396
+ * @param {() => string} genName
397
+ * @param {string[]} lines
398
+ * @param {boolean} strict
399
+ * @param {string} wordLabel
400
+ * @param {number} stepIndex
401
+ */
402
+ function emitSlotStep (
403
+ direction,
404
+ slotName,
405
+ vb,
406
+ L,
407
+ idToJs,
408
+ slotExprByName,
409
+ genName,
410
+ lines,
411
+ strict,
412
+ wordLabel,
413
+ stepIndex
414
+ ) {
415
+ if (direction === 'write') {
416
+ const argIds = vb.argIds
417
+ if (argIds.length !== 1) {
418
+ throw new Error(`emit-body[${wordLabel}]: slot_write ожидает 1 argId`)
419
+ }
420
+ const j = idToJs.get(argIds[0])
421
+ if (j == null) {
422
+ throw new Error(`emit-body[${wordLabel}]: slot_write нет JS для arg`)
423
+ }
424
+ slotExprByName.set(slotName, j)
425
+ return
426
+ }
427
+ if (direction === 'read') {
428
+ const src = slotExprByName.get(slotName)
429
+ if (src == null) {
430
+ if (strict) {
431
+ throw new Error(
432
+ `emit-body[${wordLabel}]: шаг ${stepIndex} slot_read :${slotName} без предшествующей записи`
433
+ )
434
+ }
435
+ return
436
+ }
437
+ const nIn = vb.argIds.length
438
+ const baseLen = L - nIn
439
+ const topOutIds = vb.resultIds.slice(baseLen)
440
+ if (topOutIds.length !== 1) {
441
+ throw new Error(`emit-body[${wordLabel}]: slot_read один выход на вершине`)
442
+ }
443
+ const readId = topOutIds[0]
444
+ const nm = genName()
445
+ lines.push(`const ${nm} = ${src};`)
446
+ idToJs.set(readId, nm)
447
+ return
448
+ }
449
+ throw new Error(`emit-body[${wordLabel}]: Slot без direction`)
450
+ }
451
+
452
+ function emitLiteralStep (node, vb, idToJs, genName, lines) {
453
+ const pre = node.preTypes
454
+ const L = Array.isArray(pre) ? pre.length : 0
455
+ const nIn = vb.argIds.length
456
+ const baseLen = L - nIn
457
+ const topOutIds = vb.resultIds.slice(baseLen)
458
+ if (topOutIds.length !== 1) {
459
+ throw new Error('emit-body: Literal ожидает ровно один новый id на вершине')
460
+ }
461
+ const litId = topOutIds[0]
462
+ const expr = literalToJsExpr(node)
463
+ const nm = genName()
464
+ lines.push(`const ${nm} = ${expr};`)
465
+ idToJs.set(litId, nm)
466
+ }
467
+
468
+ /**
469
+ * @param {string} name
470
+ * @param {{ argIds: string[], resultIds: string[] }} vb
471
+ * @param {number} L
472
+ * @param {Map<string, string>} idToJs
473
+ * @param {() => string} genName
474
+ * @param {string[]} lines
475
+ */
476
+ function emitBuiltinShuffleStep (
477
+ name,
478
+ vb,
479
+ L,
480
+ idToJs,
481
+ genName,
482
+ lines
483
+ ) {
484
+ const shuffler = BUILTIN_SHUFFLE_EXPR[name]
485
+ if (!shuffler) return false
486
+ const nIn = vb.argIds.length
487
+ const baseLen = L - nIn
488
+ const topOutIds = vb.resultIds.slice(baseLen)
489
+ const argExprs = vb.argIds.map((id) => {
490
+ const j = idToJs.get(id)
491
+ if (j == null) {
492
+ throw new Error(`emit-body: нет JS-имени для argId ${id} (builtin ${name})`)
493
+ }
494
+ return j
495
+ })
496
+ const outExprs = shuffler(argExprs)
497
+ if (outExprs.length !== topOutIds.length) {
498
+ throw new Error(`emit-body: внутренняя ошибка длины shuffle для ${name}`)
499
+ }
500
+ const seen = new Set()
501
+ for (let i = 0; i < topOutIds.length; i++) {
502
+ const rid = topOutIds[i]
503
+ const expr = outExprs[i]
504
+ if (seen.has(rid)) continue
505
+ seen.add(rid)
506
+ if (isShuffleReuseExpr(expr)) {
507
+ idToJs.set(rid, expr)
508
+ } else {
509
+ const nm = genName()
510
+ lines.push(`const ${nm} = ${expr};`)
511
+ idToJs.set(rid, nm)
512
+ }
513
+ }
514
+ return true
515
+ }
516
+
517
+ /**
518
+ * @param {QuoteEmitMeta} meta
519
+ * @param {{
520
+ * idToJs: Map<string, string>
521
+ * lines: string[]
522
+ * genName: () => string
523
+ * genQuoteName: () => string
524
+ * quoteMetaByValueId: Map<string, QuoteEmitMeta>
525
+ * slotExprByName: Map<string, string>
526
+ * strict: boolean
527
+ * wordLabel: string
528
+ * entryParamCounter: { n: number }
529
+ * callerAsync: boolean
530
+ * calleeAsyncResolver?: CalleeAsyncResolver | null
531
+ * sumEliminatorNames?: Set<string> | null
532
+ * }} env
533
+ * @returns {string} имя `qN`
534
+ */
535
+ function emitQuoteAsFirstClass (meta, env) {
536
+ const n = meta.innerEntryStackIds.length
537
+ const nOut = inferInnerOutLen(meta.innerSteps)
538
+ const params =
539
+ n === 0 ? '' : Array.from({ length: n }, (_, i) => `p${i}`).join(', ')
540
+ const qn = env.genQuoteName()
541
+ const merged = new Map()
542
+ for (let i = 0; i < n; i++) {
543
+ merged.set(meta.innerEntryStackIds[i], `p${i}`)
544
+ }
545
+ const innerLines = []
546
+ const innerAsync = env.calleeAsyncResolver
547
+ ? irStepsCallsPropagatingAsyncCallee(
548
+ meta.innerSteps,
549
+ env.calleeAsyncResolver
550
+ )
551
+ : irStepsContainsCalleeAsync(meta.innerSteps)
552
+ const innerEnv = {
553
+ idToJs: merged,
554
+ lines: innerLines,
555
+ genName: env.genName,
556
+ genQuoteName: env.genQuoteName,
557
+ quoteMetaByValueId: env.quoteMetaByValueId,
558
+ slotExprByName: env.slotExprByName,
559
+ strict: env.strict,
560
+ wordLabel: env.wordLabel,
561
+ entryParamCounter: env.entryParamCounter,
562
+ resolveQualifiedCallee: env.resolveQualifiedCallee,
563
+ callerAsync: innerAsync,
564
+ calleeAsyncResolver: env.calleeAsyncResolver,
565
+ sumEliminatorNames: env.sumEliminatorNames,
566
+ initialStackIds:
567
+ meta.innerEntryStackIds.length > 0 ? meta.innerEntryStackIds : undefined
568
+ }
569
+ const finalIds = processIrSteps(meta.innerSteps, innerEnv)
570
+ const nsig = sigWireFromStackLens(n, nOut)
571
+ appendReturnForLines(innerLines, finalIds, merged, nsig, env.wordLabel)
572
+ const body = innerLines.join('\n')
573
+ const innerBlock = body ? ` ${body.split('\n').join('\n ')}` : ''
574
+ const asyncKw = innerAsync ? 'async ' : ''
575
+ env.lines.push(`const ${qn} = ${asyncKw}(${params}) => {\n${innerBlock}\n}`)
576
+ return qn
577
+ }
578
+
579
+ /**
580
+ * @param {Record<string, unknown>} node
581
+ * @param {{ argIds: string[], resultIds: string[] }} vb
582
+ * @param {number} L
583
+ * @param {Map<string, string>} idToJs
584
+ * @param {() => string} genName
585
+ * @param {string[]} lines
586
+ * @param {Map<string, QuoteEmitMeta>} quoteMetaByValueId
587
+ * @param {() => string} genQuoteName
588
+ * @param {{ n: number }} entryParamCounter
589
+ * @param {boolean} strict
590
+ * @param {string} wordLabel
591
+ * @param {((moduleAlias: string, wordName: string) => string) | undefined} resolveQualifiedCallee
592
+ * @param {boolean} callerAsync
593
+ * @param {CalleeAsyncResolver | null | undefined} calleeAsyncResolver
594
+ * @param {string[]} stackIds стек id до шага (дно→вершина)
595
+ * @param {Set<string> | null | undefined} sumEliminatorNames
596
+ * @param {Map<string, string>} slotExprByName
597
+ */
598
+ function emitWordLocalStep (
599
+ node,
600
+ vb,
601
+ L,
602
+ idToJs,
603
+ genName,
604
+ lines,
605
+ quoteMetaByValueId,
606
+ genQuoteName,
607
+ entryParamCounter,
608
+ strict,
609
+ wordLabel,
610
+ resolveQualifiedCallee,
611
+ callerAsync,
612
+ calleeAsyncResolver,
613
+ stackIds,
614
+ sumEliminatorNames,
615
+ slotExprByName
616
+ ) {
617
+ const name = /** @type {string} */ (node.name)
618
+ assertValidJsCallee(name)
619
+ const nIn = vb.argIds.length
620
+ const baseLen = L - nIn
621
+ const topOutIds = vb.resultIds.slice(baseLen)
622
+ const env = {
623
+ idToJs,
624
+ lines,
625
+ genName,
626
+ genQuoteName,
627
+ quoteMetaByValueId,
628
+ slotExprByName,
629
+ strict,
630
+ wordLabel,
631
+ entryParamCounter,
632
+ resolveQualifiedCallee,
633
+ callerAsync,
634
+ calleeAsyncResolver,
635
+ sumEliminatorNames,
636
+ initialStackIds: undefined
637
+ }
638
+ /** @type {string[]} */
639
+ let argExprs = vb.argIds.map((id) => {
640
+ const qm = quoteMetaByValueId.get(id)
641
+ if (qm) {
642
+ const existing = idToJs.get(id)
643
+ if (existing != null) return existing
644
+ return emitQuoteAsFirstClass(qm, env)
645
+ }
646
+ const j = idToJs.get(id)
647
+ if (j == null) {
648
+ throw new Error(`emit-body: нет JS-имени для argId ${id} (Word ${name})`)
649
+ }
650
+ return j
651
+ })
652
+ if (sumEliminatorNames?.has(name) === true) {
653
+ const prefixLen = L - nIn
654
+ if (stackIds.length !== L) {
655
+ throw new Error(
656
+ `emit-body[${wordLabel}]: неверная длина стека для eliminator (${stackIds.length} !== ${L})`
657
+ )
658
+ }
659
+ const prefixIds = stackIds.slice(0, prefixLen)
660
+ const prefixExprs = prefixIds.map((id) => {
661
+ const qm = quoteMetaByValueId.get(id)
662
+ if (qm) {
663
+ const existing = idToJs.get(id)
664
+ if (existing != null) return existing
665
+ return emitQuoteAsFirstClass(qm, env)
666
+ }
667
+ const j = idToJs.get(id)
668
+ if (j == null) {
669
+ throw new Error(`emit-body: нет JS-имени для prefix id ${id} (Word ${name})`)
670
+ }
671
+ return j
672
+ })
673
+ argExprs = [...prefixExprs, ...argExprs]
674
+ }
675
+ const innerCall = `${name}(${argExprs.join(', ')})`
676
+ const useAwait =
677
+ callerAsync && calleeStepNeedsAwaitFromSig(node, calleeAsyncResolver)
678
+ const callExpr = useAwait ? `await ${innerCall}` : innerCall
679
+ if (topOutIds.length === 0) {
680
+ lines.push(`${callExpr};`)
681
+ return
682
+ }
683
+ if (topOutIds.length === 1) {
684
+ const nm = genName()
685
+ lines.push(`const ${nm} = ${callExpr};`)
686
+ idToJs.set(topOutIds[0], nm)
687
+ return
688
+ }
689
+ const names = topOutIds.map(() => genName())
690
+ lines.push(`const [${names.join(', ')}] = ${callExpr};`)
691
+ for (let i = 0; i < topOutIds.length; i++) {
692
+ idToJs.set(topOutIds[i], names[i])
693
+ }
694
+ }
695
+
696
+ /**
697
+ * @param {Record<string, unknown>} node
698
+ * @param {{ argIds: string[], resultIds: string[] }} vb
699
+ * @param {number} L
700
+ * @param {Map<string, string>} idToJs
701
+ * @param {() => string} genName
702
+ * @param {string[]} lines
703
+ * @param {Map<string, QuoteEmitMeta>} quoteMetaByValueId
704
+ * @param {() => string} genQuoteName
705
+ * @param {{ n: number }} entryParamCounter
706
+ * @param {boolean} strict
707
+ * @param {string} wordLabel
708
+ * @param {(moduleAlias: string, wordName: string) => string} resolveQualifiedCallee
709
+ * @param {boolean} callerAsync
710
+ * @param {CalleeAsyncResolver | null | undefined} calleeAsyncResolver
711
+ * @param {Map<string, string>} slotExprByName
712
+ */
713
+ function emitWordQualifiedStep (
714
+ node,
715
+ vb,
716
+ L,
717
+ idToJs,
718
+ genName,
719
+ lines,
720
+ quoteMetaByValueId,
721
+ genQuoteName,
722
+ entryParamCounter,
723
+ strict,
724
+ wordLabel,
725
+ resolveQualifiedCallee,
726
+ callerAsync,
727
+ calleeAsyncResolver,
728
+ slotExprByName
729
+ ) {
730
+ const mod = /** @type {string} */ (node.module)
731
+ const name = /** @type {string} */ (node.word ?? node.name)
732
+ const calleeExpr = resolveQualifiedCallee(mod, name)
733
+ const nIn = vb.argIds.length
734
+ const baseLen = L - nIn
735
+ const topOutIds = vb.resultIds.slice(baseLen)
736
+ const env = {
737
+ idToJs,
738
+ lines,
739
+ genName,
740
+ genQuoteName,
741
+ quoteMetaByValueId,
742
+ slotExprByName,
743
+ strict,
744
+ wordLabel,
745
+ entryParamCounter,
746
+ resolveQualifiedCallee,
747
+ callerAsync,
748
+ calleeAsyncResolver,
749
+ sumEliminatorNames: null,
750
+ initialStackIds: undefined
751
+ }
752
+ const argExprs = vb.argIds.map((id) => {
753
+ const qm = quoteMetaByValueId.get(id)
754
+ if (qm) {
755
+ const existing = idToJs.get(id)
756
+ if (existing != null) return existing
757
+ return emitQuoteAsFirstClass(qm, env)
758
+ }
759
+ const j = idToJs.get(id)
760
+ if (j == null) {
761
+ throw new Error(`emit-body: нет JS-имени для argId ${id} (qualified ${mod}/${name})`)
762
+ }
763
+ return j
764
+ })
765
+ const innerCall = `${calleeExpr}(${argExprs.join(', ')})`
766
+ const useAwait =
767
+ callerAsync && calleeStepNeedsAwaitFromSig(node, calleeAsyncResolver)
768
+ const callExpr = useAwait ? `await ${innerCall}` : innerCall
769
+ if (topOutIds.length === 0) {
770
+ lines.push(`${callExpr};`)
771
+ return
772
+ }
773
+ if (topOutIds.length === 1) {
774
+ const nm = genName()
775
+ lines.push(`const ${nm} = ${callExpr};`)
776
+ idToJs.set(topOutIds[0], nm)
777
+ return
778
+ }
779
+ const names = topOutIds.map(() => genName())
780
+ lines.push(`const [${names.join(', ')}] = ${callExpr};`)
781
+ for (let i = 0; i < topOutIds.length; i++) {
782
+ idToJs.set(topOutIds[i], names[i])
783
+ }
784
+ }
785
+
786
+ /**
787
+ * @param {Record<string, unknown>} node
788
+ * @param {{ argIds: string[], resultIds: string[] }} vb
789
+ * @param {number} L
790
+ * @param {{
791
+ * idToJs: Map<string, string>
792
+ * lines: string[]
793
+ * genName: () => string
794
+ * genQuoteName: () => string
795
+ * quoteMetaByValueId: Map<string, QuoteEmitMeta>
796
+ * strict: boolean
797
+ * wordLabel: string
798
+ * entryParamCounter: { n: number }
799
+ * resolveQualifiedCallee?: (moduleAlias: string, wordName: string) => string
800
+ * callerAsync: boolean
801
+ * calleeAsyncResolver?: CalleeAsyncResolver | null
802
+ * }} env
803
+ */
804
+ function emitCallInlineStep (node, vb, L, env) {
805
+ const meta = /** @type {{
806
+ kind?: 'inline' | 'invoke'
807
+ innerSteps?: Record<string, unknown>[]
808
+ innerEntryStackIds?: string[]
809
+ innerInLen?: number
810
+ innerOutLen?: number
811
+ } | undefined} */ (node.callInlineMeta)
812
+ if (!meta || typeof meta !== 'object') {
813
+ throw new Error(`emit-body[${env.wordLabel}]: call без callInlineMeta`)
814
+ }
815
+
816
+ const baseLen = L - vb.argIds.length
817
+ const topOutIds = vb.resultIds.slice(baseLen)
818
+ const m = topOutIds.length
819
+
820
+ if (meta.kind === 'invoke') {
821
+ const n = meta.innerInLen
822
+ const mMeta = meta.innerOutLen
823
+ if (typeof n !== 'number' || typeof mMeta !== 'number') {
824
+ throw new Error(`emit-body[${env.wordLabel}]: call invoke — нет innerInLen/innerOutLen`)
825
+ }
826
+ if (m !== mMeta) {
827
+ throw new Error(
828
+ `emit-body[${env.wordLabel}]: call invoke — m пост-стека (${m}) ≠ innerOutLen (${mMeta})`
829
+ )
830
+ }
831
+ if (vb.argIds.length !== n + 1) {
832
+ throw new Error(
833
+ `emit-body[${env.wordLabel}]: call invoke — ожидалось ${n + 1} argIds, есть ${vb.argIds.length}`
834
+ )
835
+ }
836
+ const quoteId = vb.argIds[vb.argIds.length - 1]
837
+ const quoteJs = env.idToJs.get(quoteId)
838
+ if (quoteJs == null) {
839
+ throw new Error(`emit-body[${env.wordLabel}]: call invoke — нет JS для quote id ${quoteId}`)
840
+ }
841
+ const callArgExprs = vb.argIds.slice(0, -1).map((id) => {
842
+ const j = env.idToJs.get(id)
843
+ if (j == null) {
844
+ throw new Error(`emit-body[${env.wordLabel}]: call invoke — нет JS для arg ${id}`)
845
+ }
846
+ return j
847
+ })
848
+ const paren = callArgExprs.length ? `(${callArgExprs.join(', ')})` : '()'
849
+ let callExpr = `${quoteJs}${paren}`
850
+ const qm = env.quoteMetaByValueId.get(quoteId)
851
+ const invokeInnerAsync =
852
+ qm != null &&
853
+ env.calleeAsyncResolver != null &&
854
+ irStepsCallsPropagatingAsyncCallee(
855
+ qm.innerSteps,
856
+ env.calleeAsyncResolver
857
+ )
858
+ const useAwaitInvoke =
859
+ env.callerAsync &&
860
+ (node.calleeAsync === true || invokeInnerAsync)
861
+ if (useAwaitInvoke) {
862
+ callExpr = `await ${callExpr}`
863
+ }
864
+ if (m === 0) {
865
+ env.lines.push(`${callExpr};`)
866
+ } else if (m === 1) {
867
+ const nm = env.genName()
868
+ env.lines.push(`const ${nm} = ${callExpr};`)
869
+ env.idToJs.set(topOutIds[0], nm)
870
+ } else {
871
+ const names = topOutIds.map(() => env.genName())
872
+ env.lines.push(`const [${names.join(', ')}] = ${callExpr};`)
873
+ for (let i = 0; i < m; i++) {
874
+ env.idToJs.set(topOutIds[i], names[i])
875
+ }
876
+ }
877
+ return
878
+ }
879
+
880
+ const inlineKind = meta.kind == null || meta.kind === 'inline'
881
+ if (
882
+ !inlineKind ||
883
+ !Array.isArray(meta.innerSteps) ||
884
+ !Array.isArray(meta.innerEntryStackIds)
885
+ ) {
886
+ throw new Error(`emit-body[${env.wordLabel}]: call без inline callInlineMeta`)
887
+ }
888
+ const innerArgs = vb.argIds.slice(0, -1)
889
+ if (innerArgs.length !== meta.innerEntryStackIds.length) {
890
+ throw new Error(`emit-body[${env.wordLabel}]: call — несовпадение числа аргументов quotation`)
891
+ }
892
+ const merged = new Map(env.idToJs)
893
+ for (let i = 0; i < meta.innerEntryStackIds.length; i++) {
894
+ const aid = innerArgs[i]
895
+ const j = env.idToJs.get(aid)
896
+ if (j == null) {
897
+ throw new Error(`emit-body[${env.wordLabel}]: call — нет JS для arg ${aid}`)
898
+ }
899
+ merged.set(meta.innerEntryStackIds[i], j)
900
+ }
901
+ const innerEnv = {
902
+ idToJs: merged,
903
+ lines: env.lines,
904
+ genName: env.genName,
905
+ genQuoteName: env.genQuoteName,
906
+ quoteMetaByValueId: env.quoteMetaByValueId,
907
+ slotExprByName: env.slotExprByName,
908
+ strict: env.strict,
909
+ wordLabel: env.wordLabel,
910
+ entryParamCounter: env.entryParamCounter,
911
+ resolveQualifiedCallee: env.resolveQualifiedCallee,
912
+ callerAsync: env.callerAsync,
913
+ calleeAsyncResolver: env.calleeAsyncResolver,
914
+ sumEliminatorNames: env.sumEliminatorNames,
915
+ initialStackIds:
916
+ Array.isArray(meta.innerEntryStackIds) && meta.innerEntryStackIds.length > 0
917
+ ? meta.innerEntryStackIds
918
+ : undefined
919
+ }
920
+ const innerFinalIds = processIrSteps(meta.innerSteps, innerEnv)
921
+ if (innerFinalIds.length !== m) {
922
+ throw new Error(
923
+ `emit-body[${env.wordLabel}]: call — ожидалось ${m} выходов, внутри ${innerFinalIds.length}`
924
+ )
925
+ }
926
+ for (let i = 0; i < m; i++) {
927
+ const rid = topOutIds[i]
928
+ const innerId = innerFinalIds[i]
929
+ const j = merged.get(innerId)
930
+ if (j == null) {
931
+ throw new Error(`emit-body[${env.wordLabel}]: call — нет JS для внутреннего id ${innerId}`)
932
+ }
933
+ env.idToJs.set(rid, j)
934
+ }
935
+ }
936
+
937
+ /**
938
+ * @param {Record<string, unknown>[]} irSteps
939
+ * @param {{
940
+ * idToJs: Map<string, string>
941
+ * lines: string[]
942
+ * genName: () => string
943
+ * genQuoteName: () => string
944
+ * quoteMetaByValueId: Map<string, QuoteEmitMeta>
945
+ * slotExprByName: Map<string, string>
946
+ * strict: boolean
947
+ * wordLabel: string
948
+ * entryParamCounter: { n: number }
949
+ * resolveQualifiedCallee?: (moduleAlias: string, wordName: string) => string
950
+ * callerAsync: boolean
951
+ * calleeAsyncResolver?: CalleeAsyncResolver | null
952
+ * sumEliminatorNames?: Set<string> | null
953
+ * initialStackIds?: string[] | undefined
954
+ * }} env
955
+ * @returns {string[]}
956
+ */
957
+ function processIrSteps (irSteps, env) {
958
+ /** @type {string[]} */
959
+ let currentStackIds = Array.isArray(env.initialStackIds)
960
+ ? [...env.initialStackIds]
961
+ : []
962
+ const {
963
+ idToJs,
964
+ lines,
965
+ genName,
966
+ genQuoteName,
967
+ quoteMetaByValueId,
968
+ slotExprByName,
969
+ strict,
970
+ wordLabel,
971
+ entryParamCounter,
972
+ resolveQualifiedCallee,
973
+ sumEliminatorNames
974
+ } = env
975
+
976
+ if (!Array.isArray(irSteps)) {
977
+ throw new Error('emit-body: irSteps должен быть массивом')
978
+ }
979
+
980
+ for (let si = 0; si < irSteps.length; si++) {
981
+ const raw = irSteps[si]
982
+ if (raw == null || typeof raw !== 'object') continue
983
+ const node = /** @type {Record<string, unknown>} */ (raw)
984
+ const kind = String(node.kind ?? '')
985
+ const vb = /** @type {{ argIds?: string[], resultIds?: string[] } | undefined} */ (
986
+ node.valueBindings
987
+ )
988
+ const pre = node.preTypes
989
+ const L = Array.isArray(pre) ? pre.length : -1
990
+
991
+ if (!vb || !Array.isArray(vb.argIds) || !Array.isArray(vb.resultIds)) {
992
+ if (strict) {
993
+ throw new Error(
994
+ `emit-body[${wordLabel}]: шаг ${si} (${kind}) без valueBindings`
995
+ )
996
+ }
997
+ continue
998
+ }
999
+
1000
+ if (L < 0) {
1001
+ if (strict) {
1002
+ throw new Error(`emit-body[${wordLabel}]: шаг ${si} (${kind}) без preTypes`)
1003
+ }
1004
+ continue
1005
+ }
1006
+
1007
+ for (const aid of vb.argIds) {
1008
+ if (quoteMetaByValueId.has(aid)) continue
1009
+ if (!idToJs.has(aid)) {
1010
+ const pn = `p${entryParamCounter.n++}`
1011
+ idToJs.set(aid, pn)
1012
+ }
1013
+ }
1014
+
1015
+ const stackIds = currentStackIds
1016
+
1017
+ switch (kind) {
1018
+ case 'Literal':
1019
+ emitLiteralStep(node, vb, idToJs, genName, lines)
1020
+ break
1021
+ case 'ListLiteral':
1022
+ emitListLiteralStep(node, vb, L, idToJs, genName, lines)
1023
+ break
1024
+ case 'Builtin': {
1025
+ const bname = /** @type {string} */ (node.name)
1026
+ if (bname === 'call') {
1027
+ emitCallInlineStep(node, vb, L, env)
1028
+ break
1029
+ }
1030
+ if (tryEmitCombinatorBuiltin(bname, node, vb, L, env, processIrSteps)) {
1031
+ break
1032
+ }
1033
+ if (!emitBuiltinShuffleStep(bname, vb, L, idToJs, genName, lines)) {
1034
+ throw new Error(
1035
+ `emit-body[${wordLabel}]: неподдержанный builtin ${JSON.stringify(bname)}`
1036
+ )
1037
+ }
1038
+ break
1039
+ }
1040
+ case 'Slot': {
1041
+ const dir = /** @type {'read'|'write' | undefined} */ (node.direction)
1042
+ const slotName = String(node.slotName ?? '')
1043
+ if (dir !== 'read' && dir !== 'write') {
1044
+ throw new Error(`emit-body[${wordLabel}]: Slot без direction`)
1045
+ }
1046
+ emitSlotStep(
1047
+ dir,
1048
+ slotName,
1049
+ vb,
1050
+ L,
1051
+ idToJs,
1052
+ slotExprByName,
1053
+ genName,
1054
+ lines,
1055
+ strict,
1056
+ wordLabel,
1057
+ si
1058
+ )
1059
+ break
1060
+ }
1061
+ case 'Unknown': {
1062
+ const ak = String(node.astKind ?? 'unknown')
1063
+ throw new Error(
1064
+ `emit-body[${wordLabel}]: шаг ${si} недопустим для codegen (Unknown AST ${ak})`
1065
+ )
1066
+ }
1067
+ case 'Word': {
1068
+ const ref = node.ref
1069
+ if (ref === 'local') {
1070
+ emitWordLocalStep(
1071
+ node,
1072
+ vb,
1073
+ L,
1074
+ idToJs,
1075
+ genName,
1076
+ lines,
1077
+ quoteMetaByValueId,
1078
+ genQuoteName,
1079
+ entryParamCounter,
1080
+ strict,
1081
+ wordLabel,
1082
+ resolveQualifiedCallee,
1083
+ env.callerAsync,
1084
+ env.calleeAsyncResolver,
1085
+ stackIds,
1086
+ sumEliminatorNames ?? null,
1087
+ slotExprByName
1088
+ )
1089
+ } else if (ref === 'qualified') {
1090
+ if (typeof resolveQualifiedCallee !== 'function') {
1091
+ throw new Error(
1092
+ `emit-body[${wordLabel}]: qualified Word без resolveQualifiedCallee`
1093
+ )
1094
+ }
1095
+ emitWordQualifiedStep(
1096
+ node,
1097
+ vb,
1098
+ L,
1099
+ idToJs,
1100
+ genName,
1101
+ lines,
1102
+ quoteMetaByValueId,
1103
+ genQuoteName,
1104
+ entryParamCounter,
1105
+ strict,
1106
+ wordLabel,
1107
+ resolveQualifiedCallee,
1108
+ env.callerAsync,
1109
+ env.calleeAsyncResolver,
1110
+ slotExprByName
1111
+ )
1112
+ } else {
1113
+ throw new Error(
1114
+ `emit-body[${wordLabel}]: Word ref=${String(ref)} пока не поддержан`
1115
+ )
1116
+ }
1117
+ break
1118
+ }
1119
+ case 'Quotation': {
1120
+ const meta = /** @type {QuoteEmitMeta | undefined} */ (node.quoteEmitMeta)
1121
+ if (!meta) {
1122
+ throw new Error(`emit-body[${wordLabel}]: Quotation без quoteEmitMeta`)
1123
+ }
1124
+ const delta = vb.resultIds.length - L
1125
+ const newIds = vb.resultIds.slice(L)
1126
+ if (newIds.length !== delta) {
1127
+ throw new Error('emit-body: Quotation delta ids')
1128
+ }
1129
+ for (const qid of newIds) {
1130
+ quoteMetaByValueId.set(qid, meta)
1131
+ }
1132
+ break
1133
+ }
1134
+ default:
1135
+ throw new Error(`emit-body[${wordLabel}]: неподдержанный kind ${kind}`)
1136
+ }
1137
+
1138
+ currentStackIds = [...vb.resultIds]
1139
+ }
1140
+
1141
+ return currentStackIds
1142
+ }
1143
+
1144
+ /**
1145
+ * Генерирует JS-фрагмент тела слова по `irSteps` с заполненными `valueBindings`.
1146
+ *
1147
+ * Контракт `return`: если передан `normalizedSig`, в конец добавляется `return`:
1148
+ * один выход — `return <expr>;`, несколько — `return [<expr>, ...];` (дно → вершина),
1149
+ * ноль выходов — `return;`. Без `normalizedSig` оператор `return` не добавляется.
1150
+ *
1151
+ * @param {Record<string, unknown>[]} irSteps
1152
+ * @param {{
1153
+ * strict?: boolean
1154
+ * normalizedSig?: NormalizedSignature | null
1155
+ * wordName?: string
1156
+ * entryStackIds?: string[] | null
1157
+ * resolveQualifiedCallee?: (moduleAlias: string, wordName: string) => string
1158
+ * callerAsync?: boolean
1159
+ * calleeAsyncResolver?: CalleeAsyncResolver | null
1160
+ * sumEliminatorNames?: Set<string> | null
1161
+ * }} [options]
1162
+ * @returns {{ source: string }}
1163
+ *
1164
+ * Входной стек слова: если `bind-values` подставил «resync»-id до первого реального шага,
1165
+ * в теле используются свободные имена `p0`, `p1`, … (дно → вершина, вершина — больший индекс),
1166
+ * как формальные параметры будущей обёртки-функции.
1167
+ */
1168
+ export function emitWordBodyIr (irSteps, options = {}) {
1169
+ const strict = options.strict === true
1170
+ const wordLabel = options.wordName != null ? String(options.wordName) : 'word'
1171
+ /** @type {Map<string, string>} */
1172
+ const idToJs = new Map()
1173
+ let counter = 0
1174
+ let quoteCounter = 0
1175
+ const genName = () => {
1176
+ const n = counter++
1177
+ return `v${n}`
1178
+ }
1179
+ const genQuoteName = () => {
1180
+ const n = quoteCounter++
1181
+ return `q${n}`
1182
+ }
1183
+ /** @type {Map<string, QuoteEmitMeta>} */
1184
+ const quoteMetaByValueId = new Map()
1185
+ /** @type {Map<string, string>} */
1186
+ const slotExprByName = new Map()
1187
+ /** @type {string[]} */
1188
+ const lines = []
1189
+ const entryParamCounter = { n: 0 }
1190
+
1191
+ const env = {
1192
+ idToJs,
1193
+ lines,
1194
+ genName,
1195
+ genQuoteName,
1196
+ quoteMetaByValueId,
1197
+ slotExprByName,
1198
+ strict,
1199
+ wordLabel,
1200
+ entryParamCounter,
1201
+ resolveQualifiedCallee: options.resolveQualifiedCallee,
1202
+ callerAsync: options.callerAsync === true,
1203
+ calleeAsyncResolver: options.calleeAsyncResolver ?? null,
1204
+ sumEliminatorNames: options.sumEliminatorNames ?? null,
1205
+ initialStackIds:
1206
+ Array.isArray(options.entryStackIds) && options.entryStackIds.length > 0
1207
+ ? options.entryStackIds
1208
+ : undefined
1209
+ }
1210
+
1211
+ const nsig = options.normalizedSig
1212
+ const entryStackIds = options.entryStackIds
1213
+ if (
1214
+ nsig != null &&
1215
+ typeof nsig === 'object' &&
1216
+ Array.isArray(nsig.left) &&
1217
+ Array.isArray(entryStackIds) &&
1218
+ entryStackIds.length === nsig.left.length
1219
+ ) {
1220
+ let pAcc = 0
1221
+ const allocP = () => `p${pAcc++}`
1222
+ for (let i = 0; i < entryStackIds.length; i++) {
1223
+ idToJs.set(entryStackIds[i], jsParamNameForEntrySlot(nsig.left[i], allocP))
1224
+ }
1225
+ entryParamCounter.n = pAcc
1226
+ }
1227
+
1228
+ const currentStackIds = processIrSteps(irSteps, env)
1229
+
1230
+ if (options.normalizedSig != null) {
1231
+ for (const id of currentStackIds) {
1232
+ if (!idToJs.has(id) && quoteMetaByValueId.has(id)) {
1233
+ const meta = quoteMetaByValueId.get(id)
1234
+ if (meta) {
1235
+ const qn = emitQuoteAsFirstClass(meta, {
1236
+ idToJs,
1237
+ lines,
1238
+ genName,
1239
+ genQuoteName,
1240
+ quoteMetaByValueId,
1241
+ slotExprByName,
1242
+ strict,
1243
+ wordLabel,
1244
+ entryParamCounter,
1245
+ resolveQualifiedCallee: options.resolveQualifiedCallee,
1246
+ callerAsync: options.callerAsync === true,
1247
+ calleeAsyncResolver: options.calleeAsyncResolver ?? null,
1248
+ sumEliminatorNames: options.sumEliminatorNames ?? null,
1249
+ initialStackIds: undefined
1250
+ })
1251
+ idToJs.set(id, qn)
1252
+ }
1253
+ }
1254
+ }
1255
+ appendReturnForLines(
1256
+ lines,
1257
+ currentStackIds,
1258
+ idToJs,
1259
+ options.normalizedSig,
1260
+ wordLabel
1261
+ )
1262
+ }
1263
+
1264
+ return { source: lines.join('\n') }
1265
+ }