@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,1021 @@
1
+ /**
2
+ * L3: проверка тела слова по стеку (RFC-typecheck-0.1 §5.2–5.6).
3
+ * Снимки стека по шагам — этап 9 (RFC-IR-0.1 §3, RFC-typecheck §3, §6).
4
+ *
5
+ * Модули `.js` (FFI): контракт из `@sail` в JSDoc; тело JS не проверяется (RFC-0.1 §10.0).
6
+ */
7
+ import path from 'node:path'
8
+ import * as diag from '../parse/diagnostics.js'
9
+ import { snapshotStackSlots } from './stack-step-snapshots.js'
10
+ import { getBuiltinStackSignature } from './builtin-signatures.js'
11
+ import {
12
+ applySubstDeep,
13
+ applySubstToNormSig,
14
+ derefType,
15
+ formatIrType,
16
+ freshInstanceSignature,
17
+ shareTvarsInSignature,
18
+ unifyStackWithOutput,
19
+ unifyTypes
20
+ } from './unify-type.js'
21
+ import {
22
+ calleeAsyncPropagatesToCaller,
23
+ calleeFailPropagatesToCaller,
24
+ contextAllowsAsyncCallFromCallee,
25
+ contextAllowsFailCallFromCallee,
26
+ declaredEffectFlags,
27
+ effectArraysFromFlags
28
+ } from './effect-decl.js'
29
+
30
+ /** Счётчик для свежих tvar пустого / полиморфного `List` в литералах `[ … ]`. */
31
+ let listLiteralFreshSeq = 0
32
+
33
+ /** @returns {{ kind: 'tvar', name: string, source: null, decl: null }} */
34
+ function freshListLiteralElemTvar () {
35
+ listLiteralFreshSeq++
36
+ return {
37
+ kind: 'tvar',
38
+ name: `ρL${listLiteralFreshSeq}`,
39
+ source: null,
40
+ decl: null
41
+ }
42
+ }
43
+
44
+ /** Встроенные слова, исполняющие вложенную quotation (RFC-typecheck §5.6, §5.7 п.3). */
45
+ const BUILTINS_EXECUTING_QUOTES = new Set([
46
+ 'dip',
47
+ 'keep',
48
+ 'bi',
49
+ 'tri',
50
+ 'spread',
51
+ 'both'
52
+ ])
53
+
54
+ /**
55
+ * @param {object[]} out
56
+ * @param {object} span
57
+ * @param {string} code
58
+ * @param {string} message
59
+ */
60
+ function pushDiag (out, span, code, message) {
61
+ const d = { code, message }
62
+ if (span?.start) {
63
+ d.offset = span.start.offset
64
+ d.line = span.start.line
65
+ d.column = span.start.column
66
+ }
67
+ out.push(d)
68
+ }
69
+
70
+ /**
71
+ * RFC-typecheck §3: при ошибке на шаге k сохранить `pre` стека перед шагом; `post` может отсутствовать.
72
+ * @param {{ steps: object[], nestedByParentStep: Map<number, object> } | null} snapshotRecord
73
+ * @param {number} si
74
+ * @param {object[] | null} preSnap
75
+ */
76
+ function recordStepPreOnFailure (snapshotRecord, si, preSnap) {
77
+ if (snapshotRecord == null || preSnap == null) return
78
+ snapshotRecord.steps[si] = { pre: preSnap }
79
+ }
80
+
81
+ /**
82
+ * @param {import('./build-type-env.js').TypecheckEnv} env
83
+ * @param {string} modulePath
84
+ * @param {string} wordName
85
+ * @param {{ steps: object[], nestedByParentStep: Map<number, object> }} snapshotRecord
86
+ */
87
+ function putStackSnapshotsForWord (env, modulePath, wordName, snapshotRecord) {
88
+ if (env.stackSnapshotsByPath == null) return
89
+ let perPath = env.stackSnapshotsByPath.get(modulePath)
90
+ if (!perPath) {
91
+ perPath = new Map()
92
+ env.stackSnapshotsByPath.set(modulePath, perPath)
93
+ }
94
+ perPath.set(wordName, snapshotRecord)
95
+ }
96
+
97
+ /** @returns {{ kind: 'prim', name: string, source: null, decl: null }} */
98
+ function prim (name) {
99
+ return { kind: 'prim', name, source: null, decl: null }
100
+ }
101
+
102
+ /**
103
+ * @param {object} step
104
+ * @returns {object | null}
105
+ */
106
+ function literalIrType (step) {
107
+ switch (step.litKind) {
108
+ case 'number':
109
+ return prim('Num')
110
+ case 'string':
111
+ return prim('Str')
112
+ case 'bool':
113
+ return prim('Bool')
114
+ case 'nil':
115
+ return prim('Nil')
116
+ case 'bigint':
117
+ return prim('Num')
118
+ case 'regexp':
119
+ return prim('Str')
120
+ default:
121
+ return null
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Подпись expected для E1304 (RFC-0.1 §13) при неподдерживаемом литерале.
127
+ *
128
+ * @param {object} step
129
+ * @returns {string}
130
+ */
131
+ function literalExpectedTypeLabel (step) {
132
+ switch (step.litKind) {
133
+ case 'number':
134
+ case 'bigint':
135
+ return 'Num'
136
+ case 'string':
137
+ case 'regexp':
138
+ return 'Str'
139
+ case 'bool':
140
+ return 'Bool'
141
+ case 'nil':
142
+ return 'Nil'
143
+ default:
144
+ return 'литерал'
145
+ }
146
+ }
147
+
148
+ /**
149
+ * @param {import('./build-type-env.js').TypecheckEnv} env
150
+ * @param {string} modulePath
151
+ * @param {string | null} modAlias
152
+ * @param {string} wordName
153
+ * @returns {import('./normalize-sig.js').NormalizedSignature | null}
154
+ */
155
+ export function lookupWordSignatureIr (env, modulePath, modAlias, wordName) {
156
+ if (!modAlias) {
157
+ const local = env.sigIrByPath.get(modulePath)?.get(wordName) ?? null
158
+ if (local) return local
159
+
160
+ const snap = env.snapshots?.get(modulePath)
161
+ if (!snap?.ok || !Array.isArray(snap.items)) return null
162
+ const scope = env.scopeByPath?.get(modulePath)
163
+ if (!scope?.importMap) return null
164
+
165
+ let foundPath = null
166
+ for (const item of snap.items) {
167
+ if (item.kind !== 'import') continue
168
+ const words = item.bracket?.words
169
+ if (!Array.isArray(words) || !words.includes(wordName)) continue
170
+ const dep = scope.importMap.get(item.module)
171
+ const p = dep?.path
172
+ if (typeof p !== 'string') continue
173
+ if (foundPath != null && foundPath !== p) return null
174
+ foundPath = p
175
+ }
176
+ if (foundPath == null) return null
177
+ return env.sigIrByPath.get(foundPath)?.get(wordName) ?? null
178
+ }
179
+ const dep = env.scopeByPath.get(modulePath)?.importMap.get(modAlias)
180
+ const p = dep?.path
181
+ if (!p) return null
182
+ return env.sigIrByPath.get(p)?.get(wordName) ?? null
183
+ }
184
+
185
+ /**
186
+ * @param {object[]} stack
187
+ * @param {import('./normalize-sig.js').NormalizedSignature} calleeFresh
188
+ * @param {WeakMap<object, object>} subst
189
+ * @param {object[]} diags
190
+ * @param {object | null} span
191
+ * @returns {boolean}
192
+ */
193
+ function applyCalleeStackEffect (stack, calleeFresh, subst, diags, span) {
194
+ const k = calleeFresh.left.length
195
+ if (stack.length < k) {
196
+ pushDiag(
197
+ diags,
198
+ span,
199
+ 'E1303',
200
+ diag.e1303StackEffect(
201
+ `минимум ${k} слотов на входе`,
202
+ `${stack.length} слотов`
203
+ )
204
+ )
205
+ return false
206
+ }
207
+ const base = stack.length - k
208
+ for (let i = 0; i < k; i++) {
209
+ if (
210
+ !unifyTypes(
211
+ stack[base + i],
212
+ calleeFresh.left[i],
213
+ subst,
214
+ diags,
215
+ span
216
+ )
217
+ ) {
218
+ return false
219
+ }
220
+ }
221
+ const pref = stack.slice(0, base)
222
+ const outSlots = calleeFresh.right.map((t) => applySubstDeep(t, subst))
223
+ stack.length = 0
224
+ stack.push(...pref, ...outSlots)
225
+ return true
226
+ }
227
+
228
+ /**
229
+ * ADT eliminator: E1308 при недостатке слотов, E1307 при сбое унификации веток (RFC-0.1 §13).
230
+ *
231
+ * @param {string} calleeDisplayName
232
+ */
233
+ function applyCalleeStackEffectForWordRef (
234
+ stack,
235
+ calleeFresh,
236
+ subst,
237
+ diags,
238
+ span,
239
+ calleeDisplayName
240
+ ) {
241
+ const k = calleeFresh.left.length
242
+ if (calleeFresh.adtEliminator && stack.length < k) {
243
+ pushDiag(
244
+ diags,
245
+ span,
246
+ 'E1308',
247
+ diag.e1308EliminatorQuotes(calleeDisplayName)
248
+ )
249
+ return false
250
+ }
251
+ const ok = applyCalleeStackEffect(stack, calleeFresh, subst, diags, span)
252
+ if (!ok && calleeFresh.adtEliminator) {
253
+ pushDiag(diags, span, 'E1307', diag.e1307EliminatorBranches())
254
+ }
255
+ return ok
256
+ }
257
+
258
+ /**
259
+ * @param {object[]} stack
260
+ * @param {import('./normalize-sig.js').NormalizedSignature} calleeFresh
261
+ * @param {WeakMap<object, object>} subst
262
+ * @returns {import('./normalize-sig.js').NormalizedSignature[]}
263
+ */
264
+ function collectQuoteInnersFromStackInputs (stack, calleeFresh, subst) {
265
+ const k = calleeFresh.left.length
266
+ const base = stack.length - k
267
+ /** @type {import('./normalize-sig.js').NormalizedSignature[]} */
268
+ const out = []
269
+ for (let i = 0; i < k; i++) {
270
+ const req = calleeFresh.left[i]
271
+ if (req.kind !== 'quote' && req.kind !== 'named_quote') continue
272
+ const concrete = derefType(stack[base + i], subst)
273
+ if (
274
+ concrete &&
275
+ (concrete.kind === 'quote' || concrete.kind === 'named_quote')
276
+ ) {
277
+ out.push(applySubstToNormSig(concrete.inner, subst))
278
+ }
279
+ }
280
+ return out
281
+ }
282
+
283
+ /**
284
+ * @param {import('./effect-decl.js').EffectFlags} containerEff
285
+ * @param {import('./normalize-sig.js').NormalizedSignature} executedSig
286
+ * @param {boolean} quotationInferOpen
287
+ */
288
+ function checkExecutedNormSigEffects (
289
+ containerEff,
290
+ executedSig,
291
+ diags,
292
+ span,
293
+ callerWordName,
294
+ calleeLabel,
295
+ quotationInferOpen
296
+ ) {
297
+ const f = declaredEffectFlags(executedSig)
298
+ if (calleeAsyncPropagatesToCaller(f)) {
299
+ if (!contextAllowsAsyncCallFromCallee(containerEff)) {
300
+ if (quotationInferOpen) {
301
+ containerEff.plusAsync = true
302
+ } else {
303
+ pushDiag(
304
+ diags,
305
+ span,
306
+ 'E1310',
307
+ diag.e1310AsyncCall(calleeLabel, callerWordName)
308
+ )
309
+ return false
310
+ }
311
+ }
312
+ }
313
+ if (calleeFailPropagatesToCaller(f)) {
314
+ if (!contextAllowsFailCallFromCallee(containerEff)) {
315
+ if (quotationInferOpen) {
316
+ containerEff.plusFail = true
317
+ } else {
318
+ pushDiag(
319
+ diags,
320
+ span,
321
+ 'E1312',
322
+ diag.e1312FailCall(calleeLabel, callerWordName)
323
+ )
324
+ return false
325
+ }
326
+ }
327
+ }
328
+ return true
329
+ }
330
+
331
+ /**
332
+ * @param {import('./effect-decl.js').EffectFlags} containerEff
333
+ */
334
+ function ensureCalleeWordEffects (
335
+ containerEff,
336
+ calleeIr,
337
+ quotationInferOpen,
338
+ diags,
339
+ span,
340
+ callerWordName,
341
+ calleeLabel
342
+ ) {
343
+ return checkExecutedNormSigEffects(
344
+ containerEff,
345
+ calleeIr,
346
+ diags,
347
+ span,
348
+ callerWordName,
349
+ calleeLabel,
350
+ quotationInferOpen
351
+ )
352
+ }
353
+
354
+ /**
355
+ * L3 маркер → IR §6: `calleeAsync` не ставить при поглощении исходящим `-Async` (RFC-compile §10.5, вариант B).
356
+ *
357
+ * @param {import('./effect-decl.js').EffectFlags} calleeFlags
358
+ * @param {import('./effect-decl.js').EffectFlags} containerEff
359
+ */
360
+ function markCalleeAsyncForIr (calleeFlags, containerEff) {
361
+ return calleeAsyncPropagatesToCaller(calleeFlags) && !containerEff.minusAsync
362
+ }
363
+
364
+ /**
365
+ * @param {import('./effect-decl.js').EffectFlags} calleeFlags
366
+ * @param {import('./effect-decl.js').EffectFlags} containerEff
367
+ */
368
+ function markCalleeMayFailForIr (calleeFlags, containerEff) {
369
+ return calleeFailPropagatesToCaller(calleeFlags) && !containerEff.minusFail
370
+ }
371
+
372
+ /**
373
+ * RFC-builtins §4.3: ( ~rest I… Quote(I->O) -> ~rest O… ), длина I задаётся вложенной сигнатурой.
374
+ *
375
+ * @param {object[]} stack
376
+ * @param {WeakMap<object, object>} subst
377
+ * @param {object[]} diags
378
+ * @param {object | null} span
379
+ * @param {import('./effect-decl.js').EffectFlags} containerEff
380
+ * @param {string} callerWordName
381
+ * @param {string} modulePath
382
+ * @param {object[] | null} callSiteMarks
383
+ * @param {boolean} quotationInferOpen
384
+ * @returns {boolean}
385
+ */
386
+ function applyCallStack (
387
+ stack,
388
+ subst,
389
+ diags,
390
+ span,
391
+ containerEff,
392
+ callerWordName,
393
+ modulePath,
394
+ callSiteMarks,
395
+ quotationInferOpen
396
+ ) {
397
+ if (stack.length < 1) {
398
+ pushDiag(
399
+ diags,
400
+ span,
401
+ 'E1303',
402
+ diag.e1303StackEffect('call: quotation на вершине', 'пустой стек')
403
+ )
404
+ return false
405
+ }
406
+ const qi = stack.length - 1
407
+ const top = derefType(stack[qi], subst)
408
+ if (
409
+ !top ||
410
+ (top.kind !== 'quote' && top.kind !== 'named_quote')
411
+ ) {
412
+ pushDiag(
413
+ diags,
414
+ span,
415
+ 'E1306',
416
+ diag.e1306CallNeedsQuote(top ? formatIrType(top) : 'пусто')
417
+ )
418
+ return false
419
+ }
420
+ const innerResolved = applySubstToNormSig(top.inner, subst)
421
+ if (
422
+ !checkExecutedNormSigEffects(
423
+ containerEff,
424
+ innerResolved,
425
+ diags,
426
+ span,
427
+ callerWordName,
428
+ 'call',
429
+ quotationInferOpen
430
+ )
431
+ ) {
432
+ return false
433
+ }
434
+ if (callSiteMarks) {
435
+ const cf = declaredEffectFlags(innerResolved)
436
+ callSiteMarks.push({
437
+ modulePath,
438
+ kind: 'call',
439
+ span,
440
+ calleeAsync: markCalleeAsyncForIr(cf, containerEff),
441
+ calleeMayFail: markCalleeMayFailForIr(cf, containerEff)
442
+ })
443
+ }
444
+ stack.splice(qi, 1)
445
+ const inner = top.inner
446
+ const n = inner.left.length
447
+ if (stack.length < n) {
448
+ pushDiag(
449
+ diags,
450
+ span,
451
+ 'E1303',
452
+ diag.e1303StackEffect(
453
+ `call: минимум ${n} слотов под вход quotation`,
454
+ `${stack.length} слотов`
455
+ )
456
+ )
457
+ return false
458
+ }
459
+ const base = stack.length - n
460
+ for (let i = 0; i < n; i++) {
461
+ if (
462
+ !unifyTypes(
463
+ stack[base + i],
464
+ inner.left[i],
465
+ subst,
466
+ diags,
467
+ span
468
+ )
469
+ ) {
470
+ return false
471
+ }
472
+ }
473
+ const pref = stack.slice(0, base)
474
+ const outSlots = inner.right.map((t) => applySubstDeep(t, subst))
475
+ stack.length = 0
476
+ stack.push(...pref, ...outSlots)
477
+ return true
478
+ }
479
+
480
+ /**
481
+ * @param {object | null} quoteExpectation — IR `quote` / `named_quote` из объявленного выхода слова (RFC-typecheck §5.6 п.4).
482
+ * @param {import('./effect-decl.js').EffectFlags} containerEff — контекст всплытия (слово или вложенная quotation_sig).
483
+ * @param {boolean} quotationInferOpen — тело открытой цитаты без ожидаемой inner-сигнатуры (инфер +Async/+Fail).
484
+ * @param {object[] | null} callSiteMarks — накопление для RFC-IR §6 / RFC-typecheck §3.
485
+ * @param {{ steps: object[], nestedByParentStep: Map<number, object> } | null} snapshotRecord этап 9: цепочка pre/post (`post` опционален на шаге с ошибкой; RFC-typecheck §3); null — не писать снимки.
486
+ */
487
+ function walkWordSteps (
488
+ env,
489
+ modulePath,
490
+ body,
491
+ stack,
492
+ slots,
493
+ subst,
494
+ diags,
495
+ defaultSpan,
496
+ quoteExpectation,
497
+ containerEff,
498
+ quotationInferOpen,
499
+ callerWordName,
500
+ callSiteMarks,
501
+ snapshotRecord
502
+ ) {
503
+ for (let si = 0; si < body.length; si++) {
504
+ const step = body[si]
505
+ const sp = step.span ?? defaultSpan
506
+ const preSnap =
507
+ snapshotRecord != null ? snapshotStackSlots(stack, subst) : null
508
+ const failWithPre = () => {
509
+ recordStepPreOnFailure(snapshotRecord, si, preSnap)
510
+ return false
511
+ }
512
+ switch (step.kind) {
513
+ case 'literal': {
514
+ const ty = literalIrType(step)
515
+ if (!ty) {
516
+ pushDiag(
517
+ diags,
518
+ sp,
519
+ 'E1304',
520
+ diag.e1304TypeMismatch(literalExpectedTypeLabel(step), step.litKind)
521
+ )
522
+ return failWithPre()
523
+ }
524
+ stack.push(ty)
525
+ break
526
+ }
527
+ case 'builtin': {
528
+ const nm = step.name
529
+ if (nm === 'call') {
530
+ if (
531
+ !applyCallStack(
532
+ stack,
533
+ subst,
534
+ diags,
535
+ sp,
536
+ containerEff,
537
+ callerWordName,
538
+ modulePath,
539
+ callSiteMarks,
540
+ quotationInferOpen
541
+ )
542
+ ) {
543
+ return failWithPre()
544
+ }
545
+ break
546
+ }
547
+ const template = getBuiltinStackSignature(nm)
548
+ if (template) {
549
+ const inst = freshInstanceSignature(shareTvarsInSignature(template))
550
+ if (BUILTINS_EXECUTING_QUOTES.has(nm)) {
551
+ const inners = collectQuoteInnersFromStackInputs(
552
+ stack,
553
+ inst,
554
+ subst
555
+ )
556
+ for (const innerSig of inners) {
557
+ if (
558
+ !checkExecutedNormSigEffects(
559
+ containerEff,
560
+ innerSig,
561
+ diags,
562
+ sp,
563
+ callerWordName,
564
+ nm,
565
+ quotationInferOpen
566
+ )
567
+ ) {
568
+ return failWithPre()
569
+ }
570
+ }
571
+ if (callSiteMarks) {
572
+ for (const innerSig of inners) {
573
+ const cf = declaredEffectFlags(innerSig)
574
+ callSiteMarks.push({
575
+ modulePath,
576
+ kind: 'builtin_quote',
577
+ builtin: nm,
578
+ span: sp,
579
+ calleeAsync: markCalleeAsyncForIr(cf, containerEff),
580
+ calleeMayFail: markCalleeMayFailForIr(cf, containerEff)
581
+ })
582
+ }
583
+ }
584
+ }
585
+ if (!applyCalleeStackEffect(stack, inst, subst, diags, sp)) {
586
+ return failWithPre()
587
+ }
588
+ break
589
+ }
590
+ pushDiag(diags, sp, 'E1206', diag.e1206UnknownBuiltin(nm))
591
+ return failWithPre()
592
+ }
593
+ case 'word_ref': {
594
+ const calleeIr = lookupWordSignatureIr(env, modulePath, null, step.name)
595
+ if (!calleeIr) {
596
+ pushDiag(diags, sp, 'E1304', diag.e1304UnknownTypeInSignature(step.name))
597
+ return failWithPre()
598
+ }
599
+ if (
600
+ !ensureCalleeWordEffects(
601
+ containerEff,
602
+ calleeIr,
603
+ quotationInferOpen,
604
+ diags,
605
+ sp,
606
+ callerWordName,
607
+ step.name
608
+ )
609
+ ) {
610
+ return failWithPre()
611
+ }
612
+ if (callSiteMarks) {
613
+ const cf = declaredEffectFlags(calleeIr)
614
+ callSiteMarks.push({
615
+ modulePath,
616
+ kind: 'word_ref',
617
+ name: step.name,
618
+ span: sp,
619
+ calleeAsync: markCalleeAsyncForIr(cf, containerEff),
620
+ calleeMayFail: markCalleeMayFailForIr(cf, containerEff)
621
+ })
622
+ }
623
+ const inst = freshInstanceSignature(shareTvarsInSignature(calleeIr))
624
+ if (
625
+ !applyCalleeStackEffectForWordRef(
626
+ stack,
627
+ inst,
628
+ subst,
629
+ diags,
630
+ sp,
631
+ step.name
632
+ )
633
+ ) {
634
+ return failWithPre()
635
+ }
636
+ break
637
+ }
638
+ case 'module_word_ref': {
639
+ const calleeIr = lookupWordSignatureIr(
640
+ env,
641
+ modulePath,
642
+ step.module,
643
+ step.word
644
+ )
645
+ if (!calleeIr) {
646
+ pushDiag(
647
+ diags,
648
+ sp,
649
+ 'E1204',
650
+ diag.e1204MissingMember(step.module, step.word)
651
+ )
652
+ return failWithPre()
653
+ }
654
+ const qual = `~${step.module}/${step.word}`
655
+ if (
656
+ !ensureCalleeWordEffects(
657
+ containerEff,
658
+ calleeIr,
659
+ quotationInferOpen,
660
+ diags,
661
+ sp,
662
+ callerWordName,
663
+ qual
664
+ )
665
+ ) {
666
+ return failWithPre()
667
+ }
668
+ if (callSiteMarks) {
669
+ const cf = declaredEffectFlags(calleeIr)
670
+ callSiteMarks.push({
671
+ modulePath,
672
+ kind: 'module_word_ref',
673
+ module: step.module,
674
+ word: step.word,
675
+ span: sp,
676
+ calleeAsync: markCalleeAsyncForIr(cf, containerEff),
677
+ calleeMayFail: markCalleeMayFailForIr(cf, containerEff)
678
+ })
679
+ }
680
+ const inst = freshInstanceSignature(shareTvarsInSignature(calleeIr))
681
+ if (
682
+ !applyCalleeStackEffectForWordRef(
683
+ stack,
684
+ inst,
685
+ subst,
686
+ diags,
687
+ sp,
688
+ qual
689
+ )
690
+ ) {
691
+ return failWithPre()
692
+ }
693
+ break
694
+ }
695
+ case 'quotation': {
696
+ const bodySteps = step.body ?? []
697
+ const exp = quoteExpectation
698
+ const expInner =
699
+ exp && (exp.kind === 'quote' || exp.kind === 'named_quote')
700
+ ? exp.inner
701
+ : null
702
+
703
+ if (bodySteps.length === 0 && !expInner) {
704
+ stack.push({
705
+ kind: 'quote',
706
+ source: step,
707
+ inner: {
708
+ left: [],
709
+ right: [],
710
+ effectsAdd: [],
711
+ effectsRemove: []
712
+ }
713
+ })
714
+ break
715
+ }
716
+
717
+ let innerDecl
718
+ let inferOpenQuote = false
719
+ if (expInner) {
720
+ innerDecl = freshInstanceSignature(
721
+ shareTvarsInSignature(expInner)
722
+ )
723
+ } else {
724
+ inferOpenQuote = true
725
+ const leftSeed =
726
+ bodySteps.length > 0
727
+ ? stack.map((slot) => slot)
728
+ : []
729
+ innerDecl = freshInstanceSignature(
730
+ shareTvarsInSignature({
731
+ left: leftSeed,
732
+ right: [],
733
+ effectsAdd: [],
734
+ effectsRemove: []
735
+ })
736
+ )
737
+ }
738
+ /** @type {import('./effect-decl.js').EffectFlags} */
739
+ const innerContainerEff = inferOpenQuote
740
+ ? {
741
+ plusAsync: false,
742
+ minusAsync: containerEff.minusAsync,
743
+ plusFail: false,
744
+ minusFail: containerEff.minusFail
745
+ }
746
+ : { ...declaredEffectFlags(innerDecl) }
747
+ const innerStack = innerDecl.left.slice()
748
+ const innerSlots = new Map()
749
+ const innerSnapshotRecord =
750
+ snapshotRecord != null
751
+ ? { steps: [], nestedByParentStep: new Map() }
752
+ : null
753
+ if (
754
+ !walkWordSteps(
755
+ env,
756
+ modulePath,
757
+ bodySteps,
758
+ innerStack,
759
+ innerSlots,
760
+ subst,
761
+ diags,
762
+ sp,
763
+ null,
764
+ innerContainerEff,
765
+ inferOpenQuote,
766
+ callerWordName,
767
+ callSiteMarks,
768
+ innerSnapshotRecord
769
+ )
770
+ ) {
771
+ if (snapshotRecord != null && innerSnapshotRecord != null) {
772
+ snapshotRecord.nestedByParentStep.set(si, innerSnapshotRecord)
773
+ }
774
+ return failWithPre()
775
+ }
776
+ if (snapshotRecord != null && innerSnapshotRecord != null) {
777
+ snapshotRecord.nestedByParentStep.set(si, innerSnapshotRecord)
778
+ }
779
+ if (!inferOpenQuote) {
780
+ const rightExpected = innerDecl.right.map((t) =>
781
+ applySubstDeep(t, subst)
782
+ )
783
+ const stackD = innerStack.map((t) => applySubstDeep(t, subst))
784
+ if (
785
+ !unifyStackWithOutput(stackD, rightExpected, subst, diags, sp)
786
+ ) {
787
+ return failWithPre()
788
+ }
789
+ }
790
+ const leftIr = innerDecl.left.map((t) => applySubstDeep(t, subst))
791
+ const rightIr = innerStack.map((t) => applySubstDeep(t, subst))
792
+ const eff = effectArraysFromFlags(innerContainerEff, sp)
793
+ const innerNode = {
794
+ left: leftIr,
795
+ right: rightIr,
796
+ effectsAdd: eff.effectsAdd,
797
+ effectsRemove: eff.effectsRemove
798
+ }
799
+ if (exp && exp.kind === 'named_quote') {
800
+ stack.push({
801
+ kind: 'named_quote',
802
+ prefix: exp.prefix,
803
+ source: step,
804
+ inner: innerNode
805
+ })
806
+ } else {
807
+ stack.push({
808
+ kind: 'quote',
809
+ source: step,
810
+ inner: innerNode
811
+ })
812
+ }
813
+ break
814
+ }
815
+ case 'list_literal': {
816
+ const elements = step.elements ?? []
817
+ /** @type {object | null} */
818
+ let elemTy = null
819
+ if (elements.length === 0) {
820
+ elemTy = freshListLiteralElemTvar()
821
+ } else {
822
+ for (const el of elements) {
823
+ if (el.kind !== 'literal') {
824
+ pushDiag(
825
+ diags,
826
+ sp,
827
+ 'E1304',
828
+ diag.e1304TypeMismatch('list_literal', 'только литералы')
829
+ )
830
+ return failWithPre()
831
+ }
832
+ const ty = literalIrType(el)
833
+ if (!ty) {
834
+ pushDiag(
835
+ diags,
836
+ sp,
837
+ 'E1304',
838
+ diag.e1304TypeMismatch(literalExpectedTypeLabel(el), el.litKind)
839
+ )
840
+ return failWithPre()
841
+ }
842
+ if (elemTy == null) {
843
+ elemTy = ty
844
+ } else if (!unifyTypes(elemTy, ty, subst, diags, sp)) {
845
+ return failWithPre()
846
+ }
847
+ }
848
+ }
849
+ stack.push({
850
+ kind: 'app',
851
+ ctor: 'List',
852
+ args: [applySubstDeep(/** @type {object} */ (elemTy), subst)],
853
+ source: null,
854
+ decl: null
855
+ })
856
+ break
857
+ }
858
+ case 'slot_write': {
859
+ if (stack.length < 1) {
860
+ pushDiag(
861
+ diags,
862
+ sp,
863
+ 'E1303',
864
+ diag.e1303StackEffect('1 слот для :slot', 'пустой стек')
865
+ )
866
+ return failWithPre()
867
+ }
868
+ if (slots.has(step.name)) {
869
+ pushDiag(diags, sp, 'E1313', diag.e1313DuplicateSlotWrite(step.name))
870
+ return failWithPre()
871
+ }
872
+ const top = stack.pop()
873
+ slots.set(step.name, derefType(top, subst))
874
+ break
875
+ }
876
+ case 'slot_read': {
877
+ if (!slots.has(step.name)) {
878
+ pushDiag(diags, sp, 'E1314', diag.e1314SlotReadBeforeWrite(step.name))
879
+ return failWithPre()
880
+ }
881
+ const t = slots.get(step.name)
882
+ stack.push(applySubstDeep(t, subst))
883
+ break
884
+ }
885
+ default:
886
+ pushDiag(
887
+ diags,
888
+ sp,
889
+ 'E1304',
890
+ diag.e1304TypeMismatch('известный word_step', step.kind)
891
+ )
892
+ return failWithPre()
893
+ }
894
+ for (let i = 0; i < stack.length; i++) {
895
+ stack[i] = applySubstDeep(stack[i], subst)
896
+ }
897
+ if (snapshotRecord != null && preSnap != null) {
898
+ snapshotRecord.steps[si] = {
899
+ pre: preSnap,
900
+ post: snapshotStackSlots(stack, subst)
901
+ }
902
+ }
903
+ }
904
+ return true
905
+ }
906
+
907
+ /**
908
+ * @param {import('./build-type-env.js').TypecheckEnv} env
909
+ * @param {string} modulePath
910
+ * @param {object} word
911
+ * @param {object[]} diags
912
+ * @returns {boolean}
913
+ */
914
+ export function checkWordBody (env, modulePath, word, diags) {
915
+ const rawSig = env.sigIrByPath?.get(modulePath)?.get(word.name)
916
+ if (!rawSig) {
917
+ pushDiag(
918
+ diags,
919
+ word.span,
920
+ 'E1304',
921
+ diag.e1304UnknownTypeInSignature(word.name)
922
+ )
923
+ return false
924
+ }
925
+
926
+ const declSig = freshInstanceSignature(shareTvarsInSignature(rawSig))
927
+ const subst = new WeakMap()
928
+ /** @type {object[]} */
929
+ const stack = declSig.left.slice()
930
+ /** @type {Map<string, object>} */
931
+ const slots = new Map()
932
+
933
+ const defaultSpan = word.signature?.span ?? word.span
934
+ const rq =
935
+ word.body.length === 1 && word.body[0].kind === 'quotation'
936
+ ? declSig.right.length === 1
937
+ ? declSig.right[0]
938
+ : null
939
+ : null
940
+ const quoteExpect =
941
+ rq && (rq.kind === 'quote' || rq.kind === 'named_quote') ? rq : null
942
+
943
+ const containerEff = { ...declaredEffectFlags(declSig) }
944
+ /** @type {object[]} */
945
+ const callSiteMarks = []
946
+
947
+ /** @type {{ steps: object[], nestedByParentStep: Map<number, object> }} */
948
+ const snapshotRecord = { steps: [], nestedByParentStep: new Map() }
949
+
950
+ if (
951
+ !walkWordSteps(
952
+ env,
953
+ modulePath,
954
+ word.body,
955
+ stack,
956
+ slots,
957
+ subst,
958
+ diags,
959
+ defaultSpan,
960
+ quoteExpect,
961
+ containerEff,
962
+ false,
963
+ word.name,
964
+ callSiteMarks,
965
+ env.stackSnapshotsByPath != null ? snapshotRecord : null
966
+ )
967
+ ) {
968
+ putStackSnapshotsForWord(env, modulePath, word.name, snapshotRecord)
969
+ return false
970
+ }
971
+
972
+ for (const m of callSiteMarks) {
973
+ env.callSiteEffectMarks.push({ ...m, callerWord: word.name })
974
+ }
975
+
976
+ const outExpected = declSig.right.map((t) => applySubstDeep(t, subst))
977
+ const stackD = stack.map((t) => applySubstDeep(t, subst))
978
+ if (!unifyStackWithOutput(stackD, outExpected, subst, diags, defaultSpan)) {
979
+ putStackSnapshotsForWord(env, modulePath, word.name, snapshotRecord)
980
+ return false
981
+ }
982
+ putStackSnapshotsForWord(env, modulePath, word.name, snapshotRecord)
983
+ return true
984
+ }
985
+
986
+ /**
987
+ * @param {import('./build-type-env.js').TypecheckEnv} env
988
+ * @returns {object[]}
989
+ */
990
+ export function checkAllWordBodies (env) {
991
+ /** @type {object[]} */
992
+ const diags = []
993
+
994
+ /** @type {Map<string, Map<string, { asyncDefinition: boolean, mayFail: boolean }>>} */
995
+ const defFx = new Map()
996
+ for (const p of env.modulePathsOrdered) {
997
+ const sigs = env.sigIrByPath?.get(p)
998
+ /** @type {Map<string, { asyncDefinition: boolean, mayFail: boolean }>} */
999
+ const m = new Map()
1000
+ if (sigs) {
1001
+ for (const [name, sig] of sigs) {
1002
+ const f = declaredEffectFlags(sig)
1003
+ m.set(name, { asyncDefinition: f.plusAsync, mayFail: f.plusFail })
1004
+ }
1005
+ }
1006
+ defFx.set(p, m)
1007
+ }
1008
+ env.definitionEffectsByPath = defFx
1009
+ env.callSiteEffectMarks = []
1010
+ env.stackSnapshotsByPath = new Map()
1011
+
1012
+ for (const p of env.modulePathsOrdered) {
1013
+ const words = env.wordDeclByPath.get(p)
1014
+ if (!words) continue
1015
+ if (path.extname(p).toLowerCase() === '.js') continue
1016
+ for (const word of words.values()) {
1017
+ checkWordBody(env, p, word, diags)
1018
+ }
1019
+ }
1020
+ return diags
1021
+ }