@algosail/lang 0.2.11 → 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
@@ -0,0 +1,594 @@
1
+ /**
2
+ * L4 фаза 4: привязки значений (RFC-IR section 4).
3
+ * Внутренний массив id (дно→вершина) — только алгоритм обхода; в wire нет поля «стек».
4
+ *
5
+ * На узле шага опционально **`valueBindings`**:
6
+ * - `argIds: string[]` — id, снимаемые для эффекта шага; **последний элемент — вершина** до шага.
7
+ * - `resultIds: string[]` — id после шага, порядок **дно → вершина** (как `postTypes`).
8
+ * - `dup`: один `argId`, два одинаковых id в `resultIds` (та же привязка дважды).
9
+ *
10
+ * Шаги без `preTypes`/`postTypes` или при несовпадении длины стека id с `preTypes` (в режиме по умолчанию):
11
+ * привязки для шага не выставляются, стек id пересобирается из `postTypes` свежими id, чтобы не ломать длину для следующих шагов.
12
+ * `strict: true` — ошибка при несовместимости.
13
+ *
14
+ * Семантика арности встроенных слов — по шаблонам {@link getBuiltinStackSignature} (без повторного typecheck).
15
+ */
16
+
17
+ import { getBuiltinStackSignature } from '../typecheck/builtin-signatures.js'
18
+
19
+ /**
20
+ * @typedef {{
21
+ * wordName: string
22
+ * modulePath: string
23
+ * strict?: boolean
24
+ * resolveLocalWordSig?: (name: string) => import('../typecheck/normalize-sig.js').NormalizedSignature | null | undefined
25
+ * resolveQualifiedWordSig?: (module: string, word: string) => import('../typecheck/normalize-sig.js').NormalizedSignature | null | undefined
26
+ * }} BindValueBindingsOptions
27
+ */
28
+
29
+ /** RFC-builtins §4.4–4.5: ветки valueBindings + combinatorInlineMeta для async-обхода. */
30
+ const BUILTIN_COMBINATOR_NAMES = new Set([
31
+ 'dip',
32
+ 'keep',
33
+ 'bi',
34
+ 'tri',
35
+ 'spread',
36
+ 'both'
37
+ ])
38
+
39
+ /** @type {Record<string, (ids: string[]) => string[]>} */
40
+ const BUILTIN_SHUFFLE_TOP = {
41
+ dup: (ids) => [ids[0], ids[0]],
42
+ drop: () => [],
43
+ swap: (ids) => [ids[1], ids[0]],
44
+ over: (ids) => [ids[0], ids[1], ids[0]],
45
+ dup2: (ids) => [ids[0], ids[1], ids[0], ids[1]],
46
+ drop2: () => [],
47
+ rot: (ids) => [ids[1], ids[2], ids[0]],
48
+ reverse: (ids) => [ids[2], ids[0], ids[1]],
49
+ nip: (ids) => [ids[1]],
50
+ tuck: (ids) => [ids[1], ids[0], ids[1]],
51
+ swap2: (ids) => [...ids.slice(2, 4), ...ids.slice(0, 2)]
52
+ }
53
+
54
+ /**
55
+ * @param {BindValueBindingsOptions} options
56
+ */
57
+ function createCtx (options) {
58
+ let counter = 0
59
+ const base = `${options.modulePath}#${options.wordName}#`
60
+ return {
61
+ options,
62
+ strict: options.strict === true,
63
+ stack: /** @type {string[]} */ ([]),
64
+ /** дно→вершина: id входного стека слова после первого resync (только корневой bindRecursive) */
65
+ entryStackIds: /** @type {string[] | null} */ (null),
66
+ /** id значения Quote → тело для codegen (этап 2: `call`, HOF) */
67
+ quoteRegistry:
68
+ /** @type {Map<string, { innerSteps: Record<string, unknown>[], innerEntryStackIds: string[] }>} */ (
69
+ new Map()
70
+ ),
71
+ makeId (hint = 'v') {
72
+ counter++
73
+ return `${base}${hint}${counter}`
74
+ }
75
+ }
76
+ }
77
+
78
+ /**
79
+ * @param {{ stack: string[], makeId: (hint?: string) => string, strict: boolean }} ctx
80
+ * @param {number} R
81
+ */
82
+ function resyncStack (ctx, R) {
83
+ ctx.stack = Array.from({ length: R }, () => ctx.makeId('resync'))
84
+ }
85
+
86
+ /**
87
+ * @param {Record<string, unknown>} node
88
+ * @param {string[]} argIds
89
+ * @param {string[]} resultIds
90
+ */
91
+ function attachBindings (node, argIds, resultIds) {
92
+ node.valueBindings = { argIds: [...argIds], resultIds: [...resultIds] }
93
+ }
94
+
95
+ /**
96
+ * @param {Record<string, unknown>[]} irSteps
97
+ * @param {{ stack: string[], makeId: (hint?: string) => string, strict: boolean, options: BindValueBindingsOptions, entryStackIds: string[] | null }} ctx
98
+ * @param {boolean} isRootLevel
99
+ */
100
+ function bindRecursive (irSteps, ctx, isRootLevel) {
101
+ for (let si = 0; si < irSteps.length; si++) {
102
+ const raw = irSteps[si]
103
+ if (raw == null || typeof raw !== 'object') continue
104
+ const node = /** @type {Record<string, unknown>} */ (raw)
105
+ const pre = node.preTypes
106
+ const post = node.postTypes
107
+ if (!Array.isArray(pre)) {
108
+ if (ctx.strict) {
109
+ throw new Error(`bind-values: шаг без preTypes (${node.kind})`)
110
+ }
111
+ continue
112
+ }
113
+ const L = pre.length
114
+ const R =
115
+ post !== undefined && Array.isArray(post) ? post.length : null
116
+ if (R === null) {
117
+ if (ctx.strict) {
118
+ throw new Error(`bind-values: шаг без postTypes (${node.kind})`)
119
+ }
120
+ continue
121
+ }
122
+
123
+ if (ctx.stack.length !== L) {
124
+ if (ctx.strict) {
125
+ throw new Error(
126
+ `bind-values: ожидалась длина стека id ${L}, есть ${ctx.stack.length} (${node.kind})`
127
+ )
128
+ }
129
+ resyncStack(ctx, L)
130
+ }
131
+
132
+ if (isRootLevel && si === 0) {
133
+ ctx.entryStackIds = [...ctx.stack]
134
+ }
135
+
136
+ const stackIds = ctx.stack
137
+
138
+ switch (node.kind) {
139
+ case 'Literal':
140
+ case 'ListLiteral': {
141
+ const litId = ctx.makeId('lit')
142
+ const argIds = []
143
+ const resultIds = [...stackIds, litId]
144
+ attachBindings(node, argIds, resultIds)
145
+ ctx.stack = resultIds
146
+ break
147
+ }
148
+ case 'Builtin': {
149
+ const name = /** @type {string} */ (node.name)
150
+ if (name === 'call') {
151
+ if (L < 1) {
152
+ if (ctx.strict) {
153
+ throw new Error('bind-values: call при пустом стеке id')
154
+ }
155
+ resyncStack(ctx, R)
156
+ break
157
+ }
158
+ const quoteId = stackIds[L - 1]
159
+ const reg = ctx.quoteRegistry.get(quoteId)
160
+ const topT =
161
+ Array.isArray(node.preTypes) && node.preTypes.length >= L
162
+ ? node.preTypes[L - 1]
163
+ : null
164
+ const qInner =
165
+ topT != null &&
166
+ typeof topT === 'object' &&
167
+ (topT.kind === 'quote' || topT.kind === 'named_quote') &&
168
+ topT.inner != null &&
169
+ typeof topT.inner === 'object'
170
+ ? /** @type {{ left?: unknown[], right?: unknown[] }} */ (topT.inner)
171
+ : null
172
+
173
+ /** @type {number} */
174
+ let n
175
+ /** @type {Record<string, unknown>[]} */
176
+ let innerStepsArr
177
+ /** @type {string[]} */
178
+ let innerEntryArr
179
+ /** @type {'inline' | 'invoke'} */
180
+ let callKind
181
+
182
+ if (reg) {
183
+ n = reg.innerEntryStackIds.length
184
+ innerStepsArr = reg.innerSteps
185
+ innerEntryArr = [...reg.innerEntryStackIds]
186
+ callKind = 'inline'
187
+ } else if (qInner) {
188
+ n = Array.isArray(qInner.left) ? qInner.left.length : 0
189
+ innerStepsArr = []
190
+ innerEntryArr = []
191
+ callKind = 'invoke'
192
+ } else {
193
+ if (ctx.strict) {
194
+ throw new Error(
195
+ `bind-values: call — нет quotation в реестре и нет Quote в preTypes для id ${quoteId}`
196
+ )
197
+ }
198
+ resyncStack(ctx, R)
199
+ break
200
+ }
201
+
202
+ if (L < 1 + n) {
203
+ if (ctx.strict) {
204
+ throw new Error('bind-values: call — недостаточно слотов под вход quotation')
205
+ }
206
+ resyncStack(ctx, R)
207
+ break
208
+ }
209
+ const prefLen = L - 1 - n
210
+ const m = R - prefLen
211
+ if (m < 0 || prefLen < 0) {
212
+ if (ctx.strict) {
213
+ throw new Error('bind-values: call — несогласованные длины pre/post')
214
+ }
215
+ resyncStack(ctx, R)
216
+ break
217
+ }
218
+ if (
219
+ callKind === 'invoke' &&
220
+ qInner &&
221
+ Array.isArray(qInner.right) &&
222
+ qInner.right.length !== m
223
+ ) {
224
+ if (ctx.strict) {
225
+ throw new Error(
226
+ `bind-values: call invoke — несовпадение m (${m}) и inner.right (${qInner.right.length})`
227
+ )
228
+ }
229
+ resyncStack(ctx, R)
230
+ break
231
+ }
232
+ const argIds = stackIds.slice(prefLen)
233
+ const baseIds = stackIds.slice(0, prefLen)
234
+ const topOut = Array.from({ length: m }, () => ctx.makeId('call'))
235
+ const resultIds = baseIds.concat(topOut)
236
+ if (resultIds.length !== R) {
237
+ if (ctx.strict) {
238
+ throw new Error(
239
+ `bind-values: call: ожидалась длина пост-стека ${R}, получилось ${resultIds.length}`
240
+ )
241
+ }
242
+ resyncStack(ctx, R)
243
+ break
244
+ }
245
+ /** @type {Record<string, unknown>} */
246
+ const callNode = node
247
+ if (callKind === 'inline') {
248
+ callNode.callInlineMeta = {
249
+ kind: 'inline',
250
+ innerSteps: innerStepsArr,
251
+ innerEntryStackIds: innerEntryArr
252
+ }
253
+ } else {
254
+ callNode.callInlineMeta = {
255
+ kind: 'invoke',
256
+ innerInLen: n,
257
+ innerOutLen: m
258
+ }
259
+ }
260
+ attachBindings(callNode, argIds, resultIds)
261
+ ctx.stack = resultIds
262
+ break
263
+ }
264
+ if (BUILTIN_COMBINATOR_NAMES.has(name)) {
265
+ const combTemplate = getBuiltinStackSignature(name)
266
+ if (!combTemplate) {
267
+ if (ctx.strict) {
268
+ throw new Error(`bind-values: нет сигнатуры для комбинатора ${name}`)
269
+ }
270
+ resyncStack(ctx, R)
271
+ break
272
+ }
273
+ const nCombIn = combTemplate.left.length
274
+ const nCombOut = combTemplate.right.length
275
+ const combArgIds =
276
+ nCombIn === 0 ? [] : stackIds.slice(-nCombIn)
277
+ const combBase =
278
+ nCombIn === 0 ? stackIds : stackIds.slice(0, stackIds.length - nCombIn)
279
+ const mk = () => ctx.makeId(name)
280
+ /** @type {string[]} */
281
+ let combTopOut
282
+ switch (name) {
283
+ case 'dip':
284
+ case 'keep':
285
+ combTopOut = [mk(), combArgIds[1]]
286
+ break
287
+ case 'bi':
288
+ case 'tri': {
289
+ const freshTops = Array.from(
290
+ { length: nCombOut - 1 },
291
+ () => mk()
292
+ )
293
+ combTopOut = [combArgIds[0], ...freshTops]
294
+ break
295
+ }
296
+ case 'spread':
297
+ case 'both':
298
+ combTopOut = [combArgIds[0], mk(), mk()]
299
+ break
300
+ default:
301
+ if (ctx.strict) {
302
+ throw new Error(`bind-values: неподдержанный комбинатор ${name}`)
303
+ }
304
+ resyncStack(ctx, R)
305
+ combTopOut = []
306
+ }
307
+ if (combTopOut.length !== nCombOut) {
308
+ if (ctx.strict) {
309
+ throw new Error(
310
+ `bind-values: комбинатор ${name}: внутренняя ошибка длины topOut`
311
+ )
312
+ }
313
+ resyncStack(ctx, R)
314
+ break
315
+ }
316
+ const combResultIds = combBase.concat(combTopOut)
317
+ if (combResultIds.length !== R) {
318
+ if (ctx.strict) {
319
+ throw new Error(
320
+ `bind-values: комбинатор ${name}: ожидалась длина пост-стека ${R}, получилось ${combResultIds.length}`
321
+ )
322
+ }
323
+ resyncStack(ctx, R)
324
+ break
325
+ }
326
+ const pre = node.preTypes
327
+ /** @type {object[]} */
328
+ const parts = []
329
+ /** @type {number[]} */
330
+ let quoteIdxs = []
331
+ if (name === 'dip' || name === 'keep') quoteIdxs = [2]
332
+ else if (name === 'bi') quoteIdxs = [2, 3]
333
+ else if (name === 'tri') quoteIdxs = [2, 3, 4]
334
+ else if (name === 'spread') quoteIdxs = [3, 4]
335
+ else if (name === 'both') quoteIdxs = [3]
336
+ for (const qi of quoteIdxs) {
337
+ const qid = combArgIds[qi]
338
+ const reg = ctx.quoteRegistry.get(qid)
339
+ if (reg) {
340
+ parts.push({
341
+ kind: 'inline',
342
+ innerSteps: reg.innerSteps,
343
+ innerEntryStackIds: [...reg.innerEntryStackIds]
344
+ })
345
+ continue
346
+ }
347
+ const idxTy = L - nCombIn + qi
348
+ const slotTy =
349
+ Array.isArray(pre) && idxTy >= 0 && idxTy < pre.length
350
+ ? pre[idxTy]
351
+ : null
352
+ /** @type {{ left?: unknown[], right?: unknown[] } | null} */
353
+ let inner = null
354
+ if (
355
+ slotTy != null &&
356
+ typeof slotTy === 'object' &&
357
+ /** @type {{ kind?: string }} */ (slotTy).kind !== undefined &&
358
+ (/** @type {{ kind: string }} */ (slotTy).kind === 'quote' ||
359
+ /** @type {{ kind: string }} */ (slotTy).kind === 'named_quote')
360
+ ) {
361
+ const inn = /** @type {{ inner?: object }} */ (slotTy).inner
362
+ if (inn != null && typeof inn === 'object') {
363
+ const io = /** @type {{ left?: unknown[], right?: unknown[] }} */ (
364
+ inn
365
+ )
366
+ if (Array.isArray(io.left) && Array.isArray(io.right)) {
367
+ inner = io
368
+ }
369
+ }
370
+ }
371
+ if (!inner) {
372
+ if (ctx.strict) {
373
+ throw new Error(
374
+ `bind-values: ${name} — нет quotation в реестре и нет Quote в preTypes для id ${qid}`
375
+ )
376
+ }
377
+ resyncStack(ctx, R)
378
+ break
379
+ }
380
+ parts.push({
381
+ kind: 'invoke',
382
+ innerInLen: inner.left.length,
383
+ innerOutLen: inner.right.length
384
+ })
385
+ }
386
+ if (parts.length !== quoteIdxs.length) {
387
+ break
388
+ }
389
+ /** @type {Record<string, unknown>} */
390
+ const combNode = node
391
+ combNode.combinatorInlineMeta = { parts }
392
+ attachBindings(combNode, combArgIds, combResultIds)
393
+ ctx.stack = combResultIds
394
+ break
395
+ }
396
+ const template = getBuiltinStackSignature(name)
397
+ if (!template) {
398
+ if (ctx.strict) {
399
+ throw new Error(`bind-values: неизвестный builtin ${name}`)
400
+ }
401
+ resyncStack(ctx, R)
402
+ break
403
+ }
404
+ const nIn = template.left.length
405
+ const nOut = template.right.length
406
+ const argIds = nIn === 0 ? [] : stackIds.slice(-nIn)
407
+ const base = nIn === 0 ? stackIds : stackIds.slice(0, stackIds.length - nIn)
408
+ const shuffle = BUILTIN_SHUFFLE_TOP[name]
409
+ let topOut
410
+ if (shuffle) {
411
+ topOut = shuffle(argIds)
412
+ } else {
413
+ topOut = Array.from({ length: nOut }, () => ctx.makeId(name))
414
+ }
415
+ if (topOut.length !== nOut) {
416
+ if (ctx.strict) {
417
+ throw new Error(`bind-values: внутренняя ошибка shuffle ${name}`)
418
+ }
419
+ resyncStack(ctx, R)
420
+ break
421
+ }
422
+ const resultIds = base.concat(topOut)
423
+ if (resultIds.length !== R) {
424
+ if (ctx.strict) {
425
+ throw new Error(
426
+ `bind-values: builtin ${name}: ожидалась длина пост-стека ${R}, получилось ${resultIds.length}`
427
+ )
428
+ }
429
+ resyncStack(ctx, R)
430
+ break
431
+ }
432
+ attachBindings(node, argIds, resultIds)
433
+ ctx.stack = resultIds
434
+ break
435
+ }
436
+ case 'Word': {
437
+ const sig =
438
+ node.ref === 'qualified' &&
439
+ typeof node.module === 'string' &&
440
+ typeof node.word === 'string'
441
+ ? ctx.options.resolveQualifiedWordSig?.(node.module, node.word) ??
442
+ null
443
+ : typeof node.name === 'string'
444
+ ? ctx.options.resolveLocalWordSig?.(node.name) ?? null
445
+ : null
446
+ if (!sig) {
447
+ if (ctx.strict) {
448
+ throw new Error('bind-values: нет сигнатуры для вызова Word')
449
+ }
450
+ resyncStack(ctx, R)
451
+ break
452
+ }
453
+ const nIn = sig.left.length
454
+ const nOut = sig.right.length
455
+ // slice(-0) === slice(0) даёт весь массив — для nIn===0 нужны пустые argIds
456
+ const argIds = nIn === 0 ? [] : stackIds.slice(-nIn)
457
+ const base = nIn === 0 ? stackIds : stackIds.slice(0, stackIds.length - nIn)
458
+ const topOut = Array.from({ length: nOut }, () => ctx.makeId('w'))
459
+ const resultIds = base.concat(topOut)
460
+ if (resultIds.length !== R) {
461
+ if (ctx.strict) {
462
+ throw new Error(
463
+ `bind-values: Word: несовпадение длины пост-стека (ожидалось ${R})`
464
+ )
465
+ }
466
+ resyncStack(ctx, R)
467
+ break
468
+ }
469
+ attachBindings(node, argIds, resultIds)
470
+ ctx.stack = resultIds
471
+ break
472
+ }
473
+ case 'Quotation': {
474
+ const innerSteps = /** @type {Record<string, unknown>[] | undefined} */ (
475
+ node.steps
476
+ )
477
+ /** @type {string[]} */
478
+ let innerEntryStackIds = []
479
+ if (Array.isArray(innerSteps) && innerSteps.length > 0) {
480
+ const inner0 = innerSteps[0]
481
+ const innerPreLen = Array.isArray(inner0?.preTypes)
482
+ ? inner0.preTypes.length
483
+ : 0
484
+ const innerStack = Array.from({ length: innerPreLen }, () =>
485
+ ctx.makeId('iq')
486
+ )
487
+ innerEntryStackIds = [...innerStack]
488
+ const innerCtx = {
489
+ ...ctx,
490
+ stack: innerStack
491
+ }
492
+ bindRecursive(innerSteps, innerCtx, false)
493
+ }
494
+
495
+ /** @type {Record<string, unknown>} */
496
+ const quotNode = node
497
+ quotNode.quoteEmitMeta = {
498
+ innerSteps: Array.isArray(innerSteps) ? innerSteps : [],
499
+ innerEntryStackIds
500
+ }
501
+
502
+ let argIds
503
+ let resultIds
504
+ if (R > L) {
505
+ argIds = []
506
+ const delta = R - L
507
+ resultIds = stackIds.concat(
508
+ Array.from({ length: delta }, () => ctx.makeId('q'))
509
+ )
510
+ const newQuoteIds = resultIds.slice(stackIds.length)
511
+ for (const qid of newQuoteIds) {
512
+ ctx.quoteRegistry.set(qid, {
513
+ innerSteps: Array.isArray(innerSteps) ? innerSteps : [],
514
+ innerEntryStackIds: [...innerEntryStackIds]
515
+ })
516
+ }
517
+ } else if (R < L) {
518
+ const drop = L - R
519
+ argIds = stackIds.slice(-drop)
520
+ resultIds = stackIds.slice(0, R)
521
+ } else {
522
+ argIds = []
523
+ resultIds = [...stackIds]
524
+ }
525
+ if (resultIds.length !== R) {
526
+ if (ctx.strict) {
527
+ throw new Error('bind-values: Quotation: несовпадение длин')
528
+ }
529
+ resyncStack(ctx, R)
530
+ break
531
+ }
532
+ attachBindings(node, argIds, resultIds)
533
+ ctx.stack = resultIds
534
+ break
535
+ }
536
+ case 'Slot': {
537
+ const dir = node.direction
538
+ const slotName = /** @type {string} */ (node.slotName)
539
+ if (dir === 'write') {
540
+ const argIds = stackIds.slice(-1)
541
+ const base = stackIds.slice(0, stackIds.length - 1)
542
+ const resultIds = base
543
+ if (resultIds.length !== R || argIds.length !== 1) {
544
+ if (ctx.strict) {
545
+ throw new Error('bind-values: slot_write')
546
+ }
547
+ resyncStack(ctx, R)
548
+ break
549
+ }
550
+ attachBindings(node, argIds, resultIds)
551
+ ctx.stack = resultIds
552
+ } else if (dir === 'read') {
553
+ const argIds = []
554
+ const readId = ctx.makeId(`slot_${slotName}`)
555
+ const resultIds = stackIds.concat([readId])
556
+ if (resultIds.length !== R) {
557
+ if (ctx.strict) {
558
+ throw new Error('bind-values: slot_read')
559
+ }
560
+ resyncStack(ctx, R)
561
+ break
562
+ }
563
+ attachBindings(node, argIds, resultIds)
564
+ ctx.stack = resultIds
565
+ } else if (ctx.strict) {
566
+ throw new Error('bind-values: Slot без direction')
567
+ } else {
568
+ resyncStack(ctx, R)
569
+ }
570
+ break
571
+ }
572
+ default: {
573
+ if (ctx.strict) {
574
+ throw new Error(`bind-values: неподдержанный kind ${node.kind}`)
575
+ }
576
+ resyncStack(ctx, R)
577
+ }
578
+ }
579
+ }
580
+ }
581
+
582
+ /**
583
+ * Заполняет `valueBindings` на узлах `irSteps` (и рекурсивно в `Quotation.steps`).
584
+ *
585
+ * @param {Record<string, unknown>[]} irSteps
586
+ * @param {BindValueBindingsOptions} options
587
+ * @returns {string[] | null} id входного стека слова (дно→вершина) или null
588
+ */
589
+ export function bindValueBindingsForIrSteps (irSteps, options) {
590
+ if (!Array.isArray(irSteps)) return null
591
+ const ctx = createCtx(options)
592
+ bindRecursive(irSteps, ctx, true)
593
+ return ctx.entryStackIds
594
+ }