@algosail/lang 0.2.11 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/bin/sail.mjs +4 -0
  2. package/cli/run-cli.js +176 -0
  3. package/index.js +11 -2
  4. package/lib/codegen/README.md +230 -0
  5. package/lib/codegen/codegen-diagnostics.js +164 -0
  6. package/lib/codegen/compile-graph.js +107 -0
  7. package/lib/codegen/emit-adt.js +177 -0
  8. package/lib/codegen/emit-body.js +1265 -0
  9. package/lib/codegen/emit-builtin.js +371 -0
  10. package/lib/codegen/emit-jsdoc-sail.js +383 -0
  11. package/lib/codegen/emit-module.js +498 -0
  12. package/lib/codegen/esm-imports.js +26 -0
  13. package/lib/codegen/index.js +69 -0
  14. package/lib/codegen/out-layout.js +102 -0
  15. package/lib/ffi/extract-jsdoc-sail.js +34 -0
  16. package/lib/io-node/index.js +4 -0
  17. package/lib/io-node/package-root.js +18 -0
  18. package/lib/io-node/read-file.js +12 -0
  19. package/lib/io-node/resolve-package.js +24 -0
  20. package/lib/io-node/resolve-sail-names-from-disk.js +21 -0
  21. package/lib/ir/assert-json-serializable.js +30 -0
  22. package/lib/ir/attach-call-effects.js +108 -0
  23. package/lib/ir/bind-values.js +594 -0
  24. package/lib/ir/build-module-ir.js +290 -0
  25. package/lib/ir/index.js +31 -0
  26. package/lib/ir/lower-body-steps.js +170 -0
  27. package/lib/ir/module-metadata.js +65 -0
  28. package/lib/ir/schema-version.js +15 -0
  29. package/lib/ir/serialize.js +202 -0
  30. package/lib/ir/stitch-types.js +92 -0
  31. package/lib/names/adt-autogen.js +22 -0
  32. package/lib/names/import-path.js +28 -0
  33. package/lib/names/index.js +1 -0
  34. package/lib/names/local-declarations.js +127 -0
  35. package/lib/names/lower-first.js +6 -0
  36. package/lib/names/module-scope.js +120 -0
  37. package/lib/names/resolve-sail.js +365 -0
  38. package/lib/names/walk-ast-refs.js +91 -0
  39. package/lib/parse/ast-build.js +51 -0
  40. package/lib/parse/ast-spec.js +212 -0
  41. package/lib/parse/builtins-set.js +12 -0
  42. package/lib/parse/diagnostics.js +180 -0
  43. package/lib/parse/index.js +46 -0
  44. package/lib/parse/lexer.js +390 -0
  45. package/lib/parse/parse-source.js +912 -0
  46. package/lib/typecheck/adt-autogen-sigs.js +345 -0
  47. package/lib/typecheck/build-type-env.js +148 -0
  48. package/lib/typecheck/builtin-signatures.js +183 -0
  49. package/lib/typecheck/check-word-body.js +1021 -0
  50. package/lib/typecheck/effect-decl.js +124 -0
  51. package/lib/typecheck/index.js +55 -0
  52. package/lib/typecheck/normalize-sig.js +369 -0
  53. package/lib/typecheck/stack-step-snapshots.js +56 -0
  54. package/lib/typecheck/unify-type.js +665 -0
  55. package/lib/typecheck/validate-adt.js +201 -0
  56. package/package.json +4 -9
  57. package/scripts/regen-demo-full-syntax-ast.mjs +22 -0
  58. package/test/cli/sail-cli.test.js +64 -0
  59. package/test/codegen/compile-bracket-ffi-e2e.test.js +64 -0
  60. package/test/codegen/compile-stage0.test.js +128 -0
  61. package/test/codegen/compile-stage4-layout.test.js +124 -0
  62. package/test/codegen/e2e-prelude-ffi-adt/app/extra.sail +6 -0
  63. package/test/codegen/e2e-prelude-ffi-adt/app/lib.sail +33 -0
  64. package/test/codegen/e2e-prelude-ffi-adt/app/main.sail +28 -0
  65. package/test/codegen/e2e-prelude-ffi-adt/artifacts/.gitignore +2 -0
  66. package/test/codegen/e2e-prelude-ffi-adt/ffi/helpers.js +27 -0
  67. package/test/codegen/e2e-prelude-ffi-adt.test.js +100 -0
  68. package/test/codegen/emit-adt-stage6.test.js +168 -0
  69. package/test/codegen/emit-async-stage5.test.js +164 -0
  70. package/test/codegen/emit-body-stage2.test.js +139 -0
  71. package/test/codegen/emit-body.test.js +163 -0
  72. package/test/codegen/emit-builtins-stage7.test.js +258 -0
  73. package/test/codegen/emit-diagnostics-stage9.test.js +90 -0
  74. package/test/codegen/emit-jsdoc-stage8.test.js +113 -0
  75. package/test/codegen/emit-module-stage3.test.js +78 -0
  76. package/test/conformance/conformance-ir-l4.test.js +38 -0
  77. package/test/conformance/conformance-l5-codegen.test.js +111 -0
  78. package/test/conformance/conformance-runner.js +91 -0
  79. package/test/conformance/conformance-suite-l3.test.js +32 -0
  80. package/test/ffi/prelude-jsdoc.test.js +49 -0
  81. package/test/fixtures/demo-full-syntax.ast.json +1471 -0
  82. package/test/fixtures/demo-full-syntax.sail +35 -0
  83. package/test/fixtures/io-node-ffi-adt/ffi.js +7 -0
  84. package/test/fixtures/io-node-ffi-adt/use.sail +4 -0
  85. package/test/fixtures/io-node-mini/dep.sail +2 -0
  86. package/test/fixtures/io-node-mini/entry.sail +4 -0
  87. package/test/fixtures/io-node-prelude/entry.sail +4 -0
  88. package/test/fixtures/io-node-reexport-chain/a.sail +4 -0
  89. package/test/fixtures/io-node-reexport-chain/b.sail +2 -0
  90. package/test/fixtures/io-node-reexport-chain/c.sail +2 -0
  91. package/test/io-node/resolve-disk.test.js +59 -0
  92. package/test/ir/bind-values.test.js +84 -0
  93. package/test/ir/build-module-ir.test.js +100 -0
  94. package/test/ir/call-effects.test.js +97 -0
  95. package/test/ir/ffi-bracket-ir.test.js +59 -0
  96. package/test/ir/full-ir-document.test.js +51 -0
  97. package/test/ir/ir-document-assert.js +67 -0
  98. package/test/ir/lower-body-steps.test.js +90 -0
  99. package/test/ir/module-metadata.test.js +42 -0
  100. package/test/ir/serialization-model.test.js +172 -0
  101. package/test/ir/stitch-types.test.js +74 -0
  102. package/test/names/l2-resolve-adt-autogen.test.js +155 -0
  103. package/test/names/l2-resolve-bracket-ffi.test.js +108 -0
  104. package/test/names/l2-resolve-declaration-and-bracket-errors.test.js +276 -0
  105. package/test/names/l2-resolve-graph.test.js +105 -0
  106. package/test/names/l2-resolve-single-file.test.js +79 -0
  107. package/test/parse/ast-spec.test.js +56 -0
  108. package/test/parse/ast.test.js +476 -0
  109. package/test/parse/contract.test.js +37 -0
  110. package/test/parse/fixtures-full-syntax.test.js +24 -0
  111. package/test/parse/helpers.js +27 -0
  112. package/test/parse/l0-lex-diagnostics-matrix.test.js +59 -0
  113. package/test/parse/l0-lex.test.js +40 -0
  114. package/test/parse/l1-diagnostics.test.js +77 -0
  115. package/test/parse/l1-import.test.js +28 -0
  116. package/test/parse/l1-parse-diagnostics-matrix.test.js +32 -0
  117. package/test/parse/l1-top-level.test.js +47 -0
  118. package/test/parse/l1-types.test.js +31 -0
  119. package/test/parse/l1-words.test.js +49 -0
  120. package/test/parse/l2-diagnostics-contract.test.js +67 -0
  121. package/test/parse/l3-diagnostics-contract.test.js +66 -0
  122. package/test/typecheck/adt-decl-stage2.test.js +83 -0
  123. package/test/typecheck/container-contract-e1309.test.js +258 -0
  124. package/test/typecheck/ffi-bracket-l3.test.js +61 -0
  125. package/test/typecheck/l3-diagnostics-matrix.test.js +248 -0
  126. package/test/typecheck/l3-partial-pipeline.test.js +74 -0
  127. package/test/typecheck/opaque-ffi-type.test.js +78 -0
  128. package/test/typecheck/sig-type-stage3.test.js +190 -0
  129. package/test/typecheck/stack-check-stage4.test.js +149 -0
  130. package/test/typecheck/stack-check-stage5.test.js +74 -0
  131. package/test/typecheck/stack-check-stage6.test.js +56 -0
  132. package/test/typecheck/stack-check-stage7.test.js +160 -0
  133. package/test/typecheck/stack-check-stage8.test.js +146 -0
  134. package/test/typecheck/stack-check-stage9.test.js +105 -0
  135. package/test/typecheck/typecheck-env.test.js +53 -0
  136. package/test/typecheck/typecheck-pipeline.test.js +37 -0
  137. package/README.md +0 -37
  138. package/cli/sail.js +0 -151
  139. package/cli/typecheck.js +0 -39
  140. package/docs/ARCHITECTURE.md +0 -50
  141. package/docs/CHANGELOG.md +0 -18
  142. package/docs/FFI-GUIDE.md +0 -65
  143. package/docs/RELEASE.md +0 -36
  144. package/docs/TESTING.md +0 -86
  145. package/test/integration.test.js +0 -61
@@ -0,0 +1,476 @@
1
+ /**
2
+ * Контракт AST для публичного `parseSource` (RFC-0.1 п.12.2, `lang/lib/parse/ast-spec.js`).
3
+ *
4
+ * Ожидается: при успехе всегда есть объект `ast` (в т.ч. для пустого/пробельного файла);
5
+ * при ошибке `ast === undefined`. Следующий этап — реализация в `parse-source.js` / `index.js`.
6
+ */
7
+ import test from 'brittle'
8
+ import { parseSource } from '../../index.js'
9
+
10
+ /** @param {import('brittle').Test} t */
11
+ function assertPos (t, p, label) {
12
+ t.ok(p !== null && typeof p === 'object', `${label} is an object`)
13
+ t.ok(Number.isFinite(p.offset) && p.offset >= 0, `${label}.offset`)
14
+ t.ok(Number.isFinite(p.line) && p.line >= 1, `${label}.line`)
15
+ t.ok(Number.isFinite(p.column) && p.column >= 1, `${label}.column`)
16
+ }
17
+
18
+ /** @param {import('brittle').Test} t */
19
+ function assertSpan (t, span) {
20
+ t.ok(span != null && typeof span === 'object', 'span is an object')
21
+ if (span == null || typeof span !== 'object') return
22
+ assertPos(t, span.start, 'span.start')
23
+ assertPos(t, span.end, 'span.end')
24
+ }
25
+
26
+ /**
27
+ * @param {import('brittle').Test} t
28
+ * @param {unknown} ast
29
+ * @returns {boolean}
30
+ */
31
+ function assertSourceFileShell (t, ast) {
32
+ t.ok(ast !== null && ast !== undefined && typeof ast === 'object', 'ast is an object')
33
+ if (ast == null || typeof ast !== 'object') return false
34
+ t.is(ast.kind, 'source_file')
35
+ t.ok(Array.isArray(ast.items), 'ast.items is an array')
36
+ if (!Array.isArray(ast.items)) return false
37
+ assertSpan(t, ast.span)
38
+ return ast.span != null && ast.span.start != null && ast.span.end != null
39
+ }
40
+
41
+ test('parseSource: при ошибке ast отсутствует (undefined)', function (t) {
42
+ const r = parseSource('not_a_marker')
43
+ t.ok(r.ok === false)
44
+ t.is(r.ast, undefined)
45
+ t.end()
46
+ })
47
+
48
+ test('parseSource: при успехе ast — объект source_file с span и items', function (t) {
49
+ const r = parseSource('+Math ./math.sail')
50
+ t.ok(r.ok === true)
51
+ t.ok(r.ast !== undefined && r.ast !== null)
52
+ assertSourceFileShell(t, r.ast)
53
+ t.end()
54
+ })
55
+
56
+ test('AST: пустой файл и только пробелы дают source_file с пустыми items', function (t) {
57
+ for (const src of ['', ' \n\t \n']) {
58
+ const r = parseSource(src)
59
+ t.ok(r.ok === true, `ok for ${JSON.stringify(src)}`)
60
+ t.ok(r.ast !== undefined && r.ast !== null, `ast present for ${JSON.stringify(src)}`)
61
+ if (!assertSourceFileShell(t, r.ast)) continue
62
+ t.alike(r.ast.items, [])
63
+ }
64
+ t.end()
65
+ })
66
+
67
+ test('AST: простой импорт +Module path', function (t) {
68
+ const r = parseSource('+Math ./math.sail\n')
69
+ t.ok(r.ok)
70
+ if (!assertSourceFileShell(t, r.ast)) return t.end()
71
+ t.is(r.ast.items.length, 1)
72
+ const imp = r.ast.items[0]
73
+ t.is(imp.kind, 'import')
74
+ assertSpan(t, imp.span)
75
+ t.is(imp.module, 'Math')
76
+ t.is(imp.path, './math.sail')
77
+ t.alike(imp.doc, [])
78
+ t.is(imp.bracket, null)
79
+ t.end()
80
+ })
81
+
82
+ test('AST: скобочный импорт — bracket.words и bracket.types', function (t) {
83
+ const r = parseSource('+Lib ( @add &Point ) ./lib.sail')
84
+ t.ok(r.ok)
85
+ if (!assertSourceFileShell(t, r.ast)) return t.end()
86
+ const imp = r.ast.items[0]
87
+ t.is(imp.kind, 'import')
88
+ t.ok(imp.bracket !== null)
89
+ t.alike(imp.bracket.words, ['add'])
90
+ t.alike(imp.bracket.types, ['Point'])
91
+ t.end()
92
+ })
93
+
94
+ test('AST: doc перед импортом попадает в import.doc', function (t) {
95
+ const r = parseSource('-- hello --\n+Mod ./p.sail')
96
+ t.ok(r.ok)
97
+ if (!assertSourceFileShell(t, r.ast)) return t.end()
98
+ const imp = r.ast.items[0]
99
+ t.alike(imp.doc, [' hello '])
100
+ t.end()
101
+ })
102
+
103
+ test('AST: sum-тип с тегами и пустыми doc', function (t) {
104
+ const src = '&Bool\n| True\n| False'
105
+ const r = parseSource(src)
106
+ t.ok(r.ok)
107
+ if (!assertSourceFileShell(t, r.ast)) return t.end()
108
+ const sum = r.ast.items[0]
109
+ t.is(sum.kind, 'sum_type')
110
+ assertSpan(t, sum.span)
111
+ t.is(sum.name, 'Bool')
112
+ t.alike(sum.typeParams, [])
113
+ t.alike(sum.doc, [])
114
+ t.is(sum.tags.length, 2)
115
+ t.is(sum.tags[0].name, 'True')
116
+ t.is(sum.tags[0].payload, null)
117
+ t.alike(sum.tags[0].doc, [])
118
+ assertSpan(t, sum.tags[0].span)
119
+ t.is(sum.tags[1].name, 'False')
120
+ t.end()
121
+ })
122
+
123
+ test('AST: sum-тип с параметром типа и полезной нагрузкой у тега', function (t) {
124
+ const src = '&Maybe a\n| None\n| Some a'
125
+ const r = parseSource(src)
126
+ t.ok(r.ok)
127
+ if (!assertSourceFileShell(t, r.ast)) return t.end()
128
+ const sum = r.ast.items[0]
129
+ t.is(sum.name, 'Maybe')
130
+ t.alike(sum.typeParams, ['a'])
131
+ t.is(sum.tags[0].name, 'None')
132
+ t.is(sum.tags[0].payload, null)
133
+ t.is(sum.tags[1].name, 'Some')
134
+ t.ok(sum.tags[1].payload !== null)
135
+ t.is(sum.tags[1].payload.kind, 'type_var')
136
+ t.is(sum.tags[1].payload.name, 'a')
137
+ t.end()
138
+ })
139
+
140
+ test('AST: product-тип с полями', function (t) {
141
+ const src = '&Point\n:x Num\n:y Num'
142
+ const r = parseSource(src)
143
+ t.ok(r.ok)
144
+ if (!assertSourceFileShell(t, r.ast)) return t.end()
145
+ const prod = r.ast.items[0]
146
+ t.is(prod.kind, 'product_type')
147
+ t.is(prod.name, 'Point')
148
+ t.alike(prod.typeParams, [])
149
+ t.alike(prod.doc, [])
150
+ t.is(prod.fields.length, 2)
151
+ t.is(prod.fields[0].name, 'x')
152
+ t.is(prod.fields[0].type.kind, 'type_name')
153
+ t.is(prod.fields[0].type.name, 'Num')
154
+ t.alike(prod.fields[0].doc, [])
155
+ assertSpan(t, prod.fields[0].span)
156
+ t.is(prod.fields[1].name, 'y')
157
+ t.end()
158
+ })
159
+
160
+ test('AST: слово с пустой сигнатурой ( -> ) и пустым телом', function (t) {
161
+ const r = parseSource('@nop ( -> )\n\n')
162
+ t.ok(r.ok)
163
+ if (!assertSourceFileShell(t, r.ast)) return t.end()
164
+ t.is(r.ast.items.length, 1)
165
+ const w = r.ast.items[0]
166
+ t.is(w.kind, 'word')
167
+ t.is(w.name, 'nop')
168
+ assertSpan(t, w.span)
169
+ t.alike(w.doc, [])
170
+ const sig = w.signature
171
+ t.is(sig.kind, 'signature')
172
+ assertSpan(t, sig.span)
173
+ t.alike(sig.left, [])
174
+ t.alike(sig.right, [])
175
+ t.alike(sig.effectsAdd, [])
176
+ t.alike(sig.effectsRemove, [])
177
+ t.alike(w.body, [])
178
+ t.end()
179
+ })
180
+
181
+ test('AST: сигнатура со stack_var ~s и типом Num с обеих сторон стрелки', function (t) {
182
+ const r = parseSource('@id ( ~s Num -> ~s Num )\n\n')
183
+ t.ok(r.ok)
184
+ if (!assertSourceFileShell(t, r.ast)) return t.end()
185
+ const w0 = r.ast.items[0]
186
+ t.ok(w0 && w0.signature, 'word has signature')
187
+ if (!w0 || !w0.signature) return t.end()
188
+ const sig = w0.signature
189
+ t.is(sig.left.length, 2)
190
+ t.is(sig.left[0].kind, 'stack_var')
191
+ t.is(sig.left[0].name, 's')
192
+ assertSpan(t, sig.left[0].span)
193
+ t.is(sig.left[1].kind, 'sig_type_expr')
194
+ t.is(sig.left[1].type.kind, 'type_name')
195
+ t.is(sig.left[1].type.name, 'Num')
196
+ assertSpan(t, sig.left[1].span)
197
+
198
+ t.is(sig.right.length, 2)
199
+ t.is(sig.right[0].kind, 'stack_var')
200
+ t.is(sig.right[0].name, 's')
201
+ t.is(sig.right[1].kind, 'sig_type_expr')
202
+ t.is(sig.right[1].type.kind, 'type_name')
203
+ t.is(sig.right[1].type.name, 'Num')
204
+ t.alike(sig.effectsAdd, [])
205
+ t.alike(sig.effectsRemove, [])
206
+ t.end()
207
+ })
208
+
209
+ test('AST: маркеры эффектов только в конце правой части (effectsAdd / effectsRemove, side right)', function (t) {
210
+ const r = parseSource('@run ( Num -> Str +Async +Fail -Async )\n\n')
211
+ t.ok(r.ok)
212
+ if (!assertSourceFileShell(t, r.ast)) return t.end()
213
+ const w0 = r.ast.items[0]
214
+ t.ok(w0 && w0.signature, 'word has signature')
215
+ if (!w0 || !w0.signature) return t.end()
216
+ const sig = w0.signature
217
+ t.is(sig.left.length, 1)
218
+ t.is(sig.left[0].kind, 'sig_type_expr')
219
+ t.is(sig.left[0].type.kind, 'type_name')
220
+ t.is(sig.left[0].type.name, 'Num')
221
+ t.is(sig.right.length, 1)
222
+ t.is(sig.right[0].kind, 'sig_type_expr')
223
+ t.is(sig.right[0].type.kind, 'type_name')
224
+ t.is(sig.right[0].type.name, 'Str')
225
+ t.is(sig.effectsAdd.length, 2)
226
+ t.is(sig.effectsAdd[0].kind, 'sig_effect_add')
227
+ t.is(sig.effectsAdd[0].name, 'Async')
228
+ t.is(sig.effectsAdd[0].side, 'right')
229
+ assertSpan(t, sig.effectsAdd[0].span)
230
+ t.is(sig.effectsAdd[1].kind, 'sig_effect_add')
231
+ t.is(sig.effectsAdd[1].name, 'Fail')
232
+ t.is(sig.effectsAdd[1].side, 'right')
233
+ t.is(sig.effectsRemove.length, 1)
234
+ t.is(sig.effectsRemove[0].kind, 'sig_effect_remove')
235
+ t.is(sig.effectsRemove[0].name, 'Async')
236
+ t.is(sig.effectsRemove[0].side, 'right')
237
+ t.end()
238
+ })
239
+
240
+ test('AST: сигнатура — type_app List Num на слоте', function (t) {
241
+ const r = parseSource('@f ( List Num -> )\n\n')
242
+ t.ok(r.ok)
243
+ if (!assertSourceFileShell(t, r.ast)) return t.end()
244
+ const sig = r.ast.items[0].signature
245
+ t.is(sig.left.length, 1)
246
+ t.is(sig.left[0].kind, 'sig_type_expr')
247
+ t.is(sig.left[0].type.kind, 'type_app')
248
+ t.is(sig.left[0].type.ctor, 'List')
249
+ t.is(sig.left[0].type.args.length, 1)
250
+ t.is(sig.left[0].type.args[0].kind, 'type_name')
251
+ t.is(sig.left[0].type.args[0].name, 'Num')
252
+ t.alike(sig.right, [])
253
+ t.end()
254
+ })
255
+
256
+ test('AST: сигнатура — paren_type вокруг Maybe Num', function (t) {
257
+ const r = parseSource('@f ( ( Maybe Num ) -> )\n\n')
258
+ t.ok(r.ok)
259
+ if (!assertSourceFileShell(t, r.ast)) return t.end()
260
+ const ty = r.ast.items[0].signature.left[0].type
261
+ t.is(ty.kind, 'paren_type')
262
+ t.is(ty.inner.kind, 'type_app')
263
+ t.is(ty.inner.ctor, 'Maybe')
264
+ t.is(ty.inner.args.length, 1)
265
+ t.is(ty.inner.args[0].kind, 'type_name')
266
+ t.is(ty.inner.args[0].name, 'Num')
267
+ t.end()
268
+ })
269
+
270
+ test('AST: сигнатура — module_type_ref ~Lib/Point (RFC-0.1 lit_type / module_type_ref)', function (t) {
271
+ const r = parseSource('@usePoint ( ~Lib/Point -> )\n\n')
272
+ t.ok(r.ok, 'парсер должен принимать ~Module/Type в sig_item (parseSigItem → parseSigTypeExpr)')
273
+ if (!assertSourceFileShell(t, r.ast)) return t.end()
274
+ const ty = r.ast.items[0].signature.left[0].type
275
+ t.is(ty.kind, 'module_type_ref')
276
+ t.is(ty.module, 'Lib')
277
+ t.is(ty.type, 'Point')
278
+ t.end()
279
+ })
280
+
281
+ test('AST: сигнатура — Quote ( Num -> Str ) как quotation_sig (RFC-0.1 §6.1)', function (t) {
282
+ const r = parseSource('@w ( Quote ( Num -> Str ) -> )\n\n')
283
+ t.ok(r.ok, r.diagnostics?.map((d) => d.message).join('; '))
284
+ if (!assertSourceFileShell(t, r.ast)) return t.end()
285
+ const sig = r.ast.items[0].signature
286
+ t.is(sig.left.length, 1)
287
+ t.is(sig.left[0].kind, 'quotation_sig')
288
+ const inner = sig.left[0].inner
289
+ t.is(inner.left.length, 1)
290
+ t.is(inner.left[0].type.kind, 'type_name')
291
+ t.is(inner.left[0].type.name, 'Num')
292
+ t.is(inner.right.length, 1)
293
+ t.is(inner.right[0].type.name, 'Str')
294
+ t.end()
295
+ })
296
+
297
+ test('AST: сигнатура — sugar ( ( Num -> Str ) -> ) вложенная цитата', function (t) {
298
+ const r = parseSource('@w ( ( Num -> Str ) -> )\n\n')
299
+ t.ok(r.ok, r.diagnostics?.map((d) => d.message).join('; '))
300
+ if (!assertSourceFileShell(t, r.ast)) return t.end()
301
+ const sig = r.ast.items[0].signature
302
+ t.is(sig.left.length, 1)
303
+ t.is(sig.left[0].kind, 'quotation_sig')
304
+ const inner = sig.left[0].inner
305
+ t.is(inner.left[0].type.name, 'Num')
306
+ t.is(inner.right[0].type.name, 'Str')
307
+ t.end()
308
+ })
309
+
310
+ test('AST: сигнатура — ( ( Num -> Str ) ) без внешней -> — ошибка', function (t) {
311
+ const r = parseSource('@bad ( ( Num -> Str ) )\n\n')
312
+ t.ok(r.ok === false)
313
+ t.is(r.diagnostics[0].code, 'E1001')
314
+ t.end()
315
+ })
316
+
317
+ test('AST: сигнатура — Maybe ( Num -> Str ) как type_app + quotation_type', function (t) {
318
+ const r = parseSource('@f ( Maybe ( Num -> Str ) -> )\n\n')
319
+ t.ok(r.ok, r.diagnostics?.map((d) => d.message).join('; '))
320
+ if (!assertSourceFileShell(t, r.ast)) return t.end()
321
+ const ty = r.ast.items[0].signature.left[0].type
322
+ t.is(ty.kind, 'type_app')
323
+ t.is(ty.ctor, 'Maybe')
324
+ t.is(ty.args.length, 1)
325
+ t.is(ty.args[0].kind, 'quotation_type')
326
+ t.is(ty.args[0].inner.left[0].type.name, 'Num')
327
+ t.is(ty.args[0].inner.right[0].type.name, 'Str')
328
+ t.end()
329
+ })
330
+
331
+ test('AST: сигнатура — named_quotation_sig (just: ( ~s a -> ~s a )) — stack_var и type_var', function (t) {
332
+ const r = parseSource('@withJust ( just: ( ~s a -> ~s a ) -> )\n\n /id\n')
333
+ t.ok(r.ok)
334
+ if (!assertSourceFileShell(t, r.ast)) return t.end()
335
+ const sig = r.ast.items[0].signature
336
+ t.is(sig.left.length, 1)
337
+ const nq = sig.left[0]
338
+ t.is(nq.kind, 'named_quotation_sig')
339
+ t.is(nq.prefix, 'just')
340
+ assertSpan(t, nq.span)
341
+ t.is(nq.quotation.kind, 'quotation_sig')
342
+ assertSpan(t, nq.quotation.span)
343
+ const inner = nq.quotation.inner
344
+ t.is(inner.kind, 'signature')
345
+ t.is(inner.left.length, 2)
346
+ t.is(inner.left[0].kind, 'stack_var')
347
+ t.is(inner.left[0].name, 's')
348
+ t.is(inner.left[1].kind, 'sig_type_expr')
349
+ t.is(inner.left[1].type.kind, 'type_var')
350
+ t.is(inner.left[1].type.name, 'a')
351
+ t.is(inner.right.length, 2)
352
+ t.is(inner.right[0].kind, 'stack_var')
353
+ t.is(inner.right[0].name, 's')
354
+ t.is(inner.right[1].kind, 'sig_type_expr')
355
+ t.is(inner.right[1].type.kind, 'type_var')
356
+ t.is(inner.right[1].type.name, 'a')
357
+ t.alike(inner.effectsAdd, [])
358
+ t.alike(inner.effectsRemove, [])
359
+ t.end()
360
+ })
361
+
362
+ test('AST: тело слова — string и number литералы', function (t) {
363
+ const r = parseSource('@w ( -> )\n\n "hi"\n 42\n')
364
+ t.ok(r.ok)
365
+ if (!assertSourceFileShell(t, r.ast)) return t.end()
366
+ const body = r.ast.items[0].body
367
+ t.is(body.length, 2)
368
+ t.is(body[0].kind, 'literal')
369
+ t.is(body[0].litKind, 'string')
370
+ t.is(body[0].raw, '"hi"')
371
+ assertSpan(t, body[0].span)
372
+ t.is(body[1].kind, 'literal')
373
+ t.is(body[1].litKind, 'number')
374
+ t.is(body[1].raw, '42')
375
+ t.end()
376
+ })
377
+
378
+ test('AST: тело слова — bool, nil', function (t) {
379
+ const r = parseSource('@w ( -> )\n\n true\n false\n nil\n')
380
+ t.ok(r.ok)
381
+ if (!assertSourceFileShell(t, r.ast)) return t.end()
382
+ const b = r.ast.items[0].body
383
+ t.is(b.length, 3)
384
+ t.is(b[0].litKind, 'bool')
385
+ t.is(b[0].raw, 'true')
386
+ t.is(b[1].litKind, 'bool')
387
+ t.is(b[1].raw, 'false')
388
+ t.is(b[2].litKind, 'nil')
389
+ t.is(b[2].raw, 'nil')
390
+ t.end()
391
+ })
392
+
393
+ test('AST: тело слова — word_ref, builtin, module_word_ref', function (t) {
394
+ const r = parseSource('@w ( -> )\n\n /swap\n dup\n ~Math/add\n')
395
+ t.ok(r.ok)
396
+ if (!assertSourceFileShell(t, r.ast)) return t.end()
397
+ const b = r.ast.items[0].body
398
+ t.is(b.length, 3)
399
+ t.is(b[0].kind, 'word_ref')
400
+ t.is(b[0].name, 'swap')
401
+ assertSpan(t, b[0].span)
402
+ t.is(b[1].kind, 'builtin')
403
+ t.is(b[1].name, 'dup')
404
+ t.is(b[2].kind, 'module_word_ref')
405
+ t.is(b[2].module, 'Math')
406
+ t.is(b[2].word, 'add')
407
+ t.end()
408
+ })
409
+
410
+ test('AST: неизвестное lower в теле — узел builtin (RFC-0.1 §13 E1206 на L2)', function (t) {
411
+ const r = parseSource('@w ( -> )\n\n not_a_real_builtin_xyz\n')
412
+ t.ok(r.ok)
413
+ if (!assertSourceFileShell(t, r.ast)) return t.end()
414
+ const b = r.ast.items[0].body
415
+ t.is(b.length, 1)
416
+ t.is(b[0].kind, 'builtin')
417
+ t.is(b[0].name, 'not_a_real_builtin_xyz')
418
+ t.end()
419
+ })
420
+
421
+ test('AST: тело слова — list_literal и quotation', function (t) {
422
+ const r = parseSource('@w ( -> )\n\n [ 1 2 ]\n ( /drop )\n')
423
+ t.ok(r.ok)
424
+ if (!assertSourceFileShell(t, r.ast)) return t.end()
425
+ const b = r.ast.items[0].body
426
+ t.is(b.length, 2)
427
+ t.is(b[0].kind, 'list_literal')
428
+ assertSpan(t, b[0].span)
429
+ t.is(b[0].elements.length, 2)
430
+ t.is(b[0].elements[0].litKind, 'number')
431
+ t.is(b[0].elements[0].raw, '1')
432
+ t.is(b[0].elements[1].raw, '2')
433
+ t.is(b[1].kind, 'quotation')
434
+ assertSpan(t, b[1].span)
435
+ t.is(b[1].body.length, 1)
436
+ t.is(b[1].body[0].kind, 'word_ref')
437
+ t.is(b[1].body[0].name, 'drop')
438
+ t.end()
439
+ })
440
+
441
+ test('AST: тело слова — slot_write и slot_read', function (t) {
442
+ const r = parseSource('@w ( -> )\n\n :acc\n ;acc\n')
443
+ t.ok(r.ok)
444
+ if (!assertSourceFileShell(t, r.ast)) return t.end()
445
+ const b = r.ast.items[0].body
446
+ t.is(b.length, 2)
447
+ t.is(b[0].kind, 'slot_write')
448
+ t.is(b[0].name, 'acc')
449
+ t.is(b[1].kind, 'slot_read')
450
+ t.is(b[1].name, 'acc')
451
+ t.end()
452
+ })
453
+
454
+ test('AST: doc между сигнатурой и телом слова (word.doc)', function (t) {
455
+ const r = parseSource('@w ( -> )\n-- about --\n\n /id\n')
456
+ t.ok(r.ok)
457
+ if (!assertSourceFileShell(t, r.ast)) return t.end()
458
+ const w = r.ast.items[0]
459
+ t.alike(w.doc, [' about '])
460
+ t.is(w.body.length, 1)
461
+ t.is(w.body[0].kind, 'word_ref')
462
+ t.is(w.body[0].name, 'id')
463
+ t.end()
464
+ })
465
+
466
+ test('AST: несколько top-level подряд сохраняют порядок в items', function (t) {
467
+ const src = '+M ./a.sail\n&Bool\n| True\n@nop ( -> )\n'
468
+ const r = parseSource(src)
469
+ t.ok(r.ok)
470
+ if (!assertSourceFileShell(t, r.ast)) return t.end()
471
+ t.is(r.ast.items.length, 3)
472
+ t.is(r.ast.items[0].kind, 'import')
473
+ t.is(r.ast.items[1].kind, 'sum_type')
474
+ t.is(r.ast.items[2].kind, 'word')
475
+ t.end()
476
+ })
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Public parse API shape — plan + RFC-0.1 §11 (Parse step).
3
+ */
4
+ import test from 'brittle'
5
+ import { parseSource } from '../../index.js'
6
+ import { expectAccept } from './helpers.js'
7
+
8
+ test('parseSource returns { ok, diagnostics }', function (t) {
9
+ const r = parseSource('')
10
+ t.ok(typeof r === 'object' && r !== null)
11
+ t.ok('ok' in r && typeof r.ok === 'boolean')
12
+ t.ok(Array.isArray(r.diagnostics))
13
+ t.end()
14
+ })
15
+
16
+ test('parseSource: success has empty diagnostics', function (t) {
17
+ expectAccept(t, '')
18
+ t.end()
19
+ })
20
+
21
+ test('parseSource: each diagnostic has string code and message', function (t) {
22
+ const r = parseSource('@x')
23
+ t.ok(r.ok === false)
24
+ t.ok(r.diagnostics.length >= 1)
25
+ for (const d of r.diagnostics) {
26
+ t.is(typeof d.code, 'string')
27
+ t.is(typeof d.message, 'string')
28
+ }
29
+ t.end()
30
+ })
31
+
32
+ test('parseSource: non-string input is rejected', function (t) {
33
+ const r = parseSource(null)
34
+ t.ok(r.ok === false)
35
+ t.ok(r.diagnostics.some(d => d.code === 'E1001'))
36
+ t.end()
37
+ })
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Золотой эталон: один большой .sail и ожидаемое AST в JSON (test/fixtures/).
3
+ * Обновить JSON после намеренного изменения парсера: `npm run regen:fixture-ast` (из каталога lang).
4
+ */
5
+ import test from 'brittle'
6
+ import { readFileSync } from 'fs'
7
+ import path from 'path'
8
+ import { fileURLToPath } from 'url'
9
+ import { parseSource } from '../../index.js'
10
+
11
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
12
+ const fixtureDir = path.join(__dirname, '..', 'fixtures')
13
+ const fixtureSailPath = path.join(fixtureDir, 'demo-full-syntax.sail')
14
+ const fixtureAstPath = path.join(fixtureDir, 'demo-full-syntax.ast.json')
15
+
16
+ test('fixture demo-full-syntax: parse matches demo-full-syntax.ast.json', function (t) {
17
+ const sourceText = readFileSync(fixtureSailPath, 'utf8')
18
+ const expected = JSON.parse(readFileSync(fixtureAstPath, 'utf8'))
19
+ const r = parseSource(sourceText)
20
+ t.ok(r.ok, 'fixture must parse without diagnostics')
21
+ t.is(r.diagnostics.length, 0)
22
+ t.alike(r.ast, expected)
23
+ t.end()
24
+ })
@@ -0,0 +1,27 @@
1
+ import { parseSource } from '../../lib/parse/index.js'
2
+
3
+ export { parseSource }
4
+
5
+ export function expectAccept (t, sourceText) {
6
+ const r = parseSource(sourceText)
7
+ t.ok(r.ok === true, 'expected ok')
8
+ t.ok(Array.isArray(r.diagnostics), 'diagnostics must be an array')
9
+ t.is(r.diagnostics.length, 0, 'expected no diagnostics on success')
10
+ }
11
+
12
+ export function expectReject (t, sourceText, code, messageIncludes) {
13
+ const r = parseSource(sourceText)
14
+ t.ok(r.ok === false, 'expected failure')
15
+ t.ok(Array.isArray(r.diagnostics), 'diagnostics must be an array')
16
+ const d = r.diagnostics.find(x => x.code === code)
17
+ t.ok(
18
+ d,
19
+ `expected a diagnostic with code ${code}, got ${r.diagnostics.map(x => x.code).join(', ')}`
20
+ )
21
+ if (d !== undefined && messageIncludes !== undefined) {
22
+ t.ok(
23
+ typeof d.message === 'string' && d.message.includes(messageIncludes),
24
+ `message should include ${JSON.stringify(messageIncludes)}, got ${JSON.stringify(d.message)}`
25
+ )
26
+ }
27
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Матрица L0 (лексер RFC-lex-0.1, RFC-conformance-0.1 §3): диагностики из [lexer.js](lang/lib/parse/lexer.js).
3
+ * Парсер (L1) добавляет E1003–E1007; см. l1-parse-diagnostics-matrix.test.js.
4
+ *
5
+ * | Код | Место в lexer.js | Смысл | Проверка |
6
+ * |-------|-------------------------|-------|----------|
7
+ * | E1002 | readCommentToken | Незакрытый `-- … --` | **ниже** (nextToken), l0-lex.test.js (parseSource) |
8
+ * | E1001 | readString | Незакрытая строка | **ниже** (nextToken), l1-parse-diagnostics-matrix (parseSource) |
9
+ * | E1001 | `/` без regexp/word_ref | Недопустимый `/` | l1-parse-diagnostics-matrix (parseSource) |
10
+ * | E1001 | fallback символ | Неизвестный символ | **ниже** (nextToken) |
11
+ *
12
+ * Путь импорта читается `readPathToken`, не как отдельный шаг `nextToken` для `./…` (см. l0-lex «import path» через parseSource).
13
+ * Интеграция через `parseSource` для путей, тел слов и т.д. — в l0-lex.test.js и l1-*.
14
+ */
15
+ import test from 'brittle'
16
+ import { createLexerState, nextToken } from '../../lib/parse/lexer.js'
17
+
18
+ function firstTokenDiagnostics (sourceText, regexpOk = false) {
19
+ const state = createLexerState(sourceText)
20
+ const ctx = { diagnostics: [], regexpOk }
21
+ const tok = nextToken(state, ctx)
22
+ return { tok, diagnostics: ctx.diagnostics }
23
+ }
24
+
25
+ test('L0 E1002: незакрытый блочный комментарий (только лексер)', function (t) {
26
+ const { tok, diagnostics } = firstTokenDiagnostics('-- без закрытия')
27
+ t.is(tok.type, 'EOF')
28
+ const d = diagnostics.find(x => x.code === 'E1002')
29
+ t.ok(d, 'E1002')
30
+ t.ok(d.message.includes('комментарий'))
31
+ t.end()
32
+ })
33
+
34
+ test('L0 E1001: неизвестный символ (fallback nextToken)', function (t) {
35
+ const { tok, diagnostics } = firstTokenDiagnostics('§')
36
+ t.is(tok.type, 'EOF')
37
+ const d = diagnostics.find(x => x.code === 'E1001')
38
+ t.ok(d && d.message.includes('§'))
39
+ t.end()
40
+ })
41
+
42
+ test('L0 E1001: незакрытая строка (только лексер)', function (t) {
43
+ const { tok, diagnostics } = firstTokenDiagnostics('"unclosed')
44
+ t.is(tok.type, 'EOF')
45
+ const d = diagnostics.find(x => x.code === 'E1001')
46
+ t.ok(d && d.message.includes('"'))
47
+ t.end()
48
+ })
49
+
50
+ test('L0: базовая токенизация MODULE и число (без диагностик)', function (t) {
51
+ const state = createLexerState('+Lib 42')
52
+ const ctx = { diagnostics: [], regexpOk: false }
53
+ const a = nextToken(state, ctx)
54
+ const b = nextToken(state, ctx)
55
+ t.is(a.type, 'MODULE')
56
+ t.is(b.type, 'NUMBER')
57
+ t.is(ctx.diagnostics.length, 0)
58
+ t.end()
59
+ })
@@ -0,0 +1,40 @@
1
+ /**
2
+ * L0 Lex — RFC-conformance-0.1 §3, RFC-lex-0.1 (ws, comments §3, literals §6, path §7, order §8).
3
+ * Diagnostics for lex/parse — RFC-0.1 §13 (E1001–E1007).
4
+ * Матрица диагностик лексера (E1001/E1002 из lexer.js): l0-lex-diagnostics-matrix.test.js
5
+ */
6
+ import test from 'brittle'
7
+ import { expectAccept, expectReject } from './helpers.js'
8
+
9
+ test('L0: whitespace-only file is valid (RFC-0.1 §12.2 source_file)', function (t) {
10
+ expectAccept(t, '')
11
+ expectAccept(t, ' \n\t \n')
12
+ t.end()
13
+ })
14
+
15
+ test('L0: unclosed Sail block comment → E1002 (RFC-0.1 §13, RFC-lex §3)', function (t) {
16
+ expectReject(
17
+ t,
18
+ '-- opens but never closes',
19
+ 'E1002',
20
+ "Незакрытый комментарий"
21
+ )
22
+ t.end()
23
+ })
24
+
25
+ test('L0: `--` inside string literal is not comment (RFC-lex §3, §6)', function (t) {
26
+ const src = '@w ( -> )\n\n "-- not a comment --"'
27
+ expectAccept(t, src)
28
+ t.end()
29
+ })
30
+
31
+ test('L0: import path is single token without internal whitespace (RFC-lex §7)', function (t) {
32
+ expectAccept(t, '+Lib ./relative/path.sail')
33
+ expectAccept(t, '+Npm @scope/pkg-name')
34
+ t.end()
35
+ })
36
+
37
+ test('L0: regexp vs division — regexp after token that allows InputElementRegExp (RFC-lex §6)', function (t) {
38
+ expectAccept(t, '@w ( -> )\n\n /just[^/]*/ dup')
39
+ t.end()
40
+ })