@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,163 @@
1
+ /**
2
+ * L5 этап 1: emitWordBodyIr (irSteps + valueBindings).
3
+ */
4
+ import test from 'brittle'
5
+ import path from 'node:path'
6
+ import { typecheckSail } from '../../index.js'
7
+ import { buildModuleIr } from '../../lib/ir/index.js'
8
+ import { emitWordBodyIr } from '../../lib/codegen/emit-body.js'
9
+
10
+ const root = path.resolve('/virtual/sail-codegen-emit-body')
11
+ function vfs (files) {
12
+ const norm = Object.fromEntries(
13
+ Object.entries(files).map(([k, v]) => [path.normalize(k), v])
14
+ )
15
+ return (p) => norm[path.normalize(p)] ?? null
16
+ }
17
+
18
+ /** Эвристика: не моделируем стек массивом с push/pop (см. план этапа 1). */
19
+ function assertNoStackArrayModel (t, source) {
20
+ t.ok(
21
+ !/\.push\s*\(/.test(source) && !/\.pop\s*\(/.test(source),
22
+ 'ожидался код без .push( / .pop( как у массива-стека'
23
+ )
24
+ }
25
+
26
+ test('emit-body: dup — без stack-array, два одинаковых значения на выходе', function (t) {
27
+ const main = path.join(root, 'dup.sail')
28
+ const src = ['@w ( -> Num Num )', '', ' 1', '', ' dup', ''].join('\n')
29
+ const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
30
+ t.ok(r.ok, r.diagnostics?.map((d) => d.message).join('; '))
31
+ const norm = path.normalize(main)
32
+ const payload = buildModuleIr(r.env, norm, {
33
+ moduleStatus: 'ok',
34
+ valueBindings: true
35
+ })
36
+ const w = payload.words.find((x) => x.name === 'w')
37
+ const sig = w.normalizedSig
38
+ const { source } = emitWordBodyIr(w.irSteps, {
39
+ normalizedSig: sig,
40
+ wordName: 'w'
41
+ })
42
+ assertNoStackArrayModel(t, source)
43
+ t.ok(/const v\d+ = 1/.test(source), 'литерал 1')
44
+ t.ok(/return \[v\d+, v\d+\]/.test(source), 'return двух значений')
45
+ t.end()
46
+ })
47
+
48
+ test('emit-body: swap', function (t) {
49
+ const main = path.join(root, 'swap.sail')
50
+ const src = ['@w ( -> Num Num )', '', ' 1', '', ' 2', '', ' swap', ''].join(
51
+ '\n'
52
+ )
53
+ const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
54
+ t.ok(r.ok, r.diagnostics?.map((d) => d.message).join('; '))
55
+ const norm = path.normalize(main)
56
+ const payload = buildModuleIr(r.env, norm, {
57
+ moduleStatus: 'ok',
58
+ valueBindings: true
59
+ })
60
+ const w = payload.words.find((x) => x.name === 'w')
61
+ const { source } = emitWordBodyIr(w.irSteps, {
62
+ normalizedSig: w.normalizedSig,
63
+ wordName: 'w'
64
+ })
65
+ assertNoStackArrayModel(t, source)
66
+ t.ok(source.includes('1') && source.includes('2'), 'литералы присутствуют')
67
+ t.ok(
68
+ !/const\s+v\d+\s*=\s+v\d+\s*;/.test(source),
69
+ 'swap — без лишних const (только перестановка ссылок на уже существующие имена)'
70
+ )
71
+ t.end()
72
+ })
73
+
74
+ test('emit-body: drop', function (t) {
75
+ const main = path.join(root, 'drop.sail')
76
+ const src = ['@w ( -> )', '', ' 1', '', ' drop', ''].join('\n')
77
+ const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
78
+ t.ok(r.ok, r.diagnostics?.map((d) => d.message).join('; '))
79
+ const norm = path.normalize(main)
80
+ const payload = buildModuleIr(r.env, norm, {
81
+ moduleStatus: 'ok',
82
+ valueBindings: true
83
+ })
84
+ const w = payload.words.find((x) => x.name === 'w')
85
+ const { source } = emitWordBodyIr(w.irSteps, {
86
+ normalizedSig: w.normalizedSig,
87
+ wordName: 'w'
88
+ })
89
+ assertNoStackArrayModel(t, source)
90
+ t.ok(/\breturn;\s*$/.test(source.trim()), 'пустой результат — return;')
91
+ t.end()
92
+ })
93
+
94
+ test('emit-body: локальный вызов Word (вершина — последний аргумент)', function (t) {
95
+ const main = path.join(root, 'local.sail')
96
+ const src = [
97
+ '@idem ( Num -> Num )',
98
+ '',
99
+ ' dup drop',
100
+ '',
101
+ '@caller ( Num -> Num )',
102
+ '',
103
+ ' /idem',
104
+ ''
105
+ ].join('\n')
106
+ const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
107
+ t.ok(r.ok, r.diagnostics?.map((d) => d.message).join('; '))
108
+ const norm = path.normalize(main)
109
+ const payload = buildModuleIr(r.env, norm, {
110
+ moduleStatus: 'ok',
111
+ valueBindings: true
112
+ })
113
+ const caller = payload.words.find((x) => x.name === 'caller')
114
+ const { source } = emitWordBodyIr(caller.irSteps, {
115
+ normalizedSig: caller.normalizedSig,
116
+ wordName: 'caller'
117
+ })
118
+ assertNoStackArrayModel(t, source)
119
+ t.ok(/idem\s*\(\s*p0\s*\)/.test(source), 'вызов idem(p0) — вершина последний аргумент')
120
+ t.end()
121
+ })
122
+
123
+ test('emit-body: strict — шаг без valueBindings', function (t) {
124
+ const main = path.join(root, 'strict.sail')
125
+ const src = ['@w ( -> Num )', '', ' 1', ''].join('\n')
126
+ const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
127
+ t.ok(r.ok, r.diagnostics?.map((d) => d.message).join('; '))
128
+ const norm = path.normalize(main)
129
+ const payload = buildModuleIr(r.env, norm, {
130
+ moduleStatus: 'ok',
131
+ valueBindings: true
132
+ })
133
+ const w = payload.words.find((x) => x.name === 'w')
134
+ const steps = structuredClone(w.irSteps)
135
+ delete steps[0].valueBindings
136
+ t.exception(
137
+ () => {
138
+ emitWordBodyIr(steps, { strict: true, wordName: 'w' })
139
+ },
140
+ /без valueBindings/
141
+ )
142
+ t.end()
143
+ })
144
+
145
+ test('emit-body: исполнение числового сценария через new Function', function (t) {
146
+ const main = path.join(root, 'num.sail')
147
+ const src = ['@w ( -> Num )', '', ' 7', ''].join('\n')
148
+ const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
149
+ t.ok(r.ok, r.diagnostics?.map((d) => d.message).join('; '))
150
+ const norm = path.normalize(main)
151
+ const payload = buildModuleIr(r.env, norm, {
152
+ moduleStatus: 'ok',
153
+ valueBindings: true
154
+ })
155
+ const w = payload.words.find((x) => x.name === 'w')
156
+ const { source } = emitWordBodyIr(w.irSteps, {
157
+ normalizedSig: w.normalizedSig,
158
+ wordName: 'w'
159
+ })
160
+ const fn = new Function(`${source}`)
161
+ t.is(fn(), 7)
162
+ t.end()
163
+ })
@@ -0,0 +1,258 @@
1
+ /**
2
+ * L5 этап 7: комбинаторы dip, keep, bi, tri, spread, both (RFC-builtins §4.4–4.6).
3
+ * Литеральные quotation с ожидаемой сигнатурой задаются через вспомогательные слова `( -> (...) )`.
4
+ */
5
+ import test from 'brittle'
6
+ import path from 'node:path'
7
+ import { typecheckSail } from '../../index.js'
8
+ import {
9
+ emitModuleEsmSource,
10
+ resolveCompileLayout
11
+ } from '../../lib/codegen/index.js'
12
+
13
+ const root = path.resolve('/virtual/sail-codegen-builtins-7')
14
+
15
+ function esmSourceForNewFunction (source) {
16
+ return source.replace(/^export function /gm, 'function ')
17
+ }
18
+
19
+ function vfs (files) {
20
+ const norm = Object.fromEntries(
21
+ Object.entries(files).map(([k, v]) => [path.normalize(k), v])
22
+ )
23
+ return (p) => norm[path.normalize(p)] ?? null
24
+ }
25
+
26
+ const sharedPrelude = [
27
+ '@id ( Num -> Num )',
28
+ '',
29
+ ' dup',
30
+ '',
31
+ ' drop',
32
+ '',
33
+ '',
34
+ '@innerQ ( -> ( Num -> Num ) )',
35
+ '',
36
+ ' ( /id )',
37
+ '',
38
+ '',
39
+ '@innerK ( -> ( Num Num -> Num ) )',
40
+ '',
41
+ ' ( drop )',
42
+ ''
43
+ ].join('\n')
44
+
45
+ test('stage7: dip', function (t) {
46
+ const sourceRoot = path.join(root, 'dip')
47
+ const main = path.join(sourceRoot, 'main.sail')
48
+ const src =
49
+ sharedPrelude +
50
+ [
51
+ '@dipCase ( -> Num Num )',
52
+ '',
53
+ ' 1',
54
+ '',
55
+ ' 2',
56
+ '',
57
+ ' /innerQ',
58
+ '',
59
+ ' dip',
60
+ ''
61
+ ].join('\n')
62
+ const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
63
+ t.ok(r.ok)
64
+ const layout = resolveCompileLayout({
65
+ entryPath: main,
66
+ outDir: path.join(sourceRoot, '_out'),
67
+ sourceRoot
68
+ })
69
+ const g = emitModuleEsmSource({ env: r.env, modulePath: main, layout })
70
+ t.ok(g.ok)
71
+ const mod = new Function(
72
+ `${esmSourceForNewFunction(g.source)}; return { dipCase }`
73
+ )()
74
+ const out = mod.dipCase()
75
+ t.ok(Array.isArray(out))
76
+ t.is(out.length, 2)
77
+ t.is(out[0], 1)
78
+ t.is(out[1], 2)
79
+ t.end()
80
+ })
81
+
82
+ test('stage7: keep', function (t) {
83
+ const sourceRoot = path.join(root, 'keep')
84
+ const main = path.join(sourceRoot, 'main.sail')
85
+ const src =
86
+ sharedPrelude +
87
+ [
88
+ '@keepCase ( -> Num Num )',
89
+ '',
90
+ ' 1',
91
+ '',
92
+ ' 2',
93
+ '',
94
+ ' /innerK',
95
+ '',
96
+ ' keep',
97
+ ''
98
+ ].join('\n')
99
+ const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
100
+ t.ok(r.ok, r.diagnostics?.map((d) => d.message).join('; '))
101
+ const layout = resolveCompileLayout({
102
+ entryPath: main,
103
+ outDir: path.join(sourceRoot, '_out'),
104
+ sourceRoot
105
+ })
106
+ const g = emitModuleEsmSource({ env: r.env, modulePath: main, layout })
107
+ t.ok(g.ok)
108
+ const mod = new Function(
109
+ `${esmSourceForNewFunction(g.source)}; return { keepCase }`
110
+ )()
111
+ const out = mod.keepCase()
112
+ t.alike(out, [1, 2])
113
+ t.end()
114
+ })
115
+
116
+ test('stage7: bi', function (t) {
117
+ const sourceRoot = path.join(root, 'bi')
118
+ const main = path.join(sourceRoot, 'main.sail')
119
+ const src =
120
+ sharedPrelude +
121
+ [
122
+ '@biCase ( -> Num Num Num )',
123
+ '',
124
+ ' 0',
125
+ '',
126
+ ' 1',
127
+ '',
128
+ ' /innerQ',
129
+ '',
130
+ ' /innerQ',
131
+ '',
132
+ ' bi',
133
+ ''
134
+ ].join('\n')
135
+ const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
136
+ t.ok(r.ok)
137
+ const layout = resolveCompileLayout({
138
+ entryPath: main,
139
+ outDir: path.join(sourceRoot, '_out'),
140
+ sourceRoot
141
+ })
142
+ const g = emitModuleEsmSource({ env: r.env, modulePath: main, layout })
143
+ t.ok(g.ok)
144
+ const mod = new Function(
145
+ `${esmSourceForNewFunction(g.source)}; return { biCase }`
146
+ )()
147
+ t.alike(mod.biCase(), [0, 1, 1])
148
+ t.end()
149
+ })
150
+
151
+ test('stage7: tri', function (t) {
152
+ const sourceRoot = path.join(root, 'tri')
153
+ const main = path.join(sourceRoot, 'main.sail')
154
+ const src =
155
+ sharedPrelude +
156
+ [
157
+ '@triCase ( -> Num Num Num Num )',
158
+ '',
159
+ ' 0',
160
+ '',
161
+ ' 1',
162
+ '',
163
+ ' /innerQ',
164
+ '',
165
+ ' /innerQ',
166
+ '',
167
+ ' /innerQ',
168
+ '',
169
+ ' tri',
170
+ ''
171
+ ].join('\n')
172
+ const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
173
+ t.ok(r.ok)
174
+ const layout = resolveCompileLayout({
175
+ entryPath: main,
176
+ outDir: path.join(sourceRoot, '_out'),
177
+ sourceRoot
178
+ })
179
+ const g = emitModuleEsmSource({ env: r.env, modulePath: main, layout })
180
+ t.ok(g.ok)
181
+ const mod = new Function(
182
+ `${esmSourceForNewFunction(g.source)}; return { triCase }`
183
+ )()
184
+ t.alike(mod.triCase(), [0, 1, 1, 1])
185
+ t.end()
186
+ })
187
+
188
+ test('stage7: spread', function (t) {
189
+ const sourceRoot = path.join(root, 'spread')
190
+ const main = path.join(sourceRoot, 'main.sail')
191
+ const src =
192
+ sharedPrelude +
193
+ [
194
+ '@spreadCase ( -> Num Num Num )',
195
+ '',
196
+ ' 0',
197
+ '',
198
+ ' 1',
199
+ '',
200
+ ' 2',
201
+ '',
202
+ ' /innerQ',
203
+ '',
204
+ ' /innerQ',
205
+ '',
206
+ ' spread',
207
+ ''
208
+ ].join('\n')
209
+ const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
210
+ t.ok(r.ok)
211
+ const layout = resolveCompileLayout({
212
+ entryPath: main,
213
+ outDir: path.join(sourceRoot, '_out'),
214
+ sourceRoot
215
+ })
216
+ const g = emitModuleEsmSource({ env: r.env, modulePath: main, layout })
217
+ t.ok(g.ok)
218
+ const mod = new Function(
219
+ `${esmSourceForNewFunction(g.source)}; return { spreadCase }`
220
+ )()
221
+ t.alike(mod.spreadCase(), [0, 1, 2])
222
+ t.end()
223
+ })
224
+
225
+ test('stage7: both', function (t) {
226
+ const sourceRoot = path.join(root, 'both')
227
+ const main = path.join(sourceRoot, 'main.sail')
228
+ const src =
229
+ sharedPrelude +
230
+ [
231
+ '@bothCase ( -> Num Num Num )',
232
+ '',
233
+ ' 0',
234
+ '',
235
+ ' 5',
236
+ '',
237
+ ' 7',
238
+ '',
239
+ ' /innerQ',
240
+ '',
241
+ ' both',
242
+ ''
243
+ ].join('\n')
244
+ const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
245
+ t.ok(r.ok)
246
+ const layout = resolveCompileLayout({
247
+ entryPath: main,
248
+ outDir: path.join(sourceRoot, '_out'),
249
+ sourceRoot
250
+ })
251
+ const g = emitModuleEsmSource({ env: r.env, modulePath: main, layout })
252
+ t.ok(g.ok)
253
+ const mod = new Function(
254
+ `${esmSourceForNewFunction(g.source)}; return { bothCase }`
255
+ )()
256
+ t.alike(mod.bothCase(), [0, 5, 7])
257
+ t.end()
258
+ })
@@ -0,0 +1,90 @@
1
+ /**
2
+ * L5 этап 9: диагностики codegen (E51xx–E53xx), поле path, валидация layout.
3
+ */
4
+ import test from 'brittle'
5
+ import path from 'node:path'
6
+ import { typecheckSail } from '../../index.js'
7
+ import { emitCodegenStage0Stub } from '../../lib/codegen/compile-graph.js'
8
+ import {
9
+ compileSailToOutDir,
10
+ resolveCompileLayout,
11
+ sailModuleToOutputJsPath
12
+ } from '../../lib/codegen/index.js'
13
+
14
+ const root = path.resolve('/virtual/sail-codegen-diag9')
15
+
16
+ function vfs (files) {
17
+ const norm = Object.fromEntries(
18
+ Object.entries(files).map(([k, v]) => [path.normalize(k), v])
19
+ )
20
+ return (p) => norm[path.normalize(p)] ?? null
21
+ }
22
+
23
+ test('stage9: E5105 — entryPath вне явного sourceRoot', async function (t) {
24
+ const sourceRoot = path.join(root, 'sr')
25
+ const entryOutside = path.join(root, 'other', 'main.sail')
26
+ const files = {
27
+ [entryOutside]: ['@m ( -> )', '', ''].join('\n')
28
+ }
29
+ const r = await compileSailToOutDir({
30
+ entryPath: entryOutside,
31
+ outDir: path.join(root, 'out'),
32
+ sourceRoot,
33
+ readFile: vfs(files)
34
+ })
35
+ t.ok(r.ok === false)
36
+ t.is(r.diagnostics[0].code, 'E5105')
37
+ t.ok(typeof r.diagnostics[0].path === 'string')
38
+ t.end()
39
+ })
40
+
41
+ test('stage9: E5101 + path — sailModuleToOutputJsPath вне source-root', function (t) {
42
+ const layout = resolveCompileLayout({
43
+ entryPath: path.join(root, 'e.sail'),
44
+ outDir: path.join(root, 'o'),
45
+ sourceRoot: path.join(root, 'src')
46
+ })
47
+ const modOutside = path.join(root, 'external', 'm.sail')
48
+ const mapped = sailModuleToOutputJsPath(layout, modOutside)
49
+ t.ok(mapped.ok === false)
50
+ t.is(mapped.diagnostic.code, 'E5101')
51
+ t.is(path.normalize(mapped.diagnostic.path), path.normalize(modOutside))
52
+ t.end()
53
+ })
54
+
55
+ test('stage9: E5301 — FFI в замыкании без readFile у emitCodegenStage0Stub', async function (t) {
56
+ const sourceRoot = path.join(root, 'ffi-noread')
57
+ const main = path.join(sourceRoot, 'main.sail')
58
+ const helper = path.join(sourceRoot, 'helper.js')
59
+ const files = {
60
+ [main]: [
61
+ '+H ( @add ) ./helper.js',
62
+ '@main ( Num Num -> Num )',
63
+ '',
64
+ ' /add'
65
+ ].join('\n'),
66
+ [helper]: [
67
+ '/**',
68
+ ' * @sail',
69
+ ' * @add ( Num Num -> Num )',
70
+ ' */',
71
+ 'export function add (a, b) { return a + b }'
72
+ ].join('\n')
73
+ }
74
+ const read = vfs(files)
75
+ const tc = typecheckSail({ entryPath: main, readFile: read })
76
+ t.ok(tc.ok, tc.diagnostics?.map((d) => d.message).join('; ') ?? '')
77
+ const layout = resolveCompileLayout({
78
+ entryPath: main,
79
+ outDir: path.join(sourceRoot, '_out'),
80
+ sourceRoot
81
+ })
82
+ const sink = {
83
+ mkdir: async () => {},
84
+ writeFile: async () => {}
85
+ }
86
+ const gen = await emitCodegenStage0Stub(tc.env, layout, sink, undefined)
87
+ t.ok(gen.ok === false)
88
+ t.is(gen.diagnostics[0].code, 'E5301')
89
+ t.end()
90
+ })
@@ -0,0 +1,113 @@
1
+ /**
2
+ * L5 этап 8: JSDoc `@sail` в сгенерированном ESM (RFC-compile §11, RFC-0.1 §10.1).
3
+ */
4
+ import test from 'brittle'
5
+ import path from 'node:path'
6
+ import { parseSource, typecheckSail } from '../../index.js'
7
+ import {
8
+ emitModuleEsmSource,
9
+ resolveCompileLayout
10
+ } from '../../lib/codegen/index.js'
11
+ import { extractSailFragmentsFromJs } from '../../lib/ffi/extract-jsdoc-sail.js'
12
+
13
+ const root = path.resolve('/virtual/sail-codegen-jsdoc8')
14
+
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('stage8: extractSailFragmentsFromJs — & в первом фрагменте, @word во втором', function (t) {
23
+ const main = path.join(root, 'twoblock.sail')
24
+ const src = [
25
+ '& Color',
26
+ '-- цветовой тип --',
27
+ '| Red',
28
+ '| Green',
29
+ '',
30
+ '@label ( -> Str )',
31
+ '-- текст для слова --',
32
+ ' "ok"',
33
+ ''
34
+ ].join('\n')
35
+ const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
36
+ t.ok(r.ok, r.diagnostics?.map((d) => d.message).join('; '))
37
+ const layout = resolveCompileLayout({
38
+ entryPath: main,
39
+ outDir: path.join(root, '_out'),
40
+ sourceRoot: root
41
+ })
42
+ const g = emitModuleEsmSource({ env: r.env, modulePath: main, layout })
43
+ t.ok(g.ok, g.ok === false ? g.diagnostics?.[0]?.message : '')
44
+ const frags = extractSailFragmentsFromJs(g.source)
45
+ t.ok(frags.length >= 2, 'минимум: заголовок + слово')
46
+ t.ok(frags[0].includes('& Color'), 'первый фрагмент: ADT')
47
+ t.ok(frags[0].includes('| Red'), 'теги sum в заголовке')
48
+ const wordFrag = frags.find((f) => f.includes('@label'))
49
+ t.ok(wordFrag, 'фрагмент с @label')
50
+ t.ok(wordFrag.includes('@label'), '@label в извлечённом')
51
+ for (const f of frags) {
52
+ const pr = parseSource(f)
53
+ t.ok(pr.ok, `фрагмент парсится как Sail: ${pr.diagnostics?.[0]?.message ?? ''}`)
54
+ }
55
+ t.ok(g.source.includes('текст для слова'), 'doc слова в префиксе JSDoc у export')
56
+ t.end()
57
+ })
58
+
59
+ test('stage8: текст из doc AST — в префиксе JSDoc до @sail и валидный Sail после', function (t) {
60
+ const main = path.join(root, 'prose.sail')
61
+ const typeNote = 'заметка к типу Bit'
62
+ const src = [
63
+ '& Bit',
64
+ `-- ${typeNote} --`,
65
+ '| B0',
66
+ '| B1',
67
+ '',
68
+ '@flip ( Num -> )',
69
+ ' drop',
70
+ ''
71
+ ].join('\n')
72
+ const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
73
+ t.ok(r.ok, r.diagnostics?.map((d) => d.message).join('; '))
74
+ const layout = resolveCompileLayout({
75
+ entryPath: main,
76
+ outDir: path.join(root, '_out2'),
77
+ sourceRoot: root
78
+ })
79
+ const g = emitModuleEsmSource({ env: r.env, modulePath: main, layout })
80
+ t.ok(g.ok, g.ok === false ? g.diagnostics?.[0]?.message : '')
81
+ const idx = g.source.indexOf('@sail')
82
+ t.ok(idx >= 0)
83
+ const beforeFirstTag = g.source.slice(0, idx)
84
+ t.ok(
85
+ beforeFirstTag.includes(typeNote),
86
+ 'doc типа продублирован строками ` * …` до первого @sail'
87
+ )
88
+ const frags = extractSailFragmentsFromJs(g.source)
89
+ const head = frags[0]
90
+ t.ok(head.includes('--'), 'в извлечённом заголовке есть sail doc_block')
91
+ const pr = parseSource(head)
92
+ t.ok(pr.ok, pr.diagnostics?.[0]?.message ?? '')
93
+ t.end()
94
+ })
95
+
96
+ test('stage8: exportAsync — +Async в фрагменте @sail у экспорта', function (t) {
97
+ const main = path.join(root, 'asyncfrag.sail')
98
+ const src = ['@wake ( -> +Async )', '', ''].join('\n')
99
+ const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
100
+ t.ok(r.ok, r.diagnostics?.map((d) => d.message).join('; '))
101
+ const layout = resolveCompileLayout({
102
+ entryPath: main,
103
+ outDir: path.join(root, '_out3'),
104
+ sourceRoot: root
105
+ })
106
+ const g = emitModuleEsmSource({ env: r.env, modulePath: main, layout })
107
+ t.ok(g.ok, g.ok === false ? g.diagnostics?.[0]?.message : '')
108
+ t.ok(/\bexport async function wake\b/.test(g.source), 'async export')
109
+ const frags = extractSailFragmentsFromJs(g.source)
110
+ const wFrag = frags.find((f) => f.includes('@wake'))
111
+ t.ok(wFrag && /\+Async/.test(wFrag), '+Async в контракте wake')
112
+ t.end()
113
+ })
@@ -0,0 +1,78 @@
1
+ /**
2
+ * L5 этап 3: один модуль → ESM (импорты + export function), без записи на диск.
3
+ */
4
+ import test from 'brittle'
5
+ import path from 'node:path'
6
+ import { typecheckSail } from '../../index.js'
7
+ import {
8
+ emitModuleEsmSource,
9
+ resolveCompileLayout
10
+ } from '../../lib/codegen/index.js'
11
+
12
+ const root = path.resolve('/virtual/sail-codegen-stage3')
13
+
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('emit-module stage3: два .sail — import * as Lib и export function', function (t) {
22
+ const sourceRoot = path.join(root, 'repo')
23
+ const main = path.join(sourceRoot, 'main.sail')
24
+ const lib = path.join(sourceRoot, 'lib.sail')
25
+ const files = {
26
+ [main]: ['+Lib ./lib.sail', '@main ( -> )', '', ' ~Lib/hello'].join('\n'),
27
+ [lib]: ['@hello ( -> )', '', ''].join('\n')
28
+ }
29
+ const r = typecheckSail({ entryPath: main, readFile: vfs(files) })
30
+ t.ok(r.ok, r.diagnostics?.map((d) => d.message).join('; '))
31
+ const layout = resolveCompileLayout({
32
+ entryPath: main,
33
+ outDir: path.join(sourceRoot, '_out'),
34
+ sourceRoot
35
+ })
36
+ const libGen = emitModuleEsmSource({ env: r.env, modulePath: lib, layout })
37
+ t.ok(libGen.ok, libGen.ok === false ? libGen.diagnostics?.[0]?.message : '')
38
+ t.ok(
39
+ /\bexport function hello\s*\(/.test(libGen.source),
40
+ 'lib: export function hello'
41
+ )
42
+ const mainGen = emitModuleEsmSource({ env: r.env, modulePath: main, layout })
43
+ t.ok(mainGen.ok, mainGen.ok === false ? mainGen.diagnostics?.[0]?.message : '')
44
+ t.ok(
45
+ mainGen.source.includes('import * as Lib from "./lib.js"'),
46
+ 'main: статический import относительного .js'
47
+ )
48
+ t.ok(/\bLib\.hello\s*\(/.test(mainGen.source), 'main: вызов Lib.hello()')
49
+ t.ok(!/\bimport\s*\(/.test(mainGen.source), 'без import() в сгенерированном тексте')
50
+ t.end()
51
+ })
52
+
53
+ test('emit-module stage3: скобочный импорт — import { hello } и вызов hello()', function (t) {
54
+ const sourceRoot = path.join(root, 'bracket')
55
+ const main = path.join(sourceRoot, 'main.sail')
56
+ const lib = path.join(sourceRoot, 'lib.sail')
57
+ const files = {
58
+ [main]: ['+Lib ( @hello ) ./lib.sail', '@main ( -> )', '', ' ~Lib/hello'].join(
59
+ '\n'
60
+ ),
61
+ [lib]: ['@hello ( -> )', '', ''].join('\n')
62
+ }
63
+ const r = typecheckSail({ entryPath: main, readFile: vfs(files) })
64
+ t.ok(r.ok, r.diagnostics?.map((d) => d.message).join('; '))
65
+ const layout = resolveCompileLayout({
66
+ entryPath: main,
67
+ outDir: path.join(sourceRoot, '_out'),
68
+ sourceRoot
69
+ })
70
+ const mainGen = emitModuleEsmSource({ env: r.env, modulePath: main, layout })
71
+ t.ok(mainGen.ok, mainGen.ok === false ? mainGen.diagnostics?.[0]?.message : '')
72
+ t.ok(
73
+ /import \{\s*hello\s*\} from/.test(mainGen.source),
74
+ 'именованный import hello'
75
+ )
76
+ t.ok(/\bhello\s*\(\s*\)/.test(mainGen.source), 'вызов hello(), не Lib.hello')
77
+ t.end()
78
+ })