@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,90 @@
1
+ /**
2
+ * L4 фаза 2: lowerBodySteps / irSteps в module (RFC-IR section 5).
3
+ */
4
+ import test from 'brittle'
5
+ import path from 'node:path'
6
+ import { typecheckSail } from '../../index.js'
7
+ import {
8
+ assertJsonSerializable,
9
+ buildModuleIr,
10
+ irBodyStepNodeId,
11
+ lowerBodySteps
12
+ } from '../../lib/ir/index.js'
13
+
14
+ const root = path.resolve('/virtual/sail-ir-phase2')
15
+ function vfs (files) {
16
+ const norm = Object.fromEntries(
17
+ Object.entries(files).map(([k, v]) => [path.normalize(k), v])
18
+ )
19
+ return (p) => norm[path.normalize(p)] ?? null
20
+ }
21
+
22
+ test('lowerBodySteps: один литерал', function (t) {
23
+ const main = path.join(root, 'lit.sail')
24
+ const src = ['@w ( -> Num )', '', ' 42', ''].join('\n')
25
+ const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
26
+ t.ok(r.ok, r.diagnostics?.map((d) => d.message).join('; '))
27
+ const norm = path.normalize(main)
28
+ const word = r.env.wordDeclByPath.get(norm).get('w')
29
+ const steps = lowerBodySteps(norm, 'w', word.body)
30
+ t.is(steps.length, 1)
31
+ t.is(steps[0].kind, 'Literal')
32
+ t.is(steps[0].litKind, 'number')
33
+ t.is(steps[0].raw, '42')
34
+ t.ok(steps[0].nodeId.includes('#w#0'))
35
+ t.ok(steps[0].span && steps[0].span.start)
36
+ assertJsonSerializable(steps)
37
+ t.end()
38
+ })
39
+
40
+ test('lowerBodySteps: dup и drop', function (t) {
41
+ const main = path.join(root, 'dupdrop.sail')
42
+ const src = ['@w ( Num -> Num )', '', ' dup', '', ' drop', ''].join('\n')
43
+ const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
44
+ t.ok(r.ok, r.diagnostics?.map((d) => d.message).join('; '))
45
+ const norm = path.normalize(main)
46
+ const word = r.env.wordDeclByPath.get(norm).get('w')
47
+ const steps = lowerBodySteps(norm, 'w', word.body)
48
+ t.is(steps.length, 2)
49
+ t.is(steps[0].kind, 'Builtin')
50
+ t.is(steps[0].name, 'dup')
51
+ t.is(steps[1].kind, 'Builtin')
52
+ t.is(steps[1].name, 'drop')
53
+ t.is(steps[0].nodeId, irBodyStepNodeId(norm, 'w', [0]))
54
+ t.is(steps[1].nodeId, irBodyStepNodeId(norm, 'w', [1]))
55
+ assertJsonSerializable(steps)
56
+ t.end()
57
+ })
58
+
59
+ test('lowerBodySteps: quotation с литералом внутри', function (t) {
60
+ const main = path.join(root, 'quote.sail')
61
+ const src = ['@w ( -> ( -> Num ) )', '', ' ( 1 )', ''].join('\n')
62
+ const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
63
+ t.ok(r.ok, r.diagnostics?.map((d) => d.message).join('; '))
64
+ const norm = path.normalize(main)
65
+ const word = r.env.wordDeclByPath.get(norm).get('w')
66
+ const steps = lowerBodySteps(norm, 'w', word.body)
67
+ t.is(steps.length, 1)
68
+ t.is(steps[0].kind, 'Quotation')
69
+ const inner = steps[0].steps
70
+ t.ok(Array.isArray(inner) && inner.length === 1)
71
+ t.is(inner[0].kind, 'Literal')
72
+ t.is(inner[0].litKind, 'number')
73
+ t.is(inner[0].nodeId, irBodyStepNodeId(norm, 'w', [0, 0]))
74
+ assertJsonSerializable(steps)
75
+ t.end()
76
+ })
77
+
78
+ test('buildModuleIr: irSteps у слова', function (t) {
79
+ const main = path.join(root, 'module-irsteps.sail')
80
+ const src = ['@w ( -> Num )', '', ' 7', ''].join('\n')
81
+ const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
82
+ t.ok(r.ok)
83
+ const payload = buildModuleIr(r.env, main, { moduleStatus: 'ok' })
84
+ const wEntry = payload.words.find((x) => x.name === 'w')
85
+ t.ok(wEntry?.irSteps)
86
+ t.is(wEntry.irSteps.length, 1)
87
+ t.is(wEntry.irSteps[0].kind, 'Literal')
88
+ assertJsonSerializable(payload)
89
+ t.end()
90
+ })
@@ -0,0 +1,42 @@
1
+ /**
2
+ * L4 фаза 6: резолв импортов и квалифицированных вызовов (RFC-IR section 7).
3
+ */
4
+ import test from 'brittle'
5
+ import path from 'node:path'
6
+ import { typecheckSail } from '../../index.js'
7
+ import { assertJsonSerializable, buildModuleIr } from '../../lib/ir/index.js'
8
+
9
+ const root = path.resolve('/virtual/sail-ir-phase6')
10
+ function vfs (files) {
11
+ const norm = Object.fromEntries(
12
+ Object.entries(files).map(([k, v]) => [path.normalize(k), v])
13
+ )
14
+ return (p) => norm[path.normalize(p)] ?? null
15
+ }
16
+
17
+ test('фаза 6: импорт + ~Lib/hello — resolvedPath и resolvedModulePath', function (t) {
18
+ const main = path.join(root, 'main.sail')
19
+ const lib = path.join(root, 'lib.sail')
20
+ const files = {
21
+ [main]: ['+Lib ./lib.sail', '@main ( -> )', '', ' ~Lib/hello'].join('\n'),
22
+ [lib]: ['@hello ( -> )', '', ''].join('\n')
23
+ }
24
+ const r = typecheckSail({ entryPath: main, readFile: vfs(files) })
25
+ t.ok(r.ok, r.diagnostics?.map((d) => d.message).join('; '))
26
+ const normMain = path.normalize(main)
27
+ const normLib = path.normalize(lib)
28
+ const payload = buildModuleIr(r.env, normMain, { moduleStatus: 'ok' })
29
+ t.is(payload.imports.length, 1)
30
+ t.is(payload.imports[0].module, 'Lib')
31
+ t.is(payload.imports[0].resolvedPath, normLib)
32
+ const mainWord = payload.words.find((w) => w.name === 'main')
33
+ t.ok(mainWord?.irSteps?.length === 1)
34
+ const step = mainWord.irSteps[0]
35
+ t.is(step.kind, 'Word')
36
+ t.is(step.ref, 'qualified')
37
+ t.is(step.module, 'Lib')
38
+ t.is(step.word, 'hello')
39
+ t.is(step.resolvedModulePath, normLib)
40
+ assertJsonSerializable(payload)
41
+ t.end()
42
+ })
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Ранняя фаза L4: модель JSON-сериализации IR (lib/ir/serialize.js).
3
+ */
4
+ import test from 'brittle'
5
+ import { IR_SCHEMA_VERSION } from '../../lib/ir/schema-version.js'
6
+ import { assertJsonSerializable } from '../../lib/ir/assert-json-serializable.js'
7
+ import {
8
+ createSerializedIrDocument as wrap,
9
+ unwrapSerializedIrDocument as unwrap,
10
+ unwrapSerializedIrDocumentFull as unwrapFull,
11
+ stringifyIrDocument as stringify,
12
+ parseIrDocument as parse,
13
+ parseIrDocumentFull as parseFull
14
+ } from '../../lib/ir/serialize.js'
15
+
16
+ test('IR_SCHEMA_VERSION — строка semver-подобная', function (t) {
17
+ t.ok(typeof IR_SCHEMA_VERSION === 'string' && IR_SCHEMA_VERSION.length > 0)
18
+ t.end()
19
+ })
20
+
21
+ test('createSerializedIrDocument: конверт с module и irSchemaVersion', function (t) {
22
+ const doc = wrap({ path: '/x.sail', words: [] })
23
+ t.is(doc.irSchemaVersion, IR_SCHEMA_VERSION)
24
+ t.ok(doc.module && typeof doc.module === 'object')
25
+ t.is(doc.module.path, '/x.sail')
26
+ t.ok(Array.isArray(doc.module.words))
27
+ t.ok(doc.views === undefined && doc.meta === undefined)
28
+ t.end()
29
+ })
30
+
31
+ test('createSerializedIrDocument: views и meta опциональны', function (t) {
32
+ const views = { stepOrder: ['m#w#0', 'm#w#1'] }
33
+ const meta = { producer: { name: 'test', version: '0' } }
34
+ const doc = wrap({ k: 1 }, { views, meta })
35
+ t.alike(doc.views, views)
36
+ t.alike(doc.meta, meta)
37
+ t.end()
38
+ })
39
+
40
+ test('createSerializedIrDocument: views не массив', function (t) {
41
+ t.exception(() => wrap({}, { views: [] }), /options.views/)
42
+ t.end()
43
+ })
44
+
45
+ test('unwrapSerializedIrDocument: извлечение module', function (t) {
46
+ const inner = unwrap(wrap({ k: 1 }))
47
+ t.is(inner.k, 1)
48
+ t.end()
49
+ })
50
+
51
+ test('unwrapSerializedIrDocumentFull: module, views, meta', function (t) {
52
+ const views = { stepOrder: ['a#w#0'] }
53
+ const meta = { producer: { name: 'p' } }
54
+ const full = unwrapFull(wrap({ x: 2 }, { views, meta }))
55
+ t.is(full.module.x, 2)
56
+ t.alike(full.views, views)
57
+ t.alike(full.meta, meta)
58
+ t.end()
59
+ })
60
+
61
+ test('unwrapSerializedIrDocumentFull: без views/meta — undefined', function (t) {
62
+ const full = unwrapFull(wrap({}))
63
+ t.ok(full.module && typeof full.module === 'object')
64
+ t.is(full.views, undefined)
65
+ t.is(full.meta, undefined)
66
+ t.end()
67
+ })
68
+
69
+ test('unwrapSerializedIrDocument: неизвестный ключ корня не мешает', function (t) {
70
+ const inner = unwrap({
71
+ irSchemaVersion: IR_SCHEMA_VERSION,
72
+ module: { ok: true },
73
+ futureField: [1, 2, 3]
74
+ })
75
+ t.is(inner.ok, true)
76
+ t.end()
77
+ })
78
+
79
+ test('unwrapSerializedIrDocument: неверная версия', function (t) {
80
+ t.exception(
81
+ () =>
82
+ unwrap({
83
+ irSchemaVersion: '0.0.0',
84
+ module: {}
85
+ }),
86
+ /неподдерживаемая irSchemaVersion/
87
+ )
88
+ t.end()
89
+ })
90
+
91
+ test('unwrapSerializedIrDocument: не объект / нет module', function (t) {
92
+ t.exception(() => unwrap(null), /ожидался объект/)
93
+ t.exception(
94
+ () => unwrap({ irSchemaVersion: IR_SCHEMA_VERSION, module: [] }),
95
+ /module должно быть объектом/
96
+ )
97
+ t.end()
98
+ })
99
+
100
+ test('unwrapSerializedIrDocumentFull: views не массив', function (t) {
101
+ t.exception(
102
+ () =>
103
+ unwrapFull({
104
+ irSchemaVersion: IR_SCHEMA_VERSION,
105
+ module: {},
106
+ views: []
107
+ }),
108
+ /views должно быть объектом/
109
+ )
110
+ t.end()
111
+ })
112
+
113
+ test('unwrapSerializedIrDocumentFull: meta не массив', function (t) {
114
+ t.exception(
115
+ () =>
116
+ unwrapFull({
117
+ irSchemaVersion: IR_SCHEMA_VERSION,
118
+ module: {},
119
+ meta: []
120
+ }),
121
+ /meta должно быть объектом/
122
+ )
123
+ t.end()
124
+ })
125
+
126
+ test('stringifyIrDocument / parseIrDocument — round-trip', function (t) {
127
+ const payload = { modulePath: '/a.sail', exports: { words: ['main'] } }
128
+ const text = stringify(payload)
129
+ t.ok(text.includes('irSchemaVersion'))
130
+ const back = parse(text)
131
+ t.alike(back, payload)
132
+ t.end()
133
+ })
134
+
135
+ test('stringifyIrDocument / parseIrDocumentFull — views лёгкий, module с фиктивным quote', function (t) {
136
+ const modulePayload = {
137
+ steps: [
138
+ {
139
+ nodeId: 'f#w#0',
140
+ span: { start: { offset: 0, line: 1, column: 1 } },
141
+ preTypes: [],
142
+ postTypes: [
143
+ {
144
+ kind: 'quote',
145
+ inner: { steps: [] }
146
+ }
147
+ ]
148
+ }
149
+ ]
150
+ }
151
+ const views = { stepOrder: ['f#w#0'] }
152
+ const text = stringify(modulePayload, { views })
153
+ const full = parseFull(text)
154
+ t.alike(full.module, modulePayload)
155
+ t.alike(full.views, views)
156
+ t.is(full.meta, undefined)
157
+ t.end()
158
+ })
159
+
160
+ test('assertJsonSerializable: ок для plain объекта', function (t) {
161
+ assertJsonSerializable({ a: 1, b: [true, null] })
162
+ t.pass()
163
+ t.end()
164
+ })
165
+
166
+ test('assertJsonSerializable: undefined в объекте — ошибка', function (t) {
167
+ t.exception(
168
+ () => assertJsonSerializable({ a: undefined }),
169
+ /undefined/
170
+ )
171
+ t.end()
172
+ })
@@ -0,0 +1,74 @@
1
+ /**
2
+ * L4 фаза 3: stitch-types + типы на irSteps.
3
+ */
4
+ import test from 'brittle'
5
+ import path from 'node:path'
6
+ import { typecheckSail } from '../../index.js'
7
+ import {
8
+ assertJsonSerializable,
9
+ buildModuleIr,
10
+ verifyWireStackSnapshot
11
+ } from '../../lib/ir/index.js'
12
+
13
+ const root = path.resolve('/virtual/sail-ir-phase3')
14
+ function vfs (files) {
15
+ const norm = Object.fromEntries(
16
+ Object.entries(files).map(([k, v]) => [path.normalize(k), v])
17
+ )
18
+ return (p) => norm[path.normalize(p)] ?? null
19
+ }
20
+
21
+ test('фаза 3: irSteps с preTypes/postTypes и verifyWireStackSnapshot', function (t) {
22
+ const main = path.join(root, 'dup.sail')
23
+ const src = ['@w ( -> Num Num )', '', ' 1', '', ' dup', ''].join('\n')
24
+ const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
25
+ t.ok(r.ok, r.diagnostics?.map((d) => d.message).join('; '))
26
+ const norm = path.normalize(main)
27
+ const payload = buildModuleIr(r.env, norm, { moduleStatus: 'ok' })
28
+ const w = payload.words.find((x) => x.name === 'w')
29
+ t.ok(w?.irSteps?.length === 2)
30
+ t.ok(Array.isArray(w.irSteps[0].preTypes))
31
+ t.ok(Array.isArray(w.irSteps[0].postTypes))
32
+ t.is(w.irSteps[0].postTypes[0].kind, 'prim')
33
+ t.ok(Array.isArray(w.irSteps[1].preTypes))
34
+ t.ok(Array.isArray(w.irSteps[1].postTypes))
35
+ t.ok(verifyWireStackSnapshot(w.stackSteps))
36
+ assertJsonSerializable(payload)
37
+ t.end()
38
+ })
39
+
40
+ test('фаза 3: вложенная quotation — типы внутри и verify', function (t) {
41
+ const main = path.join(root, 'nested.sail')
42
+ const src = ['@w ( -> ( -> Num ) )', '', ' ( 1 )', ''].join('\n')
43
+ const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
44
+ t.ok(r.ok, r.diagnostics?.map((d) => d.message).join('; '))
45
+ const norm = path.normalize(main)
46
+ const payload = buildModuleIr(r.env, norm, { moduleStatus: 'ok' })
47
+ const w = payload.words.find((x) => x.name === 'w')
48
+ const q = w.irSteps[0]
49
+ t.is(q.kind, 'Quotation')
50
+ t.ok(Array.isArray(q.preTypes))
51
+ t.ok(Array.isArray(q.postTypes))
52
+ const inner = q.steps[0]
53
+ t.is(inner.kind, 'Literal')
54
+ t.ok(Array.isArray(inner.preTypes))
55
+ t.ok(Array.isArray(inner.postTypes))
56
+ t.ok(verifyWireStackSnapshot(w.stackSteps))
57
+ assertJsonSerializable(payload)
58
+ t.end()
59
+ })
60
+
61
+ test('фаза 3: ошибка в теле — preTypes на шаге сбоя, verify не обязан true', function (t) {
62
+ const main = path.join(root, 'fail.sail')
63
+ const src = ['@bad ( -> )', '', ' drop', ''].join('\n')
64
+ const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
65
+ t.ok(r.ok === false)
66
+ const norm = path.normalize(main)
67
+ const payload = buildModuleIr(r.env, norm, { moduleStatus: 'error' })
68
+ const bad = payload.words.find((x) => x.name === 'bad')
69
+ t.ok(bad?.irSteps?.length === 1)
70
+ t.ok(Array.isArray(bad.irSteps[0].preTypes))
71
+ t.ok(bad.irSteps[0].postTypes === undefined)
72
+ assertJsonSerializable(payload)
73
+ t.end()
74
+ })
@@ -0,0 +1,155 @@
1
+ /**
2
+ * L2: ADT — autogen не в symbol_tables.words (только типы и @); квалификация ~M/w; реэкспорт &Type.
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-adt')
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 ADT: symbol_tables — тип Maybe, autogen не в words; тело /just ok', function (t) {
17
+ const main = path.join(root, 'maybe.sail')
18
+ const src = [
19
+ '&Maybe a',
20
+ ' | Nothing',
21
+ ' | Just a',
22
+ '@t ( -> )',
23
+ '',
24
+ ' /nothing',
25
+ ' /just',
26
+ ' /maybe'
27
+ ].join('\n')
28
+ const r = resolveSailNames({ entryPath: main, readFile: vfs({ [main]: src }) })
29
+ t.ok(r.ok, r.diagnostics.map(d => d.message).join('; '))
30
+ const tab = r.symbolTables?.get(path.normalize(main))
31
+ t.ok(tab)
32
+ t.ok(tab.types.includes('Maybe'))
33
+ t.ok(tab.words.includes('t'), 'только явные @ в words')
34
+ for (const w of ['just', 'nothing', 'maybe']) {
35
+ t.ok(!tab.words.includes(w), `autogen ${w} не экспортируется в words`)
36
+ }
37
+ t.end()
38
+ })
39
+
40
+ test('L2 ADT: product Point — тип в types, autogen не в words', function (t) {
41
+ const main = path.join(root, 'point.sail')
42
+ const src = [
43
+ '&Point',
44
+ ' :x Num',
45
+ ' :y Num',
46
+ '@t ( -> )',
47
+ '',
48
+ ' /point',
49
+ ' /xPoint',
50
+ ' /withPoint'
51
+ ].join('\n')
52
+ const r = resolveSailNames({ entryPath: main, readFile: vfs({ [main]: src }) })
53
+ t.ok(r.ok, r.diagnostics.map(d => d.message).join('; '))
54
+ const tab = r.symbolTables?.get(path.normalize(main))
55
+ t.ok(tab.types.includes('Point'))
56
+ t.ok(tab.words.includes('t'))
57
+ for (const w of ['point', 'xPoint', 'yPoint', 'withPoint']) {
58
+ t.ok(!tab.words.includes(w), `autogen ${w} не в words`)
59
+ }
60
+ t.end()
61
+ })
62
+
63
+ test('L2 ADT: ~Lib/just — module_word_ref; у Lib autogen не в words', function (t) {
64
+ const main = path.join(root, 'main.sail')
65
+ const lib = path.join(root, 'lib.sail')
66
+ const files = {
67
+ [main]: [
68
+ '+Lib ./lib.sail',
69
+ '@m ( -> )',
70
+ '',
71
+ ' ~Lib/just'
72
+ ].join('\n'),
73
+ [lib]: [
74
+ '&Maybe a',
75
+ ' | Nothing',
76
+ ' | Just a'
77
+ ].join('\n')
78
+ }
79
+ const r = resolveSailNames({ entryPath: main, readFile: vfs(files) })
80
+ t.ok(r.ok, r.diagnostics.map(d => d.message).join('; '))
81
+ const libTab = r.symbolTables?.get(path.normalize(lib))
82
+ t.ok(libTab?.types.includes('Maybe'))
83
+ t.ok(!libTab?.words.includes('just'), 'just не в export words Lib')
84
+ t.end()
85
+ })
86
+
87
+ test('L2 ADT: скобочный &Maybe — /just локально', function (t) {
88
+ const main = path.join(root, 'main.sail')
89
+ const lib = path.join(root, 'lib.sail')
90
+ const files = {
91
+ [main]: [
92
+ '+Lib ( &Maybe ) ./lib.sail',
93
+ '@m ( -> )',
94
+ '',
95
+ ' /just'
96
+ ].join('\n'),
97
+ [lib]: [
98
+ '&Maybe a',
99
+ ' | Nothing',
100
+ ' | Just a'
101
+ ].join('\n')
102
+ }
103
+ const r = resolveSailNames({ entryPath: main, readFile: vfs(files) })
104
+ t.ok(r.ok, r.diagnostics.map(d => d.message).join('; '))
105
+ t.end()
106
+ })
107
+
108
+ test('L2 ADT: реэкспорт &Maybe — R.types содержит Maybe; ~R/just', function (t) {
109
+ const consumer = path.join(root, 'consumer.sail')
110
+ const reexport = path.join(root, 'reexport.sail')
111
+ const lib = path.join(root, 'lib.sail')
112
+ const files = {
113
+ [lib]: [
114
+ '&Maybe a',
115
+ ' | Nothing',
116
+ ' | Just a'
117
+ ].join('\n'),
118
+ [reexport]: '+Lib ( &Maybe ) ./lib.sail\n',
119
+ [consumer]: [
120
+ '+R ./reexport.sail',
121
+ '@use ( -> )',
122
+ '',
123
+ ' ~R/just'
124
+ ].join('\n')
125
+ }
126
+ const r = resolveSailNames({ entryPath: consumer, readFile: vfs(files) })
127
+ t.ok(r.ok, r.diagnostics.map(d => d.message).join('; '))
128
+ const rTab = r.symbolTables?.get(path.normalize(reexport))
129
+ t.ok(rTab?.types.includes('Maybe'), 'реэкспорт типа в symbol_tables')
130
+ t.ok(!rTab?.words.includes('just'))
131
+ t.end()
132
+ })
133
+
134
+ test('L2 ADT: consumer со скобочным &Maybe от реэкспортёра — /just', function (t) {
135
+ const consumer = path.join(root, 'consumer2.sail')
136
+ const reexport = path.join(root, 'reexport2.sail')
137
+ const lib = path.join(root, 'lib2.sail')
138
+ const files = {
139
+ [lib]: [
140
+ '&Maybe a',
141
+ ' | Nothing',
142
+ ' | Just a'
143
+ ].join('\n'),
144
+ [reexport]: '+Lib ( &Maybe ) ./lib2.sail\n',
145
+ [consumer]: [
146
+ '+R ( &Maybe ) ./reexport2.sail',
147
+ '@use ( -> )',
148
+ '',
149
+ ' /just'
150
+ ].join('\n')
151
+ }
152
+ const r = resolveSailNames({ entryPath: consumer, readFile: vfs(files) })
153
+ t.ok(r.ok, r.diagnostics.map(d => d.message).join('; '))
154
+ t.end()
155
+ })
@@ -0,0 +1,108 @@
1
+ /**
2
+ * L2: скобочный импорт, E1205, E1110; виртуальный модуль из .js (FFI).
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-bracket')
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 bracket: внесённое @hello — /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 ( @hello ) ./lib.sail',
22
+ '@main ( -> )',
23
+ '',
24
+ ' /hello'
25
+ ].join('\n'),
26
+ [lib]: '@hello ( -> )\n\n'
27
+ }
28
+ const r = resolveSailNames({ entryPath: main, readFile: vfs(files) })
29
+ t.ok(r.ok, r.diagnostics.map(d => d.message).join('; '))
30
+ t.end()
31
+ })
32
+
33
+ test('L2: полный импорт без скобок — /hello требует квалификации → E1205', function (t) {
34
+ const main = path.join(root, 'main.sail')
35
+ const lib = path.join(root, 'lib.sail')
36
+ const files = {
37
+ [main]: [
38
+ '+Lib ./lib.sail',
39
+ '@main ( -> )',
40
+ '',
41
+ ' /hello'
42
+ ].join('\n'),
43
+ [lib]: '@hello ( -> )\n\n'
44
+ }
45
+ const r = resolveSailNames({ entryPath: main, readFile: vfs(files) })
46
+ t.ok(r.ok === false)
47
+ const d = r.diagnostics.find(x => x.code === 'E1205')
48
+ t.ok(d && d.message.includes('hello') && d.message.includes('Lib'))
49
+ t.end()
50
+ })
51
+
52
+ test('L2 bracket: символ не в экспорте → E1110', function (t) {
53
+ const main = path.join(root, 'main.sail')
54
+ const lib = path.join(root, 'lib.sail')
55
+ const files = {
56
+ [main]: '+Lib ( @missing ) ./lib.sail\n@main ( -> )\n\n',
57
+ [lib]: '@hello ( -> )\n\n'
58
+ }
59
+ const r = resolveSailNames({ entryPath: main, readFile: vfs(files) })
60
+ t.ok(r.ok === false)
61
+ const d = r.diagnostics.find(x => x.code === 'E1110')
62
+ t.ok(d && d.message.includes('missing'))
63
+ t.end()
64
+ })
65
+
66
+ test('L2 FFI: скобочный &Buffer — нет ADT в JSDoc → E1110 (opaque не вносится скобками)', function (t) {
67
+ const main = path.join(root, 'opaque-bracket.sail')
68
+ const js = path.join(root, 'opaque-ffi.js')
69
+ const files = {
70
+ [main]: '+M ( &Buffer ) ./opaque-ffi.js\n',
71
+ [js]: [
72
+ '/**',
73
+ ' * @sail',
74
+ ' * @f ( -> )',
75
+ ' */',
76
+ 'export const f = 1'
77
+ ].join('\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 === 'E1110')
82
+ t.ok(d && d.message.includes('Buffer'))
83
+ t.end()
84
+ })
85
+
86
+ test('L2 FFI: +Num ./num.js и скобочный @add', function (t) {
87
+ const main = path.join(root, 'main.sail')
88
+ const num = path.join(root, 'num.js')
89
+ const files = {
90
+ [main]: [
91
+ '+Num ( @add ) ./num.js',
92
+ '@main ( Num Num -> Num )',
93
+ '',
94
+ ' /add'
95
+ ].join('\n'),
96
+ [num]: [
97
+ '/**',
98
+ ' * @sail',
99
+ ' * @add ( Num Num -> Num )',
100
+ ' * ',
101
+ ' */',
102
+ 'export function add (a, b) { return a + b }'
103
+ ].join('\n')
104
+ }
105
+ const r = resolveSailNames({ entryPath: main, readFile: vfs(files) })
106
+ t.ok(r.ok, r.diagnostics.map(d => d.code + ' ' + d.message).join('; '))
107
+ t.end()
108
+ })