@algosail/lang 0.2.12 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/bin/sail.mjs +4 -0
  2. package/cli/run-cli.js +176 -0
  3. package/index.js +11 -2
  4. package/lib/codegen/README.md +230 -0
  5. package/lib/codegen/codegen-diagnostics.js +164 -0
  6. package/lib/codegen/compile-graph.js +107 -0
  7. package/lib/codegen/emit-adt.js +177 -0
  8. package/lib/codegen/emit-body.js +1265 -0
  9. package/lib/codegen/emit-builtin.js +371 -0
  10. package/lib/codegen/emit-jsdoc-sail.js +383 -0
  11. package/lib/codegen/emit-module.js +498 -0
  12. package/lib/codegen/esm-imports.js +26 -0
  13. package/lib/codegen/index.js +69 -0
  14. package/lib/codegen/out-layout.js +102 -0
  15. package/lib/ffi/extract-jsdoc-sail.js +34 -0
  16. package/lib/io-node/index.js +4 -0
  17. package/lib/io-node/package-root.js +18 -0
  18. package/lib/io-node/read-file.js +12 -0
  19. package/lib/io-node/resolve-package.js +24 -0
  20. package/lib/io-node/resolve-sail-names-from-disk.js +21 -0
  21. package/lib/ir/assert-json-serializable.js +30 -0
  22. package/lib/ir/attach-call-effects.js +108 -0
  23. package/lib/ir/bind-values.js +594 -0
  24. package/lib/ir/build-module-ir.js +290 -0
  25. package/lib/ir/index.js +31 -0
  26. package/lib/ir/lower-body-steps.js +170 -0
  27. package/lib/ir/module-metadata.js +65 -0
  28. package/lib/ir/schema-version.js +15 -0
  29. package/lib/ir/serialize.js +202 -0
  30. package/lib/ir/stitch-types.js +92 -0
  31. package/lib/names/adt-autogen.js +22 -0
  32. package/lib/names/import-path.js +28 -0
  33. package/lib/names/index.js +1 -0
  34. package/lib/names/local-declarations.js +127 -0
  35. package/lib/names/lower-first.js +6 -0
  36. package/lib/names/module-scope.js +120 -0
  37. package/lib/names/resolve-sail.js +365 -0
  38. package/lib/names/walk-ast-refs.js +91 -0
  39. package/lib/parse/ast-build.js +51 -0
  40. package/lib/parse/ast-spec.js +212 -0
  41. package/lib/parse/builtins-set.js +12 -0
  42. package/lib/parse/diagnostics.js +180 -0
  43. package/lib/parse/index.js +46 -0
  44. package/lib/parse/lexer.js +390 -0
  45. package/lib/parse/parse-source.js +912 -0
  46. package/lib/typecheck/adt-autogen-sigs.js +345 -0
  47. package/lib/typecheck/build-type-env.js +148 -0
  48. package/lib/typecheck/builtin-signatures.js +183 -0
  49. package/lib/typecheck/check-word-body.js +1021 -0
  50. package/lib/typecheck/effect-decl.js +124 -0
  51. package/lib/typecheck/index.js +55 -0
  52. package/lib/typecheck/normalize-sig.js +369 -0
  53. package/lib/typecheck/stack-step-snapshots.js +56 -0
  54. package/lib/typecheck/unify-type.js +665 -0
  55. package/lib/typecheck/validate-adt.js +201 -0
  56. package/package.json +4 -9
  57. package/scripts/regen-demo-full-syntax-ast.mjs +22 -0
  58. package/test/cli/sail-cli.test.js +64 -0
  59. package/test/codegen/compile-bracket-ffi-e2e.test.js +64 -0
  60. package/test/codegen/compile-stage0.test.js +128 -0
  61. package/test/codegen/compile-stage4-layout.test.js +124 -0
  62. package/test/codegen/e2e-prelude-ffi-adt/app/extra.sail +6 -0
  63. package/test/codegen/e2e-prelude-ffi-adt/app/lib.sail +33 -0
  64. package/test/codegen/e2e-prelude-ffi-adt/app/main.sail +28 -0
  65. package/test/codegen/e2e-prelude-ffi-adt/artifacts/.gitignore +2 -0
  66. package/test/codegen/e2e-prelude-ffi-adt/ffi/helpers.js +27 -0
  67. package/test/codegen/e2e-prelude-ffi-adt.test.js +100 -0
  68. package/test/codegen/emit-adt-stage6.test.js +168 -0
  69. package/test/codegen/emit-async-stage5.test.js +164 -0
  70. package/test/codegen/emit-body-stage2.test.js +139 -0
  71. package/test/codegen/emit-body.test.js +163 -0
  72. package/test/codegen/emit-builtins-stage7.test.js +258 -0
  73. package/test/codegen/emit-diagnostics-stage9.test.js +90 -0
  74. package/test/codegen/emit-jsdoc-stage8.test.js +113 -0
  75. package/test/codegen/emit-module-stage3.test.js +78 -0
  76. package/test/conformance/conformance-ir-l4.test.js +38 -0
  77. package/test/conformance/conformance-l5-codegen.test.js +111 -0
  78. package/test/conformance/conformance-runner.js +91 -0
  79. package/test/conformance/conformance-suite-l3.test.js +32 -0
  80. package/test/ffi/prelude-jsdoc.test.js +49 -0
  81. package/test/fixtures/demo-full-syntax.ast.json +1471 -0
  82. package/test/fixtures/demo-full-syntax.sail +35 -0
  83. package/test/fixtures/io-node-ffi-adt/ffi.js +7 -0
  84. package/test/fixtures/io-node-ffi-adt/use.sail +4 -0
  85. package/test/fixtures/io-node-mini/dep.sail +2 -0
  86. package/test/fixtures/io-node-mini/entry.sail +4 -0
  87. package/test/fixtures/io-node-prelude/entry.sail +4 -0
  88. package/test/fixtures/io-node-reexport-chain/a.sail +4 -0
  89. package/test/fixtures/io-node-reexport-chain/b.sail +2 -0
  90. package/test/fixtures/io-node-reexport-chain/c.sail +2 -0
  91. package/test/io-node/resolve-disk.test.js +59 -0
  92. package/test/ir/bind-values.test.js +84 -0
  93. package/test/ir/build-module-ir.test.js +100 -0
  94. package/test/ir/call-effects.test.js +97 -0
  95. package/test/ir/ffi-bracket-ir.test.js +59 -0
  96. package/test/ir/full-ir-document.test.js +51 -0
  97. package/test/ir/ir-document-assert.js +67 -0
  98. package/test/ir/lower-body-steps.test.js +90 -0
  99. package/test/ir/module-metadata.test.js +42 -0
  100. package/test/ir/serialization-model.test.js +172 -0
  101. package/test/ir/stitch-types.test.js +74 -0
  102. package/test/names/l2-resolve-adt-autogen.test.js +155 -0
  103. package/test/names/l2-resolve-bracket-ffi.test.js +108 -0
  104. package/test/names/l2-resolve-declaration-and-bracket-errors.test.js +276 -0
  105. package/test/names/l2-resolve-graph.test.js +105 -0
  106. package/test/names/l2-resolve-single-file.test.js +79 -0
  107. package/test/parse/ast-spec.test.js +56 -0
  108. package/test/parse/ast.test.js +476 -0
  109. package/test/parse/contract.test.js +37 -0
  110. package/test/parse/fixtures-full-syntax.test.js +24 -0
  111. package/test/parse/helpers.js +27 -0
  112. package/test/parse/l0-lex-diagnostics-matrix.test.js +59 -0
  113. package/test/parse/l0-lex.test.js +40 -0
  114. package/test/parse/l1-diagnostics.test.js +77 -0
  115. package/test/parse/l1-import.test.js +28 -0
  116. package/test/parse/l1-parse-diagnostics-matrix.test.js +32 -0
  117. package/test/parse/l1-top-level.test.js +47 -0
  118. package/test/parse/l1-types.test.js +31 -0
  119. package/test/parse/l1-words.test.js +49 -0
  120. package/test/parse/l2-diagnostics-contract.test.js +67 -0
  121. package/test/parse/l3-diagnostics-contract.test.js +66 -0
  122. package/test/typecheck/adt-decl-stage2.test.js +83 -0
  123. package/test/typecheck/container-contract-e1309.test.js +258 -0
  124. package/test/typecheck/ffi-bracket-l3.test.js +61 -0
  125. package/test/typecheck/l3-diagnostics-matrix.test.js +248 -0
  126. package/test/typecheck/l3-partial-pipeline.test.js +74 -0
  127. package/test/typecheck/opaque-ffi-type.test.js +78 -0
  128. package/test/typecheck/sig-type-stage3.test.js +190 -0
  129. package/test/typecheck/stack-check-stage4.test.js +149 -0
  130. package/test/typecheck/stack-check-stage5.test.js +74 -0
  131. package/test/typecheck/stack-check-stage6.test.js +56 -0
  132. package/test/typecheck/stack-check-stage7.test.js +160 -0
  133. package/test/typecheck/stack-check-stage8.test.js +146 -0
  134. package/test/typecheck/stack-check-stage9.test.js +105 -0
  135. package/test/typecheck/typecheck-env.test.js +53 -0
  136. package/test/typecheck/typecheck-pipeline.test.js +37 -0
  137. package/README.md +0 -37
  138. package/cli/sail.js +0 -151
  139. package/cli/typecheck.js +0 -39
  140. package/docs/ARCHITECTURE.md +0 -50
  141. package/docs/CHANGELOG.md +0 -18
  142. package/docs/FFI-GUIDE.md +0 -65
  143. package/docs/RELEASE.md +0 -36
  144. package/docs/TESTING.md +0 -86
  145. package/test/integration.test.js +0 -61
@@ -0,0 +1,365 @@
1
+ /**
2
+ * L2: резолв имён по RFC-0.1 §4.2, §7, §9, §11; диагностики E11xx/E12xx.
3
+ */
4
+ import path from 'node:path'
5
+ import { parseSource } from '../parse/index.js'
6
+ import { isBuiltinWord } from '../parse/builtins-set.js'
7
+ import * as diag from '../parse/diagnostics.js'
8
+ import { extractSailFragmentsFromJs } from '../ffi/extract-jsdoc-sail.js'
9
+ import { analyzeLocalDeclarations } from './local-declarations.js'
10
+ import { resolveImportPath } from './import-path.js'
11
+ import { buildScopeForSnapshot, moduleExportsWord } from './module-scope.js'
12
+ import { walkWordDecl } from './walk-ast-refs.js'
13
+
14
+ /**
15
+ * @param {object} span
16
+ * @param {string} code
17
+ * @param {string} message
18
+ */
19
+ function pushDiag (out, span, code, message) {
20
+ const d = { code, message }
21
+ if (span?.start) {
22
+ d.offset = span.start.offset
23
+ d.line = span.start.line
24
+ d.column = span.start.column
25
+ }
26
+ out.push(d)
27
+ }
28
+
29
+ /**
30
+ * @param {string} sourceText
31
+ * @param {object[]} into
32
+ * @returns {{ kind: 'source_file', items: object[], span: object }}
33
+ */
34
+ function parseJsContractAst (sourceText, into) {
35
+ const frags = extractSailFragmentsFromJs(sourceText)
36
+ const items = []
37
+ const seenW = new Set()
38
+ const dummySpan = {
39
+ start: { offset: 0, line: 1, column: 1 },
40
+ end: { offset: 0, line: 1, column: 1 }
41
+ }
42
+ for (const frag of frags) {
43
+ const r = parseSource(frag)
44
+ if (!r.ok || !r.ast?.items?.length) {
45
+ into.push({
46
+ code: 'E1113',
47
+ message: diag.e1113InvalidJsSailJSDoc(),
48
+ offset: 0,
49
+ line: 1,
50
+ column: 1
51
+ })
52
+ continue
53
+ }
54
+ /** @type {object[]} */
55
+ const chunk = []
56
+ const localW = new Set()
57
+ let skip = false
58
+ for (const it of r.ast.items) {
59
+ if (it.kind === 'word') {
60
+ if (seenW.has(it.name)) {
61
+ pushDiag(into, it.span, 'E1106', diag.e1106DuplicateJsSailBlock(it.name))
62
+ skip = true
63
+ break
64
+ }
65
+ if (localW.has(it.name)) {
66
+ pushDiag(into, it.span, 'E1101', diag.e1101DuplicateWord(it.name))
67
+ skip = true
68
+ break
69
+ }
70
+ localW.add(it.name)
71
+ chunk.push(it)
72
+ continue
73
+ }
74
+ if (it.kind === 'sum_type' || it.kind === 'product_type') {
75
+ chunk.push(it)
76
+ continue
77
+ }
78
+ into.push({
79
+ code: 'E1113',
80
+ message: diag.e1113InvalidJsSailJSDoc(),
81
+ offset: 0,
82
+ line: 1,
83
+ column: 1
84
+ })
85
+ skip = true
86
+ break
87
+ }
88
+ if (skip) continue
89
+ for (const it of chunk) {
90
+ if (it.kind === 'word') seenW.add(it.name)
91
+ items.push(it)
92
+ }
93
+ }
94
+ return { kind: 'source_file', items, span: dummySpan }
95
+ }
96
+
97
+ function buildTypeIndex (items) {
98
+ const m = new Map()
99
+ for (const it of items) {
100
+ if (it.kind === 'sum_type' || it.kind === 'product_type') {
101
+ m.set(it.name, it)
102
+ }
103
+ }
104
+ return m
105
+ }
106
+
107
+ /**
108
+ * Скобочный реэкспорт: символы в exportWords / exportTypes и копия ADT в typeIndex (RFC-0.1 §4.2).
109
+ */
110
+ function mergeBracketReexports (importerPath, items, exportWords, exportTypes, typeIndex, ctx) {
111
+ for (const item of items) {
112
+ if (item.kind !== 'import' || !item.bracket) continue
113
+ const r = resolveImportPath(importerPath, item.path, ctx.readFile, ctx.resolvePackage)
114
+ if (!r.resolvedPath) continue
115
+ const dep = ctx.snapshots.get(r.resolvedPath)
116
+ if (!dep?.ok) continue
117
+ for (const w of item.bracket.words) {
118
+ if (dep.exportWords.has(w)) exportWords.add(w)
119
+ }
120
+ for (const ty of item.bracket.types) {
121
+ if (dep.exportTypes.has(ty)) {
122
+ exportTypes.add(ty)
123
+ if (!typeIndex.has(ty) && dep.typeIndex?.has(ty)) {
124
+ typeIndex.set(ty, dep.typeIndex.get(ty))
125
+ }
126
+ }
127
+ }
128
+ }
129
+ }
130
+
131
+ /**
132
+ * @param {object} ctx
133
+ * @param {string} absPath
134
+ */
135
+ function loadSnapshot (ctx, absPath) {
136
+ if (ctx.snapshots.has(absPath)) return ctx.snapshots.get(absPath)
137
+
138
+ if (ctx.loadingStack.includes(absPath)) {
139
+ const i = ctx.loadingStack.indexOf(absPath)
140
+ ctx.cycleModulesLabel = ctx.loadingStack.slice(i).map(p => path.basename(p)).join(', ')
141
+ return { error: 'cycle', path: absPath }
142
+ }
143
+
144
+ ctx.loadingStack.push(absPath)
145
+
146
+ const text = ctx.readFile(absPath)
147
+ if (text == null || text === undefined) {
148
+ ctx.loadingStack.pop()
149
+ const err = { error: 'missing', path: absPath }
150
+ ctx.snapshots.set(absPath, err)
151
+ return err
152
+ }
153
+
154
+ let ast
155
+ const parseDiags = []
156
+ if (absPath.endsWith('.js')) {
157
+ ast = parseJsContractAst(text, parseDiags)
158
+ } else {
159
+ const pr = parseSource(text)
160
+ if (!pr.ok) {
161
+ ctx.loadingStack.pop()
162
+ const err = { error: 'parse', diagnostics: pr.diagnostics, path: absPath }
163
+ ctx.snapshots.set(absPath, err)
164
+ return err
165
+ }
166
+ ast = pr.ast
167
+ }
168
+
169
+ const items = ast.items
170
+ const { diagnostics: declDiags, exportWords: ew0, exportTypes: et0 } = analyzeLocalDeclarations(items)
171
+ const typeIndex0 = buildTypeIndex(items)
172
+ const exportWords = new Set(ew0)
173
+ const exportTypes = new Set(et0)
174
+ const typeIndex = new Map(typeIndex0)
175
+ const allDeclDiags = [...parseDiags, ...declDiags]
176
+
177
+ for (const item of items) {
178
+ if (item.kind !== 'import') continue
179
+ const r = resolveImportPath(absPath, item.path, ctx.readFile, ctx.resolvePackage)
180
+ if (r.resolvedPath) {
181
+ const dep = loadSnapshot(ctx, r.resolvedPath)
182
+ if (dep?.error === 'cycle') {
183
+ ctx.loadingStack.pop()
184
+ const err = { error: 'cycle', path: absPath }
185
+ ctx.snapshots.set(absPath, err)
186
+ return err
187
+ }
188
+ }
189
+ }
190
+
191
+ ctx.loadingStack.pop()
192
+
193
+ mergeBracketReexports(absPath, items, exportWords, exportTypes, typeIndex, ctx)
194
+
195
+ const snap = {
196
+ ok: true,
197
+ path: absPath,
198
+ ast,
199
+ items,
200
+ declDiagnostics: allDeclDiags,
201
+ exportWords,
202
+ exportTypes,
203
+ typeIndex
204
+ }
205
+ ctx.snapshots.set(absPath, snap)
206
+ return snap
207
+ }
208
+
209
+ /**
210
+ * @param {object} snap
211
+ * @param {{ unq: Set<string>, importMap: Map<string, object> }} scope
212
+ * @param {object[]} out
213
+ */
214
+ function resolveUseSitesInSnapshot (snap, scope, out) {
215
+ for (const item of snap.items) {
216
+ if (item.kind !== 'word') continue
217
+ walkWordDecl(item, (ev) => {
218
+ if (ev.kind === 'builtin_ref') {
219
+ const n = ev.node.name
220
+ if (!isBuiltinWord(n)) {
221
+ pushDiag(out, ev.node.span, 'E1206', diag.e1206UnknownBuiltin(n))
222
+ }
223
+ return
224
+ }
225
+ if (ev.kind === 'word_ref') {
226
+ const n = ev.node.name
227
+ if (scope.unq.has(n)) return
228
+ const mods = []
229
+ for (const [alias, dep] of scope.importMap) {
230
+ if (moduleExportsWord(dep, n)) mods.push(alias)
231
+ }
232
+ if (mods.length === 0) {
233
+ pushDiag(out, ev.node.span, 'E1201', diag.e1201UnresolvedName(n))
234
+ } else if (mods.length === 1) {
235
+ pushDiag(out, ev.node.span, 'E1205', diag.e1205ImportNeedsQualification(n, mods[0]))
236
+ } else {
237
+ pushDiag(out, ev.node.span, 'E1202', diag.e1202AmbiguousName(n))
238
+ }
239
+ return
240
+ }
241
+ if (ev.kind === 'module_word_ref') {
242
+ const { module: m, word: w } = ev.node
243
+ const dep = scope.importMap.get(m)
244
+ if (!dep) {
245
+ pushDiag(out, ev.node.span, 'E1203', diag.e1203ModuleNotFound(m))
246
+ } else if (!moduleExportsWord(dep, w)) {
247
+ pushDiag(out, ev.node.span, 'E1204', diag.e1204MissingMember(m, w))
248
+ }
249
+ return
250
+ }
251
+ if (ev.kind === 'module_type_ref') {
252
+ const { module: m, type: ty } = ev.node
253
+ const dep = scope.importMap.get(m)
254
+ if (!dep) {
255
+ pushDiag(out, ev.node.span, 'E1203', diag.e1203ModuleNotFound(m))
256
+ } else if (!dep.exportTypes.has(ty)) {
257
+ pushDiag(out, ev.node.span, 'E1204', diag.e1204MissingMember(m, ty))
258
+ }
259
+ }
260
+ })
261
+ }
262
+ }
263
+
264
+ /**
265
+ * @param {Map<string, object>} snapshots
266
+ */
267
+ function buildSymbolTablesMap (snapshots) {
268
+ const m = new Map()
269
+ for (const [p, s] of snapshots) {
270
+ if (s && s.ok && s.exportWords) {
271
+ m.set(p, {
272
+ path: p,
273
+ words: [...s.exportWords].sort(),
274
+ types: [...s.exportTypes].sort()
275
+ })
276
+ }
277
+ }
278
+ return m
279
+ }
280
+
281
+ /**
282
+ * symbol_tables: путь → { words, types }. words — только `@` и реэкспорт `@` из скобок; types — локальные `&` и реэкспорт `&`.
283
+ * Имена autogen не перечисляются в words; `~M/w` для autogen учитывается через экспортируемые типы и typeIndex.
284
+ *
285
+ * @param {{ entryPath: string, readFile: (p: string) => string | null | undefined, resolvePackage?: (spec: string, fromPath: string) => string | null, withSnapshots?: boolean }} opts
286
+ * @returns {{ ok: boolean, diagnostics: object[], symbolTables?: Map<string, { path: string, words: string[], types: string[] }>, snapshots?: Map<string, object> }}
287
+ */
288
+ export function resolveSailNames (opts) {
289
+ const readFile = opts.readFile
290
+ const entryPath = path.normalize(opts.entryPath)
291
+ const ctx = {
292
+ readFile,
293
+ resolvePackage: opts.resolvePackage,
294
+ snapshots: new Map(),
295
+ loadingStack: [],
296
+ cycleModulesLabel: null
297
+ }
298
+
299
+ const entrySnap = loadSnapshot(ctx, entryPath)
300
+
301
+ if (ctx.cycleModulesLabel || entrySnap?.error === 'cycle') {
302
+ const label = ctx.cycleModulesLabel || path.basename(entryPath)
303
+ return {
304
+ ok: false,
305
+ diagnostics: [{
306
+ code: 'E1112',
307
+ message: diag.e1112CyclicSailImports(label)
308
+ }],
309
+ symbolTables: buildSymbolTablesMap(ctx.snapshots)
310
+ }
311
+ }
312
+
313
+ if (entrySnap?.error === 'missing') {
314
+ return {
315
+ ok: false,
316
+ diagnostics: [{
317
+ code: 'E1203',
318
+ message: diag.e1203ModuleNotFound(path.basename(entryPath, path.extname(entryPath)))
319
+ }],
320
+ symbolTables: buildSymbolTablesMap(ctx.snapshots)
321
+ }
322
+ }
323
+
324
+ if (entrySnap?.error === 'parse') {
325
+ return {
326
+ ok: false,
327
+ diagnostics: entrySnap.diagnostics ?? [{ code: 'E1001', message: 'parse failed' }],
328
+ symbolTables: buildSymbolTablesMap(ctx.snapshots)
329
+ }
330
+ }
331
+
332
+ if (!entrySnap || entrySnap.ok !== true) {
333
+ return {
334
+ ok: false,
335
+ diagnostics: [{ code: 'E1203', message: diag.e1203ModuleNotFound('entry') }],
336
+ symbolTables: buildSymbolTablesMap(ctx.snapshots)
337
+ }
338
+ }
339
+
340
+ const allDiags = []
341
+ for (const [, s] of ctx.snapshots) {
342
+ if (s?.ok && Array.isArray(s.declDiagnostics)) {
343
+ allDiags.push(...s.declDiagnostics)
344
+ }
345
+ }
346
+
347
+ for (const [, s] of ctx.snapshots) {
348
+ if (!s?.ok) continue
349
+ const scope = buildScopeForSnapshot(s, ctx)
350
+ allDiags.push(...scope.diags)
351
+ resolveUseSitesInSnapshot(s, scope, allDiags)
352
+ }
353
+
354
+ const ok = allDiags.length === 0
355
+ /** @type {{ ok: boolean, diagnostics: object[], symbolTables: Map<string, { path: string, words: string[], types: string[] }>, snapshots?: Map<string, object> }} */
356
+ const out = {
357
+ ok,
358
+ diagnostics: allDiags,
359
+ symbolTables: buildSymbolTablesMap(ctx.snapshots)
360
+ }
361
+ if (opts.withSnapshots) {
362
+ out.snapshots = ctx.snapshots
363
+ }
364
+ return out
365
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Обход use-sites для L2: word_ref, module_word_ref, module_type_ref (RFC-0.1 §11).
3
+ */
4
+
5
+ /** @param {object} typeExpr */
6
+ export function walkTypeExpr (typeExpr, visit) {
7
+ if (!typeExpr) return
8
+ switch (typeExpr.kind) {
9
+ case 'type_name':
10
+ case 'type_var':
11
+ return
12
+ case 'module_type_ref':
13
+ visit({ kind: 'module_type_ref', node: typeExpr })
14
+ return
15
+ case 'paren_type':
16
+ walkTypeExpr(typeExpr.inner, visit)
17
+ return
18
+ case 'type_app':
19
+ for (const a of typeExpr.args) walkTypeExpr(a, visit)
20
+ return
21
+ case 'quotation_type':
22
+ walkSignature(typeExpr.inner, visit)
23
+ return
24
+ default:
25
+ return
26
+ }
27
+ }
28
+
29
+ /** @param {object} item AstSigStackItem */
30
+ export function walkSigStackItem (item, visit) {
31
+ if (!item) return
32
+ switch (item.kind) {
33
+ case 'sig_type_expr':
34
+ walkTypeExpr(item.type, visit)
35
+ return
36
+ case 'quotation_sig':
37
+ walkSignature(item.inner, visit)
38
+ return
39
+ case 'named_quotation_sig':
40
+ walkSigStackItem(item.quotation, visit)
41
+ return
42
+ case 'stack_var':
43
+ case 'sig_effect_add':
44
+ case 'sig_effect_remove':
45
+ return
46
+ default:
47
+ return
48
+ }
49
+ }
50
+
51
+ /** @param {object} sig AstSignature */
52
+ export function walkSignature (sig, visit) {
53
+ if (!sig) return
54
+ for (const x of sig.left) walkSigStackItem(x, visit)
55
+ for (const x of sig.right) walkSigStackItem(x, visit)
56
+ }
57
+
58
+ /** @param {object[]} steps AstWordStep[] */
59
+ export function walkWordSteps (steps, visit) {
60
+ if (!steps) return
61
+ for (const step of steps) {
62
+ switch (step.kind) {
63
+ case 'word_ref':
64
+ visit({ kind: 'word_ref', node: step })
65
+ break
66
+ case 'module_word_ref':
67
+ visit({ kind: 'module_word_ref', node: step })
68
+ break
69
+ case 'builtin':
70
+ visit({ kind: 'builtin_ref', node: step })
71
+ break
72
+ case 'literal':
73
+ case 'slot_write':
74
+ case 'slot_read':
75
+ break
76
+ case 'quotation':
77
+ walkWordSteps(step.body, visit)
78
+ break
79
+ case 'list_literal':
80
+ break
81
+ default:
82
+ break
83
+ }
84
+ }
85
+ }
86
+
87
+ /** @param {object} word AstWord */
88
+ export function walkWordDecl (word, visit) {
89
+ walkSignature(word.signature, visit)
90
+ walkWordSteps(word.body, visit)
91
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * AST node factories (RFC-0.1 §12.2). No classes — plain objects.
3
+ */
4
+
5
+ /** @param {{ offset: number, line: number, column: number }} t */
6
+ export function posFromToken (t) {
7
+ return { offset: t.offset, line: t.line, column: t.column }
8
+ }
9
+
10
+ export function endPosFromState (state) {
11
+ return { offset: state.i, line: state.line, column: state.col }
12
+ }
13
+
14
+ export function openSpan (tok) {
15
+ return {
16
+ start: posFromToken(tok),
17
+ end: { offset: tok.offset, line: tok.line, column: tok.column }
18
+ }
19
+ }
20
+
21
+ export function closeSpan (state, span) {
22
+ span.end = endPosFromState(state)
23
+ return span
24
+ }
25
+
26
+ export function spanRange (state, startTok) {
27
+ return closeSpan(state, openSpan(startTok))
28
+ }
29
+
30
+ export function spanCoords (state, startOff, startLine, startCol) {
31
+ return {
32
+ start: { offset: startOff, line: startLine, column: startCol },
33
+ end: endPosFromState(state)
34
+ }
35
+ }
36
+
37
+ /**
38
+ * @param {string} kind
39
+ * @param {{ start: object, end: object }} span
40
+ * @param {Record<string, unknown>} fields
41
+ */
42
+ export function node (kind, span, fields) {
43
+ return { kind, span, ...fields }
44
+ }
45
+
46
+ export function mergeSpan (spanA, spanB) {
47
+ return {
48
+ start: spanA.start,
49
+ end: spanB.end
50
+ }
51
+ }