@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,276 @@
1
+ /**
2
+ * L2: декларации и скобочный импорт — E1101–E1106, E1108–E1111, E1113, E1204; E1110 тип в скобках.
3
+ *
4
+ * Матрица (остальные коды — в соседних файлах):
5
+ * - E1101–E1106, E1108–E1111, E1113 — этот файл
6
+ * - E1204: ~Lib/ghost (module_word_ref); ~Lib/Ghost в сигнатуре (module_type_ref)
7
+ * - E1109: скобки @hello vs локальный @hello; скобочный &T vs локальный &T; autogen от &V vs локальный &U
8
+ * - E1110 (@word в скобках) — l2-resolve-bracket-ffi.test.js; E1110 (&Type) — ниже
9
+ * - E1112, E1202 — l2-resolve-graph.test.js
10
+ * - E1201, E1203, E1205 — l2-resolve-single-file.test.js, l2-resolve-bracket-ffi.test.js
11
+ * - E1206 — l2-resolve-single-file.test.js (неизвестный builtin)
12
+ */
13
+ import test from 'brittle'
14
+ import path from 'node:path'
15
+ import { resolveSailNames } from '../../lib/names/resolve-sail.js'
16
+
17
+ const root = path.resolve('/virtual/sail-l2-decl-errs')
18
+ function vfs (files) {
19
+ const norm = Object.fromEntries(
20
+ Object.entries(files).map(([k, v]) => [path.normalize(k), v])
21
+ )
22
+ return (p) => norm[path.normalize(p)] ?? null
23
+ }
24
+
25
+ function hasCode (r, code) {
26
+ return r.diagnostics.some(d => d.code === code)
27
+ }
28
+
29
+ test('L2 E1101: два @foo в одном модуле', function (t) {
30
+ const main = path.join(root, 'e1101.sail')
31
+ const src = '@foo ( -> )\n@foo ( -> )\n\n'
32
+ const r = resolveSailNames({ entryPath: main, readFile: vfs({ [main]: src }) })
33
+ t.ok(r.ok === false)
34
+ t.ok(hasCode(r, 'E1101'))
35
+ t.end()
36
+ })
37
+
38
+ test('L2 E1102: два &T в одном модуле', function (t) {
39
+ const main = path.join(root, 'e1102.sail')
40
+ const src = [
41
+ '&T',
42
+ ' | A',
43
+ '&T',
44
+ ' | B'
45
+ ].join('\n')
46
+ const r = resolveSailNames({ entryPath: main, readFile: vfs({ [main]: src }) })
47
+ t.ok(r.ok === false)
48
+ t.ok(hasCode(r, 'E1102'))
49
+ t.end()
50
+ })
51
+
52
+ test('L2 E1103: одинаковый тег sum в двух типах модуля', function (t) {
53
+ const main = path.join(root, 'e1103.sail')
54
+ const src = [
55
+ '&U',
56
+ ' | Tag',
57
+ '&V',
58
+ ' | Tag'
59
+ ].join('\n')
60
+ const r = resolveSailNames({ entryPath: main, readFile: vfs({ [main]: src }) })
61
+ t.ok(r.ok === false)
62
+ t.ok(hasCode(r, 'E1103'))
63
+ t.end()
64
+ })
65
+
66
+ test('L2 E1104: дубликат поля product-типа', function (t) {
67
+ const main = path.join(root, 'e1104.sail')
68
+ const src = [
69
+ '&P',
70
+ ' :x Num',
71
+ ' :x Num'
72
+ ].join('\n')
73
+ const r = resolveSailNames({ entryPath: main, readFile: vfs({ [main]: src }) })
74
+ t.ok(r.ok === false)
75
+ t.ok(hasCode(r, 'E1104'))
76
+ t.end()
77
+ })
78
+
79
+ test('L2 E1105: @just конфликтует с autogen от &Maybe', function (t) {
80
+ const main = path.join(root, 'e1105.sail')
81
+ const src = [
82
+ '&Maybe',
83
+ ' | Nothing',
84
+ ' | Just Num',
85
+ '@just ( -> )',
86
+ ''
87
+ ].join('\n')
88
+ const r = resolveSailNames({ entryPath: main, readFile: vfs({ [main]: src }) })
89
+ t.ok(r.ok === false)
90
+ t.ok(hasCode(r, 'E1105'))
91
+ t.end()
92
+ })
93
+
94
+ test('L2 E1106: два @sail с одним @name в .js', function (t) {
95
+ const main = path.join(root, 'e1106.sail')
96
+ const js = path.join(root, 'e1106.js')
97
+ const sail = '+J ./e1106.js\n@main ( -> )\n\n'
98
+ const jsrc = [
99
+ '/**',
100
+ ' * @sail',
101
+ ' * @dup ( -> )',
102
+ ' */',
103
+ '/**',
104
+ ' * @sail',
105
+ ' * @dup ( -> )',
106
+ ' */',
107
+ 'export const x = 1'
108
+ ].join('\n')
109
+ const r = resolveSailNames({
110
+ entryPath: main,
111
+ readFile: vfs({ [main]: sail, [js]: jsrc })
112
+ })
113
+ t.ok(r.ok === false)
114
+ t.ok(hasCode(r, 'E1106'))
115
+ t.end()
116
+ })
117
+
118
+ test('L2 E1108: дубликат @x в списке скобочного импорта', function (t) {
119
+ const main = path.join(root, 'e1108.sail')
120
+ const lib = path.join(root, 'e1108-lib.sail')
121
+ const files = {
122
+ [main]: '+L ( @x @x ) ./e1108-lib.sail\n',
123
+ [lib]: '@x ( -> )\n\n'
124
+ }
125
+ const r = resolveSailNames({ entryPath: main, readFile: vfs(files) })
126
+ t.ok(r.ok === false)
127
+ t.ok(hasCode(r, 'E1108'))
128
+ t.end()
129
+ })
130
+
131
+ test('L2 E1109: скобочный @hello конфликтует с локальным @hello', function (t) {
132
+ const main = path.join(root, 'e1109.sail')
133
+ const lib = path.join(root, 'e1109-lib.sail')
134
+ const files = {
135
+ [main]: [
136
+ '@hello ( -> )',
137
+ '',
138
+ '+Lib ( @hello ) ./e1109-lib.sail',
139
+ ''
140
+ ].join('\n'),
141
+ [lib]: '@hello ( -> )\n\n'
142
+ }
143
+ const r = resolveSailNames({ entryPath: main, readFile: vfs(files) })
144
+ t.ok(r.ok === false)
145
+ t.ok(hasCode(r, 'E1109'))
146
+ t.end()
147
+ })
148
+
149
+ test('L2 E1110: скобочный &Ghost отсутствует в экспорте stub', function (t) {
150
+ const main = path.join(root, 'e1110type.sail')
151
+ const stub = path.join(root, 'e1110type-stub.sail')
152
+ const files = {
153
+ [main]: [
154
+ '+S ( @exported &Ghost ) ./e1110type-stub.sail',
155
+ '@main ( -> Num )',
156
+ ' 42'
157
+ ].join('\n'),
158
+ [stub]: '@exported ( -> Num )\n 5\n'
159
+ }
160
+ const r = resolveSailNames({ entryPath: main, readFile: vfs(files) })
161
+ t.ok(r.ok === false)
162
+ const d = r.diagnostics.find(x => x.code === 'E1110')
163
+ t.ok(d && d.message.includes('Ghost'))
164
+ t.end()
165
+ })
166
+
167
+ test('L2 E1111: два импорта с одним алиасом Lib', function (t) {
168
+ const main = path.join(root, 'e1111.sail')
169
+ const a = path.join(root, 'e1111-a.sail')
170
+ const b = path.join(root, 'e1111-b.sail')
171
+ const files = {
172
+ [main]: '+Lib ./e1111-a.sail\n+Lib ./e1111-b.sail\n',
173
+ [a]: '@x ( -> )\n\n',
174
+ [b]: '@y ( -> )\n\n'
175
+ }
176
+ const r = resolveSailNames({ entryPath: main, readFile: vfs(files) })
177
+ t.ok(r.ok === false)
178
+ t.ok(hasCode(r, 'E1111'))
179
+ t.end()
180
+ })
181
+
182
+ test('L2 E1113: невалидный фрагмент @sail в .js', function (t) {
183
+ const main = path.join(root, 'e1113.sail')
184
+ const js = path.join(root, 'e1113.js')
185
+ const sail = '+J ./e1113.js\n@main ( -> )\n\n'
186
+ const jsrc = [
187
+ '/**',
188
+ ' * @sail',
189
+ ' * ))))',
190
+ ' */',
191
+ 'export const x = 1'
192
+ ].join('\n')
193
+ const r = resolveSailNames({
194
+ entryPath: main,
195
+ readFile: vfs({ [main]: sail, [js]: jsrc })
196
+ })
197
+ t.ok(r.ok === false)
198
+ t.ok(hasCode(r, 'E1113'))
199
+ t.end()
200
+ })
201
+
202
+ test('L2 E1204: ~Lib/ghost при отсутствии члена в модуле', function (t) {
203
+ const main = path.join(root, 'e1204.sail')
204
+ const lib = path.join(root, 'e1204-lib.sail')
205
+ const files = {
206
+ [main]: '+Lib ./e1204-lib.sail\n@main ( -> )\n\n ~Lib/ghost\n',
207
+ [lib]: '@real ( -> )\n\n'
208
+ }
209
+ const r = resolveSailNames({ entryPath: main, readFile: vfs(files) })
210
+ t.ok(r.ok === false)
211
+ const d = r.diagnostics.find(x => x.code === 'E1204')
212
+ t.ok(d && d.message.includes('ghost'))
213
+ t.end()
214
+ })
215
+
216
+ test('L2 E1204: ~Lib/Ghost в сигнатуре — module_type_ref', function (t) {
217
+ const main = path.join(root, 'e1204-type.sail')
218
+ const lib = path.join(root, 'e1204-type-lib.sail')
219
+ const files = {
220
+ [main]: '+Lib ./e1204-type-lib.sail\n@m ( ~Lib/Ghost -> )\n\n',
221
+ [lib]: [
222
+ '&Point',
223
+ ' :x Num',
224
+ ' :y Num'
225
+ ].join('\n')
226
+ }
227
+ const r = resolveSailNames({ entryPath: main, readFile: vfs(files) })
228
+ t.ok(r.ok === false)
229
+ const d = r.diagnostics.find(x => x.code === 'E1204')
230
+ t.ok(d && d.message.includes('Ghost'))
231
+ t.end()
232
+ })
233
+
234
+ test('L2 E1109: скобочный &T при уже объявленном локальном &T', function (t) {
235
+ const main = path.join(root, 'e1109-type.sail')
236
+ const lib = path.join(root, 'e1109-type-lib.sail')
237
+ const files = {
238
+ [main]: [
239
+ '&T',
240
+ ' | X',
241
+ '+Lib ( &T ) ./e1109-type-lib.sail',
242
+ ''
243
+ ].join('\n'),
244
+ [lib]: [
245
+ '&T',
246
+ ' | A'
247
+ ].join('\n')
248
+ }
249
+ const r = resolveSailNames({ entryPath: main, readFile: vfs(files) })
250
+ t.ok(r.ok === false)
251
+ const d = r.diagnostics.find(x => x.code === 'E1109')
252
+ t.ok(d && d.message.includes('T'))
253
+ t.end()
254
+ })
255
+
256
+ test('L2 E1109: autogen импортированного &V конфликтует с autogen локального &U', function (t) {
257
+ const main = path.join(root, 'e1109-autogen.sail')
258
+ const lib = path.join(root, 'e1109-autogen-lib.sail')
259
+ const files = {
260
+ [main]: [
261
+ '&U',
262
+ ' | A',
263
+ '+Lib ( &V ) ./e1109-autogen-lib.sail',
264
+ ''
265
+ ].join('\n'),
266
+ [lib]: [
267
+ '&V',
268
+ ' | A'
269
+ ].join('\n')
270
+ }
271
+ const r = resolveSailNames({ entryPath: main, readFile: vfs(files) })
272
+ t.ok(r.ok === false)
273
+ const d = r.diagnostics.find(x => x.code === 'E1109')
274
+ t.ok(d && d.message.includes('a'))
275
+ t.end()
276
+ })
@@ -0,0 +1,105 @@
1
+ /**
2
+ * L2: несколько .sail, относительный path, ~Lib/w, цикл → E1112.
3
+ */
4
+ import test from 'brittle'
5
+ import path from 'node:path'
6
+ import { resolveSailNames } from '../../lib/names/resolve-sail.js'
7
+
8
+ const root = path.resolve('/virtual/sail-l2-graph')
9
+ function vfs (files) {
10
+ const norm = Object.fromEntries(
11
+ Object.entries(files).map(([k, v]) => [path.normalize(k), v])
12
+ )
13
+ return (p) => norm[path.normalize(p)] ?? null
14
+ }
15
+
16
+ test('L2 graph: ~Lib/hello резолвится при полном импорте', function (t) {
17
+ const main = path.join(root, 'main.sail')
18
+ const lib = path.join(root, 'lib.sail')
19
+ const files = {
20
+ [main]: [
21
+ '+Lib ./lib.sail',
22
+ '@main ( -> )',
23
+ '',
24
+ ' ~Lib/hello'
25
+ ].join('\n'),
26
+ [lib]: [
27
+ '@hello ( -> )',
28
+ '',
29
+ ''
30
+ ].join('\n')
31
+ }
32
+ const r = resolveSailNames({ entryPath: main, readFile: vfs(files) })
33
+ t.ok(r.ok, r.diagnostics.map(d => d.code + ' ' + d.message).join('; '))
34
+ t.end()
35
+ })
36
+
37
+ test('L2 graph: цикл a.sail ↔ b.sail → E1112', function (t) {
38
+ const a = path.join(root, 'a.sail')
39
+ const b = path.join(root, 'b.sail')
40
+ const files = {
41
+ [a]: '+B ./b.sail\n@x ( -> )\n\n',
42
+ [b]: '+A ./a.sail\n@y ( -> )\n\n'
43
+ }
44
+ const r = resolveSailNames({ entryPath: a, readFile: vfs(files) })
45
+ t.ok(r.ok === false)
46
+ const d = r.diagnostics.find(x => x.code === 'E1112')
47
+ t.ok(d, 'E1112')
48
+ t.ok(d.message.includes('Циклическая'))
49
+ t.end()
50
+ })
51
+
52
+ test('L2 graph: импорт только .js не даёт рёбер цикла sail→sail', function (t) {
53
+ const main = path.join(root, 'main.sail')
54
+ const stub = path.join(root, 'stub.js')
55
+ const files = {
56
+ [main]: '+J ./stub.js\n@main ( -> )\n\n',
57
+ [stub]: 'export const x = 1\n'
58
+ }
59
+ const r = resolveSailNames({ entryPath: main, readFile: vfs(files) })
60
+ t.ok(r.ok, r.diagnostics.map(d => d.message).join('; '))
61
+ t.end()
62
+ })
63
+
64
+ test('L2 graph: два модуля экспортируют одно слово — /foo → E1202', function (t) {
65
+ const main = path.join(root, 'main.sail')
66
+ const l1 = path.join(root, 'l1.sail')
67
+ const l2 = path.join(root, 'l2.sail')
68
+ const files = {
69
+ [main]: [
70
+ '+L1 ./l1.sail',
71
+ '+L2 ./l2.sail',
72
+ '@main ( -> )',
73
+ '',
74
+ ' /foo'
75
+ ].join('\n'),
76
+ [l1]: '@foo ( -> )\n\n',
77
+ [l2]: '@foo ( -> )\n\n'
78
+ }
79
+ const r = resolveSailNames({ entryPath: main, readFile: vfs(files) })
80
+ t.ok(r.ok === false)
81
+ const d = r.diagnostics.find(x => x.code === 'E1202')
82
+ t.ok(d && d.message.includes('foo'))
83
+ t.end()
84
+ })
85
+
86
+ test('L2 graph: ~Lib/тип в сигнатуре — module_type_ref', function (t) {
87
+ const main = path.join(root, 'main.sail')
88
+ const lib = path.join(root, 'lib.sail')
89
+ const files = {
90
+ [main]: [
91
+ '+Lib ./lib.sail',
92
+ '@w ( ~Lib/Point -> )',
93
+ '',
94
+ ''
95
+ ].join('\n'),
96
+ [lib]: [
97
+ '&Point',
98
+ ' :x Num',
99
+ ' :y Num'
100
+ ].join('\n')
101
+ }
102
+ const r = resolveSailNames({ entryPath: main, readFile: vfs(files) })
103
+ t.ok(r.ok, r.diagnostics.map(d => d.message).join('; '))
104
+ t.end()
105
+ })
@@ -0,0 +1,79 @@
1
+ /**
2
+ * L2: один .sail без импортов — локальные @, builtin, E1201 (RFC-0.1 §11).
3
+ */
4
+ import test from 'brittle'
5
+ import path from 'node:path'
6
+ import { resolveSailNames } from '../../lib/names/resolve-sail.js'
7
+
8
+ const root = path.resolve('/virtual/sail-l2')
9
+ function vfs (files) {
10
+ const norm = Object.fromEntries(
11
+ Object.entries(files).map(([k, v]) => [path.normalize(k), v])
12
+ )
13
+ return (p) => norm[path.normalize(p)] ?? null
14
+ }
15
+
16
+ test('L2 single: вызов локального слова через /name', function (t) {
17
+ const main = path.join(root, 'main.sail')
18
+ const src = [
19
+ '@noop ( -> )',
20
+ '',
21
+ ' ',
22
+ '@main ( -> )',
23
+ '',
24
+ ' /noop'
25
+ ].join('\n')
26
+ const r = resolveSailNames({ entryPath: main, readFile: vfs({ [main]: src }) })
27
+ t.ok(r.ok, r.diagnostics.map(d => d.code + ' ' + d.message).join('; '))
28
+ t.end()
29
+ })
30
+
31
+ test('L2 single: builtin в теле (dup) — без word_ref', function (t) {
32
+ const main = path.join(root, 'main.sail')
33
+ const src = '@main ( Num -> Num Num )\n\n dup\n'
34
+ const r = resolveSailNames({ entryPath: main, readFile: vfs({ [main]: src }) })
35
+ t.ok(r.ok, r.diagnostics.map(d => d.message).join('; '))
36
+ t.end()
37
+ })
38
+
39
+ test('L2 single: неизвестное /id → E1201', function (t) {
40
+ const main = path.join(root, 'main.sail')
41
+ const src = '@main ( -> )\n\n /nope\n'
42
+ const r = resolveSailNames({ entryPath: main, readFile: vfs({ [main]: src }) })
43
+ t.ok(r.ok === false)
44
+ const d = r.diagnostics.find(x => x.code === 'E1201')
45
+ t.ok(d, 'E1201')
46
+ t.ok(d.message.includes('nope'))
47
+ t.end()
48
+ })
49
+
50
+ test('L2 single: ~M/w без импорта → E1203', function (t) {
51
+ const main = path.join(root, 'main.sail')
52
+ const src = '@main ( -> )\n\n ~Net/fetch\n'
53
+ const r = resolveSailNames({ entryPath: main, readFile: vfs({ [main]: src }) })
54
+ t.ok(r.ok === false)
55
+ const d = r.diagnostics.find(x => x.code === 'E1203')
56
+ t.ok(d && d.message.includes('Net'))
57
+ t.end()
58
+ })
59
+
60
+ test('L2 single: неизвестное lower в теле → E1206 (не E1001 на парсе)', function (t) {
61
+ const main = path.join(root, 'main.sail')
62
+ const src = '@main ( -> )\n\n not_a_real_builtin_xyz\n'
63
+ const r = resolveSailNames({ entryPath: main, readFile: vfs({ [main]: src }) })
64
+ t.ok(r.ok === false)
65
+ const d = r.diagnostics.find(x => x.code === 'E1206')
66
+ t.ok(d && d.message.includes('not_a_real_builtin_xyz'))
67
+ t.end()
68
+ })
69
+
70
+ test('L2 single: symbol_tables содержит экспорт слов файла', function (t) {
71
+ const main = path.join(root, 'main.sail')
72
+ const src = '@a ( -> )\n\n /a\n'
73
+ const r = resolveSailNames({ entryPath: main, readFile: vfs({ [main]: src }) })
74
+ t.ok(r.ok)
75
+ const tab = r.symbolTables?.get(path.normalize(main))
76
+ t.ok(tab)
77
+ t.ok(tab.words.includes('a'))
78
+ t.end()
79
+ })
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Проверка контракта [lang/lib/parse/ast-spec.js](lang/lib/parse/ast-spec.js): AST_KINDS и согласованность с JSDoc-описанием узлов.
3
+ */
4
+ import test from 'brittle'
5
+ import { AST_KINDS } from '../../lib/parse/ast-spec.js'
6
+
7
+ test('AST_KINDS: frozen, непустой, без дубликатов', function (t) {
8
+ t.ok(Object.isFrozen(AST_KINDS))
9
+ t.ok(AST_KINDS.length > 0)
10
+ const set = new Set(AST_KINDS)
11
+ t.is(set.size, AST_KINDS.length, 'duplicate kind strings')
12
+ for (const k of AST_KINDS) {
13
+ t.ok(typeof k === 'string' && k.length > 0, `kind ${JSON.stringify(k)}`)
14
+ }
15
+ t.end()
16
+ })
17
+
18
+ test('AST_KINDS: обязательные kind из RFC-0.1 п.12.2 (топ-уровень и ядро)', function (t) {
19
+ const required = [
20
+ 'source_file',
21
+ 'import',
22
+ 'sum_type',
23
+ 'product_type',
24
+ 'word',
25
+ 'signature',
26
+ 'sig_effect_add',
27
+ 'sig_effect_remove',
28
+ 'stack_var',
29
+ 'quotation_sig',
30
+ 'named_quotation_sig',
31
+ 'sig_type_expr',
32
+ 'type_name',
33
+ 'type_var',
34
+ 'module_type_ref',
35
+ 'paren_type',
36
+ 'quotation_type',
37
+ 'type_app',
38
+ 'literal',
39
+ 'word_ref',
40
+ 'module_word_ref',
41
+ 'builtin',
42
+ 'quotation',
43
+ 'list_literal',
44
+ 'slot_write',
45
+ 'slot_read'
46
+ ]
47
+ for (const k of required) {
48
+ t.ok(AST_KINDS.includes(k), `missing kind: ${k}`)
49
+ }
50
+ t.end()
51
+ })
52
+
53
+ test('AST_KINDS: число записей совпадает с ожидаемым набором (ловит пропуски при правках)', function (t) {
54
+ t.is(AST_KINDS.length, 26)
55
+ t.end()
56
+ })