@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,912 @@
1
+ /**
2
+ * Phrase parser RFC-0.1 §12.2 (L1) + построение AST (ast-spec.js).
3
+ */
4
+
5
+ import { createLexerState, nextToken, readPathToken, skipWs } from './lexer.js'
6
+ import * as diag from './diagnostics.js'
7
+ import * as ab from './ast-build.js'
8
+
9
+ function skipInlineWs (state) {
10
+ while (state.i < state.source.length) {
11
+ const c = state.source[state.i]
12
+ if (c === ' ' || c === '\t') {
13
+ state.i++
14
+ state.col++
15
+ } else break
16
+ }
17
+ }
18
+
19
+ function createCtx (state) {
20
+ return {
21
+ state,
22
+ diagnostics: [],
23
+ regexpOk: false,
24
+ buf: null,
25
+ pushback: [],
26
+ items: [],
27
+ pendingDoc: []
28
+ }
29
+ }
30
+
31
+ function peek (ctx) {
32
+ if (ctx.pushback.length) return ctx.pushback[ctx.pushback.length - 1]
33
+ if (ctx.buf === null) ctx.buf = nextToken(ctx.state, ctx)
34
+ return ctx.buf
35
+ }
36
+
37
+ function eat (ctx) {
38
+ if (ctx.pushback.length) return ctx.pushback.pop()
39
+ const t = peek(ctx)
40
+ ctx.buf = null
41
+ return t
42
+ }
43
+
44
+ function unget (ctx, t) {
45
+ ctx.pushback.push(t)
46
+ }
47
+
48
+ function pushE1001 (ctx, t, tokenText) {
49
+ ctx.diagnostics.push({
50
+ code: 'E1001',
51
+ message: diag.e1001UnexpectedToken(tokenText),
52
+ offset: t.offset,
53
+ line: t.line,
54
+ column: t.column
55
+ })
56
+ }
57
+
58
+ function pushDiag (ctx, code, message, t) {
59
+ ctx.diagnostics.push({
60
+ code,
61
+ message,
62
+ offset: t.offset,
63
+ line: t.line,
64
+ column: t.column
65
+ })
66
+ }
67
+
68
+ function tokenSpan (ctx, tok) {
69
+ const sp = ab.openSpan(tok)
70
+ ab.closeSpan(ctx.state, sp)
71
+ return sp
72
+ }
73
+
74
+ function drainComments (ctx) {
75
+ const out = []
76
+ while (peek(ctx).type === 'COMMENT') out.push(eat(ctx).value)
77
+ return out
78
+ }
79
+
80
+ function forkCtx (ctx) {
81
+ const state = {
82
+ source: ctx.state.source,
83
+ i: ctx.state.i,
84
+ line: ctx.state.line,
85
+ col: ctx.state.col
86
+ }
87
+ const sub = createCtx(state)
88
+ sub.regexpOk = ctx.regexpOk
89
+ return sub
90
+ }
91
+
92
+ /**
93
+ * `(` … парная `)` содержит хотя бы один `->` при depth === 1 внутри этой пары
94
+ * (стрелка «между In и Out» этой скобочной группы, не внутри вложенных `(` … `)`).
95
+ * Тогда это quotation_sig / quotation_type; иначе — paren_type + type_expr.
96
+ */
97
+ function sigParenHasTopLevelArrow (ctx) {
98
+ const t = peek(ctx)
99
+ if (t.type !== '(') return false
100
+ const sub = forkCtx(ctx)
101
+ // peek() мог оставить buf и продвинуть state.i за «(»; fork иначе начнёт не с той скобки.
102
+ sub.state.i = t.offset
103
+ sub.state.line = t.line
104
+ sub.state.col = t.column
105
+ sub.buf = null
106
+ eat(sub)
107
+ let depth = 1
108
+ let sawArrowAtDepth1 = false
109
+ while (depth > 0 && peek(sub).type !== 'EOF') {
110
+ const u = eat(sub)
111
+ if (u.type === '(') depth++
112
+ else if (u.type === ')') depth--
113
+ else if (u.type === 'ARROW' && depth === 1) sawArrowAtDepth1 = true
114
+ }
115
+ return sawArrowAtDepth1
116
+ }
117
+
118
+ function skipBodyComments (ctx) {
119
+ while (peek(ctx).type === 'COMMENT') eat(ctx)
120
+ }
121
+
122
+ function wrapSigType (inner) {
123
+ return ab.node('sig_type_expr', inner.span, { type: inner })
124
+ }
125
+
126
+ function effectNameFromAdd (t) {
127
+ const s = String(t.value)
128
+ return s.startsWith('+') ? s.slice(1) : s
129
+ }
130
+
131
+ function effectNameFromSub (t) {
132
+ const s = String(t.value)
133
+ return s.startsWith('-') ? s.slice(1) : s
134
+ }
135
+
136
+ function sigTypeArgFollows (ctx) {
137
+ const t = peek(ctx)
138
+ return (
139
+ t.type === '(' ||
140
+ t.type === 'MODULE_TYPE_REF' ||
141
+ t.type === 'TYPE_NAME' ||
142
+ t.type === 'LOWER_IDENT'
143
+ )
144
+ }
145
+
146
+ function typeExprDelimiter (ctx) {
147
+ const t = peek(ctx)
148
+ return (
149
+ t.type === 'EOF' ||
150
+ t.type === ')' ||
151
+ t.type === '|' ||
152
+ t.type === 'ARROW' ||
153
+ t.type === 'MODULE' ||
154
+ t.type === 'AMP' ||
155
+ t.type === 'WORD_DEF' ||
156
+ t.type === 'COMMENT'
157
+ )
158
+ }
159
+
160
+ /**
161
+ * TypeName без параметров на слоте операнда `List` / `Dict` / `Map` (RFC-0.1 list_type, dict_type, map_type):
162
+ * иначе `Map Str Num` ошибочно разбиралось как `Map (Str Num)`.
163
+ * Параметризованные формы (`Maybe a`, `Vec T`) и группировка — через полный `buildTypeExpr` или `( … )`.
164
+ */
165
+ const PRIMITIVE_TYPE_NAMES = new Set([
166
+ 'Str', 'String', 'Num', 'Int', 'Bool', 'Float', 'Double', 'Char', 'Unit', 'Nil'
167
+ ])
168
+
169
+ /** @returns {object | null} AstTypeExpr */
170
+ function buildTypeExprCombinatorOperand (ctx) {
171
+ const t = peek(ctx)
172
+ if (t.type === '(') {
173
+ const o = eat(ctx)
174
+ const sp = ab.openSpan(o)
175
+ const inner = buildTypeExpr(ctx)
176
+ if (!inner) return null
177
+ if (peek(ctx).type !== ')') {
178
+ const u = peek(ctx)
179
+ pushE1001(ctx, u, String(u.value || u.type))
180
+ return null
181
+ }
182
+ eat(ctx)
183
+ ab.closeSpan(ctx.state, sp)
184
+ return ab.node('paren_type', sp, { inner })
185
+ }
186
+ if (t.type === 'MODULE_TYPE_REF') {
187
+ const m = eat(ctx)
188
+ return ab.node('module_type_ref', tokenSpan(ctx, m), { module: m.module, type: m.typeName })
189
+ }
190
+ if (t.type === 'LOWER_IDENT') {
191
+ const id = eat(ctx)
192
+ const sp = tokenSpan(ctx, id)
193
+ return ab.node('type_var', sp, { name: String(id.value) })
194
+ }
195
+ if (t.type !== 'TYPE_NAME') {
196
+ pushE1001(ctx, t, String(t.value || t.type))
197
+ return null
198
+ }
199
+ const nm = String(t.value)
200
+ if (PRIMITIVE_TYPE_NAMES.has(nm)) {
201
+ const idtok = eat(ctx)
202
+ return ab.node('type_name', tokenSpan(ctx, idtok), { name: nm })
203
+ }
204
+ return buildTypeExpr(ctx)
205
+ }
206
+
207
+ /** @returns {object | null} AstTypeExpr */
208
+ function buildTypeExpr (ctx) {
209
+ const t = peek(ctx)
210
+ if (t.type === '(') {
211
+ const o = eat(ctx)
212
+ const sp = ab.openSpan(o)
213
+ const inner = buildTypeExpr(ctx)
214
+ if (!inner) return null
215
+ if (peek(ctx).type !== ')') {
216
+ const u = peek(ctx)
217
+ pushE1001(ctx, u, String(u.value || u.type))
218
+ return null
219
+ }
220
+ eat(ctx)
221
+ ab.closeSpan(ctx.state, sp)
222
+ return ab.node('paren_type', sp, { inner })
223
+ }
224
+ if (t.type === 'MODULE_TYPE_REF') {
225
+ const m = eat(ctx)
226
+ return ab.node('module_type_ref', tokenSpan(ctx, m), { module: m.module, type: m.typeName })
227
+ }
228
+ if (t.type !== 'LOWER_IDENT' && t.type !== 'TYPE_NAME') {
229
+ pushE1001(ctx, t, String(t.value || t.type))
230
+ return null
231
+ }
232
+ const first = eat(ctx)
233
+ const sp = ab.openSpan(first)
234
+ if (first.value === 'List') {
235
+ const arg = buildTypeExprCombinatorOperand(ctx)
236
+ if (!arg) return null
237
+ ab.closeSpan(ctx.state, sp)
238
+ return ab.node('type_app', sp, { ctor: 'List', args: [arg] })
239
+ }
240
+ if (first.value === 'Dict') {
241
+ const arg = buildTypeExprCombinatorOperand(ctx)
242
+ if (!arg) return null
243
+ ab.closeSpan(ctx.state, sp)
244
+ return ab.node('type_app', sp, { ctor: 'Dict', args: [arg] })
245
+ }
246
+ if (first.value === 'Map') {
247
+ const a = buildTypeExprCombinatorOperand(ctx)
248
+ if (!a) return null
249
+ const b = buildTypeExprCombinatorOperand(ctx)
250
+ if (!b) return null
251
+ ab.closeSpan(ctx.state, sp)
252
+ return ab.node('type_app', sp, { ctor: 'Map', args: [a, b] })
253
+ }
254
+ if (first.type === 'TYPE_NAME') {
255
+ const args = []
256
+ while (!typeExprDelimiter(ctx) && sigTypeArgFollows(ctx)) {
257
+ const a = buildTypeExpr(ctx)
258
+ if (!a) return null
259
+ args.push(a)
260
+ }
261
+ ab.closeSpan(ctx.state, sp)
262
+ if (args.length === 0) return ab.node('type_name', sp, { name: first.value })
263
+ return ab.node('type_app', sp, { ctor: first.value, args })
264
+ }
265
+ ab.closeSpan(ctx.state, sp)
266
+ return ab.node('type_var', sp, { name: first.value })
267
+ }
268
+
269
+ /** @returns {object | null} AstTypeExpr (core of sig_type_expr) */
270
+ function buildSigTypeExprCore (ctx) {
271
+ const t = peek(ctx)
272
+ if (t.type === '(') {
273
+ if (sigParenHasTopLevelArrow(ctx)) {
274
+ const q = parseQuotSigAst(ctx)
275
+ if (!q) return null
276
+ return ab.node('quotation_type', q.node.span, { inner: q.node.inner })
277
+ }
278
+ const o = eat(ctx)
279
+ const sp = ab.openSpan(o)
280
+ const inner = buildTypeExpr(ctx)
281
+ if (!inner) return null
282
+ if (peek(ctx).type !== ')') {
283
+ const u = peek(ctx)
284
+ pushE1001(ctx, u, String(u.value || u.type))
285
+ return null
286
+ }
287
+ eat(ctx)
288
+ ab.closeSpan(ctx.state, sp)
289
+ return ab.node('paren_type', sp, { inner })
290
+ }
291
+ if (t.type === 'MODULE_TYPE_REF') {
292
+ const m = eat(ctx)
293
+ return ab.node('module_type_ref', tokenSpan(ctx, m), { module: m.module, type: m.typeName })
294
+ }
295
+ if (t.type !== 'LOWER_IDENT' && t.type !== 'TYPE_NAME') {
296
+ pushE1001(ctx, t, String(t.value || t.type))
297
+ return null
298
+ }
299
+ const first = eat(ctx)
300
+ const sp = ab.openSpan(first)
301
+ if (first.value === 'List') {
302
+ const arg = buildTypeExprCombinatorOperand(ctx)
303
+ if (!arg) return null
304
+ ab.closeSpan(ctx.state, sp)
305
+ return ab.node('type_app', sp, { ctor: 'List', args: [arg] })
306
+ }
307
+ if (first.value === 'Dict') {
308
+ const arg = buildTypeExprCombinatorOperand(ctx)
309
+ if (!arg) return null
310
+ ab.closeSpan(ctx.state, sp)
311
+ return ab.node('type_app', sp, { ctor: 'Dict', args: [arg] })
312
+ }
313
+ if (first.value === 'Map') {
314
+ const a = buildTypeExprCombinatorOperand(ctx)
315
+ if (!a) return null
316
+ const b = buildTypeExprCombinatorOperand(ctx)
317
+ if (!b) return null
318
+ ab.closeSpan(ctx.state, sp)
319
+ return ab.node('type_app', sp, { ctor: 'Map', args: [a, b] })
320
+ }
321
+ if (first.type === 'TYPE_NAME') {
322
+ const head = String(first.value)
323
+ // Примитив — один слот; соседний `TYPE_NAME` — следующий sig_item, не аргумент (RFC-0.1: скобки только у параметризованных форм).
324
+ if (PRIMITIVE_TYPE_NAMES.has(head)) {
325
+ ab.closeSpan(ctx.state, sp)
326
+ return ab.node('type_name', sp, { name: head })
327
+ }
328
+ const args = []
329
+ while (sigTypeArgFollows(ctx)) {
330
+ const a = buildSigTypeExprCore(ctx)
331
+ if (!a) return null
332
+ args.push(a)
333
+ }
334
+ ab.closeSpan(ctx.state, sp)
335
+ if (args.length === 0) return ab.node('type_name', sp, { name: first.value })
336
+ return ab.node('type_app', sp, { ctor: first.value, args })
337
+ }
338
+ ab.closeSpan(ctx.state, sp)
339
+ return ab.node('type_var', sp, { name: first.value })
340
+ }
341
+
342
+ function parseSigItemInto (ctx, side, stack, effectsAdd, effectsRemove) {
343
+ const t = peek(ctx)
344
+ if (t.type === 'COMMENT') {
345
+ pushE1001(ctx, t, 'comment')
346
+ return false
347
+ }
348
+ if (t.type === 'EFFECT_ADD') {
349
+ const e = eat(ctx)
350
+ if (side === 'left') {
351
+ pushDiag(ctx, 'E1007', diag.e1007EffectOnlyAfterArrow(String(e.value)), e)
352
+ return false
353
+ }
354
+ const sp = tokenSpan(ctx, e)
355
+ effectsAdd.push(ab.node('sig_effect_add', sp, { name: effectNameFromAdd(e), side }))
356
+ return true
357
+ }
358
+ if (t.type === 'EFFECT_SUB') {
359
+ const e = eat(ctx)
360
+ if (side === 'left') {
361
+ pushDiag(ctx, 'E1007', diag.e1007EffectOnlyAfterArrow(String(e.value)), e)
362
+ return false
363
+ }
364
+ const sp = tokenSpan(ctx, e)
365
+ effectsRemove.push(ab.node('sig_effect_remove', sp, { name: effectNameFromSub(e), side }))
366
+ return true
367
+ }
368
+ if (t.type === 'STACK_VAR') {
369
+ const s = eat(ctx)
370
+ const sp = tokenSpan(ctx, s)
371
+ stack.push(ab.node('stack_var', sp, { name: String(s.value) }))
372
+ return true
373
+ }
374
+ if (t.type === '(') {
375
+ if (sigParenHasTopLevelArrow(ctx)) {
376
+ const q = parseQuotSigAst(ctx)
377
+ if (!q) return false
378
+ stack.push(q.node)
379
+ return true
380
+ }
381
+ const o = eat(ctx)
382
+ const sp = ab.openSpan(o)
383
+ const inner = buildTypeExpr(ctx)
384
+ if (!inner) return false
385
+ if (peek(ctx).type !== ')') {
386
+ const u = peek(ctx)
387
+ pushE1001(ctx, u, String(u.value || u.type))
388
+ return false
389
+ }
390
+ eat(ctx)
391
+ ab.closeSpan(ctx.state, sp)
392
+ stack.push(wrapSigType(ab.node('paren_type', sp, { inner })))
393
+ return true
394
+ }
395
+ if (t.type === 'MODULE_TYPE_REF') {
396
+ const inner = buildSigTypeExprCore(ctx)
397
+ if (!inner) return false
398
+ stack.push(wrapSigType(inner))
399
+ return true
400
+ }
401
+ if (t.type !== 'LOWER_IDENT' && t.type !== 'TYPE_NAME') {
402
+ pushE1001(ctx, t, String(t.value || t.type))
403
+ return false
404
+ }
405
+ const a = eat(ctx)
406
+ if (peek(ctx).type === ':') {
407
+ const prefixSp = tokenSpan(ctx, a)
408
+ eat(ctx)
409
+ const res = parseQuotSigAst(ctx)
410
+ if (!res) return false
411
+ const sp = ab.mergeSpan(prefixSp, res.node.span)
412
+ const named = ab.node('named_quotation_sig', sp, { prefix: String(a.value), quotation: res.node })
413
+ stack.push(named)
414
+ return true
415
+ }
416
+ if (
417
+ a.type === 'TYPE_NAME' &&
418
+ String(a.value) === 'Quote' &&
419
+ peek(ctx).type === '('
420
+ ) {
421
+ const quoteSp = tokenSpan(ctx, a)
422
+ const q = parseQuotSigAst(ctx)
423
+ if (!q) return false
424
+ const sp = ab.mergeSpan(quoteSp, q.node.span)
425
+ stack.push(ab.node('quotation_sig', sp, { inner: q.node.inner }))
426
+ return true
427
+ }
428
+ unget(ctx, a)
429
+ const inner = buildSigTypeExprCore(ctx)
430
+ if (!inner) return false
431
+ stack.push(wrapSigType(inner))
432
+ return true
433
+ }
434
+
435
+ /** @returns {{ node: object, sawOuterArrow: boolean } | null} */
436
+ function parseQuotSigAst (ctx) {
437
+ const o = eat(ctx)
438
+ if (o.type !== '(') {
439
+ unget(ctx, o)
440
+ return null
441
+ }
442
+ const sp = ab.openSpan(o)
443
+ const left = []
444
+ const right = []
445
+ const effectsAdd = []
446
+ const effectsRemove = []
447
+ const half = (side, st) => {
448
+ while (peek(ctx).type !== 'ARROW' && peek(ctx).type !== ')') {
449
+ if (!parseSigItemInto(ctx, side, st, effectsAdd, effectsRemove)) return false
450
+ }
451
+ return true
452
+ }
453
+ if (!half('left', left)) return null
454
+ let sawOuterArrow = false
455
+ if (peek(ctx).type === 'ARROW') {
456
+ sawOuterArrow = true
457
+ eat(ctx)
458
+ if (!half('right', right)) return null
459
+ }
460
+ if (peek(ctx).type !== ')') {
461
+ const u = peek(ctx)
462
+ pushE1001(ctx, u, String(u.value || u.type))
463
+ return null
464
+ }
465
+ const c = eat(ctx)
466
+ ab.closeSpan(ctx.state, sp)
467
+ const inner = ab.node('signature', sp, {
468
+ left,
469
+ right,
470
+ effectsAdd,
471
+ effectsRemove
472
+ })
473
+ const node = ab.node('quotation_sig', sp, { inner })
474
+ return { node, sawOuterArrow }
475
+ }
476
+
477
+ function parseParenSignatureAst (ctx) {
478
+ const res = parseQuotSigAst(ctx)
479
+ if (!res) return null
480
+ const { node, sawOuterArrow } = res
481
+ const sig = node.inner
482
+ // RFC-0.1 §6.1.1: лишняя пара скобок вокруг sugar-цитаты `( ( In -> Out ) )` не нормируется;
483
+ // без верхнего `->` у слова это один stack-item `quotation_sig` — отклоняем (E1001).
484
+ if (
485
+ !sawOuterArrow &&
486
+ sig.left.length === 1 &&
487
+ sig.right.length === 0 &&
488
+ sig.left[0].kind === 'quotation_sig'
489
+ ) {
490
+ const st = sig.left[0].span.start
491
+ pushE1001(ctx, st, '(')
492
+ return null
493
+ }
494
+ return sig
495
+ }
496
+
497
+ function parseSignatureAst (ctx) {
498
+ return parseParenSignatureAst(ctx)
499
+ }
500
+
501
+ function parseImportAst (ctx) {
502
+ const modTok = eat(ctx)
503
+ ctx.regexpOk = false
504
+ ctx.buf = null
505
+ const sp = ab.openSpan(modTok)
506
+ const moduleName = String(modTok.value)
507
+ let bracket = null
508
+ const st = ctx.state
509
+ skipWs(st)
510
+ if (st.i < st.source.length && st.source[st.i] === '(') {
511
+ bracket = { words: [], types: [] }
512
+ st.i += 1
513
+ st.col += 1
514
+ while (true) {
515
+ skipWs(st)
516
+ if (st.i >= st.source.length) {
517
+ pushE1001(ctx, { type: 'EOF', offset: st.i, line: st.line, column: st.col }, 'EOF')
518
+ return null
519
+ }
520
+ if (st.source[st.i] === ')') {
521
+ st.i += 1
522
+ st.col += 1
523
+ break
524
+ }
525
+ ctx.buf = null
526
+ const u = peek(ctx)
527
+ if (u.type === 'WORD_DEF') {
528
+ const wd = eat(ctx)
529
+ bracket.words.push(String(wd.value))
530
+ } else if (u.type === 'AMP') {
531
+ eat(ctx)
532
+ const tn = eat(ctx)
533
+ if (tn.type !== 'TYPE_NAME') {
534
+ pushE1001(ctx, tn, String(tn.value || tn.type))
535
+ return null
536
+ }
537
+ bracket.types.push(String(tn.value))
538
+ } else {
539
+ pushE1001(ctx, u, String(u.value || u.type))
540
+ return null
541
+ }
542
+ }
543
+ }
544
+ const pathTok = readPathToken(ctx.state, ctx)
545
+ ctx.buf = null
546
+ if (!pathTok) {
547
+ ctx.diagnostics.push({
548
+ code: 'E1003',
549
+ message: diag.e1003BadImportPath(''),
550
+ offset: modTok.offset,
551
+ line: modTok.line,
552
+ column: modTok.column
553
+ })
554
+ return null
555
+ }
556
+ const src = ctx.state.source
557
+ skipInlineWs(ctx.state)
558
+ if (ctx.state.i < src.length) {
559
+ const ch = src[ctx.state.i]
560
+ if (ch !== '\n' && ch !== '\r') {
561
+ const lineEnd = src.indexOf('\n', ctx.state.i)
562
+ const end = lineEnd === -1 ? src.length : lineEnd
563
+ const bad = src.slice(pathTok.offset, end).replace(/\s+$/, '')
564
+ ctx.diagnostics.push({
565
+ code: 'E1003',
566
+ message: diag.e1003BadImportPath(bad),
567
+ offset: pathTok.offset,
568
+ line: pathTok.line,
569
+ column: pathTok.column
570
+ })
571
+ ctx.state.i = end
572
+ return null
573
+ }
574
+ }
575
+ ab.closeSpan(ctx.state, sp)
576
+ const doc = ctx.pendingDoc ? [...ctx.pendingDoc] : []
577
+ return ab.node('import', sp, {
578
+ module: moduleName,
579
+ path: String(pathTok.value),
580
+ bracket,
581
+ doc
582
+ })
583
+ }
584
+
585
+ function parseTypeDeclAst (ctx) {
586
+ if (peek(ctx).type !== 'AMP') return null
587
+ const amp = eat(ctx)
588
+ const declSp = ab.openSpan(amp)
589
+ const name = eat(ctx)
590
+ if (name.type !== 'TYPE_NAME') {
591
+ pushE1001(ctx, name, String(name.value || name.type))
592
+ return null
593
+ }
594
+ const typeParams = []
595
+ while (peek(ctx).type === 'LOWER_IDENT' || peek(ctx).type === 'TYPE_NAME') {
596
+ const p = eat(ctx)
597
+ typeParams.push(String(p.value))
598
+ }
599
+ const headerDoc = drainComments(ctx)
600
+ const u = peek(ctx)
601
+ if (u.type === '|') {
602
+ const tags = []
603
+ while (peek(ctx).type === '|') {
604
+ eat(ctx)
605
+ const tag = eat(ctx)
606
+ if (tag.type !== 'TYPE_NAME') {
607
+ pushE1001(ctx, tag, String(tag.value || tag.type))
608
+ return null
609
+ }
610
+ const tagSp = ab.openSpan(tag)
611
+ let payload = null
612
+ const v = peek(ctx)
613
+ if (
614
+ v.type !== '|' &&
615
+ v.type !== 'EOF' &&
616
+ v.type !== 'AMP' &&
617
+ v.type !== 'WORD_DEF' &&
618
+ v.type !== 'MODULE' &&
619
+ v.type !== 'COMMENT'
620
+ ) {
621
+ payload = buildTypeExpr(ctx)
622
+ if (!payload) return null
623
+ tagSp.end = payload.span.end
624
+ }
625
+ const tagDoc = drainComments(ctx)
626
+ ab.closeSpan(ctx.state, tagSp)
627
+ tags.push({
628
+ name: String(tag.value),
629
+ payload,
630
+ span: tagSp,
631
+ doc: tagDoc
632
+ })
633
+ }
634
+ ab.closeSpan(ctx.state, declSp)
635
+ return ab.node('sum_type', declSp, {
636
+ name: String(name.value),
637
+ typeParams,
638
+ doc: headerDoc,
639
+ tags
640
+ })
641
+ }
642
+ if (u.type === ':') {
643
+ const fields = []
644
+ while (peek(ctx).type === ':') {
645
+ eat(ctx)
646
+ const fn = eat(ctx)
647
+ if (fn.type !== 'LOWER_IDENT') {
648
+ pushE1001(ctx, fn, String(fn.value || fn.type))
649
+ return null
650
+ }
651
+ const fnSp = tokenSpan(ctx, fn)
652
+ const ty = buildTypeExpr(ctx)
653
+ if (!ty) return null
654
+ const fieldDoc = drainComments(ctx)
655
+ const fsp = ab.mergeSpan(fnSp, ty.span)
656
+ fields.push({
657
+ name: String(fn.value),
658
+ type: ty,
659
+ span: fsp,
660
+ doc: fieldDoc
661
+ })
662
+ }
663
+ ab.closeSpan(ctx.state, declSp)
664
+ return ab.node('product_type', declSp, {
665
+ name: String(name.value),
666
+ typeParams,
667
+ doc: headerDoc,
668
+ fields
669
+ })
670
+ }
671
+ pushE1001(ctx, u, String(u.value || u.type))
672
+ return null
673
+ }
674
+
675
+ function litKindFromToken (t) {
676
+ if (t.type === 'STRING') return 'string'
677
+ if (t.type === 'NUMBER') return 'number'
678
+ if (t.type === 'BIGINT') return 'bigint'
679
+ if (t.type === 'REGEXP') return 'regexp'
680
+ if (t.type === 'NIL') return 'nil'
681
+ if (t.type === 'TRUE' || t.type === 'FALSE') return 'bool'
682
+ return 'string'
683
+ }
684
+
685
+ function isLiteralToken (t) {
686
+ return (
687
+ t.type === 'STRING' ||
688
+ t.type === 'NUMBER' ||
689
+ t.type === 'BIGINT' ||
690
+ t.type === 'REGEXP' ||
691
+ t.type === 'NIL' ||
692
+ t.type === 'TRUE' ||
693
+ t.type === 'FALSE'
694
+ )
695
+ }
696
+
697
+ function parseListLiteralAst (ctx) {
698
+ const o = eat(ctx)
699
+ const sp = ab.openSpan(o)
700
+ const elements = []
701
+ while (peek(ctx).type !== ']') {
702
+ if (peek(ctx).type === 'EOF') return null
703
+ if (peek(ctx).type === 'COMMENT') {
704
+ pushE1001(ctx, peek(ctx), 'comment')
705
+ return null
706
+ }
707
+ ctx.regexpOk = true
708
+ const t = peek(ctx)
709
+ if (!isLiteralToken(t)) {
710
+ pushE1001(ctx, t, String(t.value || t.type))
711
+ return null
712
+ }
713
+ const lt = eat(ctx)
714
+ elements.push(ab.node('literal', tokenSpan(ctx, lt), { litKind: litKindFromToken(lt), raw: String(lt.value) }))
715
+ }
716
+ eat(ctx)
717
+ ab.closeSpan(ctx.state, sp)
718
+ return ab.node('list_literal', sp, { elements })
719
+ }
720
+
721
+ function parseExecutableQuotAst (ctx) {
722
+ const o = eat(ctx)
723
+ const sp = ab.openSpan(o)
724
+ const body = []
725
+ while (peek(ctx).type !== ')') {
726
+ if (peek(ctx).type === 'EOF') return null
727
+ if (peek(ctx).type === 'ARROW') {
728
+ const a = eat(ctx)
729
+ pushDiag(ctx, 'E1005', diag.e1005ArrowInQuotation(), a)
730
+ return null
731
+ }
732
+ skipBodyComments(ctx)
733
+ if (peek(ctx).type === ')') break
734
+ ctx.regexpOk = true
735
+ const step = parseWordStepAst(ctx)
736
+ if (!step) return null
737
+ body.push(step)
738
+ }
739
+ eat(ctx)
740
+ ab.closeSpan(ctx.state, sp)
741
+ return ab.node('quotation', sp, { body })
742
+ }
743
+
744
+ function parseWordStepAst (ctx) {
745
+ const t = peek(ctx)
746
+ if (t.type === 'EFFECT_ADD') {
747
+ pushDiag(ctx, 'E1006', diag.e1006EffectOutsideSignature(t.value), t)
748
+ eat(ctx)
749
+ return null
750
+ }
751
+ if (t.type === 'EFFECT_SUB') {
752
+ pushDiag(ctx, 'E1006', diag.e1006EffectOutsideSignature(t.value), t)
753
+ eat(ctx)
754
+ return null
755
+ }
756
+ if (t.type === 'WORD_DEF') {
757
+ pushE1001(ctx, t, '@')
758
+ return null
759
+ }
760
+ if (t.type === '(') return parseExecutableQuotAst(ctx)
761
+ if (t.type === '[') return parseListLiteralAst(ctx)
762
+ if (isLiteralToken(t)) {
763
+ const lt = eat(ctx)
764
+ return ab.node('literal', tokenSpan(ctx, lt), { litKind: litKindFromToken(lt), raw: String(lt.value) })
765
+ }
766
+ if (t.type === 'WORD_REF') {
767
+ const w = eat(ctx)
768
+ return ab.node('word_ref', tokenSpan(ctx, w), { name: String(w.value) })
769
+ }
770
+ if (t.type === 'MODULE_WORD_REF') {
771
+ const w = eat(ctx)
772
+ return ab.node('module_word_ref', tokenSpan(ctx, w), { module: w.module, word: w.word })
773
+ }
774
+ if (t.type === 'LOWER_IDENT') {
775
+ const id = eat(ctx)
776
+ const sp = tokenSpan(ctx, id)
777
+ const nm = String(id.value)
778
+ return ab.node('builtin', sp, { name: nm })
779
+ }
780
+ if (t.type === ':') {
781
+ eat(ctx)
782
+ const n = eat(ctx)
783
+ if (n.type !== 'LOWER_IDENT') {
784
+ pushE1001(ctx, n, String(n.value || n.type))
785
+ return null
786
+ }
787
+ return ab.node('slot_write', tokenSpan(ctx, n), { name: String(n.value) })
788
+ }
789
+ if (t.type === ';') {
790
+ eat(ctx)
791
+ const n = eat(ctx)
792
+ if (n.type !== 'LOWER_IDENT') {
793
+ pushE1001(ctx, n, String(n.value || n.type))
794
+ return null
795
+ }
796
+ return ab.node('slot_read', tokenSpan(ctx, n), { name: String(n.value) })
797
+ }
798
+ pushE1001(ctx, t, String(t.value || t.type))
799
+ return null
800
+ }
801
+
802
+ function parseWordAst (ctx) {
803
+ const at = eat(ctx)
804
+ if (at.type !== 'WORD_DEF') {
805
+ unget(ctx, at)
806
+ return null
807
+ }
808
+ ctx.regexpOk = false
809
+ const wspan = ab.openSpan(at)
810
+ const signature = parseSignatureAst(ctx)
811
+ if (!signature) {
812
+ if (ctx.diagnostics.length === 0) {
813
+ const t = peek(ctx)
814
+ pushE1001(ctx, t, String(t.value || t.type))
815
+ }
816
+ return null
817
+ }
818
+ ctx.regexpOk = true
819
+ const wordDoc = drainComments(ctx)
820
+ const body = []
821
+ for (;;) {
822
+ skipBodyComments(ctx)
823
+ ctx.regexpOk = true
824
+ const t = peek(ctx)
825
+ if (t.type === 'EOF') break
826
+ if (
827
+ t.type === 'MODULE' ||
828
+ t.type === 'AMP' ||
829
+ t.type === 'WORD_DEF'
830
+ ) {
831
+ if (t.column === 1) break
832
+ }
833
+ const step = parseWordStepAst(ctx)
834
+ if (!step) return null
835
+ body.push(step)
836
+ }
837
+ ab.closeSpan(ctx.state, wspan)
838
+ return ab.node('word', wspan, {
839
+ name: String(at.value),
840
+ signature,
841
+ doc: wordDoc,
842
+ body
843
+ })
844
+ }
845
+
846
+ function parseTopLevelAst (ctx) {
847
+ const t = peek(ctx)
848
+ ctx.regexpOk = false
849
+ if (t.type === 'MODULE') {
850
+ const n = parseImportAst(ctx)
851
+ if (n) {
852
+ ctx.items.push(n)
853
+ return true
854
+ }
855
+ return false
856
+ }
857
+ if (t.type === 'AMP') {
858
+ const n = parseTypeDeclAst(ctx)
859
+ if (n) {
860
+ ctx.items.push(n)
861
+ return true
862
+ }
863
+ return false
864
+ }
865
+ if (t.type === 'WORD_DEF') {
866
+ const n = parseWordAst(ctx)
867
+ if (n) {
868
+ ctx.items.push(n)
869
+ return true
870
+ }
871
+ return false
872
+ }
873
+ if (t.type === 'LOWER_IDENT' || t.type === 'TYPE_NAME') {
874
+ pushDiag(ctx, 'E1004', diag.e1004UnknownBlock(t.value), t)
875
+ eat(ctx)
876
+ return false
877
+ }
878
+ pushE1001(ctx, t, String(t.value || t.type))
879
+ return false
880
+ }
881
+
882
+ function finishSourceFileAst (ctx) {
883
+ const end = ab.endPosFromState(ctx.state)
884
+ const span = {
885
+ start: { offset: 0, line: 1, column: 1 },
886
+ end
887
+ }
888
+ return ab.node('source_file', span, { items: ctx.items })
889
+ }
890
+
891
+ export function parseSourceText (sourceText) {
892
+ const state = createLexerState(sourceText)
893
+ const ctx = createCtx(state)
894
+ let pendingDoc = []
895
+ while (peek(ctx).type !== 'EOF') {
896
+ pendingDoc = pendingDoc.concat(drainComments(ctx))
897
+ if (peek(ctx).type === 'EOF') break
898
+ ctx.pendingDoc = pendingDoc
899
+ pendingDoc = []
900
+ const ok = parseTopLevelAst(ctx)
901
+ ctx.pendingDoc = []
902
+ if (ctx.diagnostics.length || !ok) break
903
+ }
904
+ drainComments(ctx)
905
+ if (peek(ctx).type !== 'EOF' && ctx.diagnostics.length === 0) {
906
+ const t = peek(ctx)
907
+ pushE1001(ctx, t, String(t.value || t.type))
908
+ }
909
+ const ast =
910
+ ctx.diagnostics.length === 0 ? finishSourceFileAst(ctx) : undefined
911
+ return { diagnostics: ctx.diagnostics, ast }
912
+ }