@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,201 @@
1
+ /**
2
+ * L3 этап 2: проверка объявлений `&` (RFC-0.1 §7, §5.3) — type_expr в payload/полях.
3
+ */
4
+ import * as diag from '../parse/diagnostics.js'
5
+
6
+ /**
7
+ * @param {object[]} out
8
+ * @param {object} span
9
+ * @param {string} code
10
+ * @param {string} message
11
+ */
12
+ function pushDiag (out, span, code, message) {
13
+ const d = { code, message }
14
+ if (span?.start) {
15
+ d.offset = span.start.offset
16
+ d.line = span.start.line
17
+ d.column = span.start.column
18
+ }
19
+ out.push(d)
20
+ }
21
+
22
+ /**
23
+ * @param {object | null | undefined} typeExpr
24
+ * @param {(node: object) => void} onModuleRef
25
+ * @param {(node: object) => void} onTypeVar
26
+ */
27
+ function walkAdtSigStackItem (item, onModuleRef, onTypeVar) {
28
+ if (!item) return
29
+ switch (item.kind) {
30
+ case 'sig_type_expr':
31
+ walkAdtTypeExpr(item.type, onModuleRef, onTypeVar)
32
+ return
33
+ case 'quotation_sig':
34
+ walkAdtSignatureSlots(item.inner, onModuleRef, onTypeVar)
35
+ return
36
+ case 'named_quotation_sig':
37
+ walkAdtSigStackItem(item.quotation, onModuleRef, onTypeVar)
38
+ return
39
+ default:
40
+ return
41
+ }
42
+ }
43
+
44
+ function walkAdtSignatureSlots (sig, onModuleRef, onTypeVar) {
45
+ if (!sig) return
46
+ for (const x of sig.left) walkAdtSigStackItem(x, onModuleRef, onTypeVar)
47
+ for (const x of sig.right) walkAdtSigStackItem(x, onModuleRef, onTypeVar)
48
+ }
49
+
50
+ function walkAdtTypeExpr (typeExpr, onModuleRef, onTypeVar) {
51
+ if (!typeExpr) return
52
+ switch (typeExpr.kind) {
53
+ case 'type_var':
54
+ onTypeVar(typeExpr)
55
+ return
56
+ case 'type_name':
57
+ return
58
+ case 'module_type_ref':
59
+ onModuleRef(typeExpr)
60
+ return
61
+ case 'paren_type':
62
+ walkAdtTypeExpr(typeExpr.inner, onModuleRef, onTypeVar)
63
+ return
64
+ case 'quotation_type':
65
+ walkAdtSignatureSlots(typeExpr.inner, onModuleRef, onTypeVar)
66
+ return
67
+ case 'type_app':
68
+ for (const a of typeExpr.args) walkAdtTypeExpr(a, onModuleRef, onTypeVar)
69
+ return
70
+ default:
71
+ return
72
+ }
73
+ }
74
+
75
+ /**
76
+ * @param {object} item sum_type | product_type
77
+ * @param {Map<string, object>} importMap
78
+ * @param {object[]} diagnostics
79
+ */
80
+ function diagnoseAdtItem (item, importMap, diagnostics) {
81
+ if (item.kind === 'sum_type') {
82
+ const allowedParams = new Set(item.typeParams)
83
+ const paramList = item.typeParams.length
84
+ ? item.typeParams.join(', ')
85
+ : '(нет)'
86
+
87
+ for (const tag of item.tags) {
88
+ if (!tag.payload) continue
89
+ walkAdtTypeExpr(
90
+ tag.payload,
91
+ (node) => {
92
+ const { module: m, type: ty } = node
93
+ const dep = importMap.get(m)
94
+ if (!dep) {
95
+ pushDiag(diagnostics, node.span, 'E1203', diag.e1203ModuleNotFound(m))
96
+ } else if (!dep.exportTypes.has(ty)) {
97
+ pushDiag(diagnostics, node.span, 'E1204', diag.e1204MissingMember(m, ty))
98
+ }
99
+ },
100
+ (node) => {
101
+ if (!allowedParams.has(node.name)) {
102
+ pushDiag(
103
+ diagnostics,
104
+ node.span,
105
+ 'E1304',
106
+ diag.e1304TypeMismatch(
107
+ `параметры &${item.name}: ${paramList}`,
108
+ `переменная типа '${node.name}'`
109
+ )
110
+ )
111
+ }
112
+ }
113
+ )
114
+ }
115
+ return
116
+ }
117
+
118
+ if (item.kind === 'product_type') {
119
+ const allowedParams = new Set(item.typeParams)
120
+ const paramList = item.typeParams.length
121
+ ? item.typeParams.join(', ')
122
+ : '(нет)'
123
+
124
+ for (const field of item.fields) {
125
+ walkAdtTypeExpr(
126
+ field.type,
127
+ (node) => {
128
+ const { module: m, type: ty } = node
129
+ const dep = importMap.get(m)
130
+ if (!dep) {
131
+ pushDiag(diagnostics, node.span, 'E1203', diag.e1203ModuleNotFound(m))
132
+ } else if (!dep.exportTypes.has(ty)) {
133
+ pushDiag(diagnostics, node.span, 'E1204', diag.e1204MissingMember(m, ty))
134
+ }
135
+ },
136
+ (node) => {
137
+ if (!allowedParams.has(node.name)) {
138
+ pushDiag(
139
+ diagnostics,
140
+ node.span,
141
+ 'E1304',
142
+ diag.e1304TypeMismatch(
143
+ `параметры &${item.name}: ${paramList}`,
144
+ `переменная типа '${node.name}'`
145
+ )
146
+ )
147
+ }
148
+ }
149
+ )
150
+ }
151
+ }
152
+ }
153
+
154
+ /**
155
+ * @param {import('./build-type-env.js').TypecheckEnv} env
156
+ * @returns {{ diagnostics: object[], adtByPath: Map<string, Map<string, object>> }}
157
+ */
158
+ export function validateAdtDeclarations (env) {
159
+ /** @type {object[]} */
160
+ const diagnostics = []
161
+ /** @type {Map<string, Map<string, object>>} */
162
+ const adtByPath = new Map()
163
+
164
+ for (const p of env.modulePathsOrdered) {
165
+ const snap = env.snapshots.get(p)
166
+ const scopeInfo = env.scopeByPath.get(p)
167
+ if (!snap?.ok || !scopeInfo) continue
168
+
169
+ /** @type {Map<string, object>} */
170
+ const perPath = new Map()
171
+ for (const item of snap.items) {
172
+ if (item.kind === 'sum_type' || item.kind === 'product_type') {
173
+ const itemDiags = []
174
+ diagnoseAdtItem(item, scopeInfo.importMap, itemDiags)
175
+ diagnostics.push(...itemDiags)
176
+ if (itemDiags.length === 0) {
177
+ if (item.kind === 'sum_type') {
178
+ perPath.set(item.name, {
179
+ kind: 'sum',
180
+ name: item.name,
181
+ typeParams: [...item.typeParams],
182
+ ast: item
183
+ })
184
+ } else {
185
+ perPath.set(item.name, {
186
+ kind: 'product',
187
+ name: item.name,
188
+ typeParams: [...item.typeParams],
189
+ ast: item
190
+ })
191
+ }
192
+ }
193
+ }
194
+ }
195
+ if (perPath.size > 0) {
196
+ adtByPath.set(p, perPath)
197
+ }
198
+ }
199
+
200
+ return { diagnostics, adtByPath }
201
+ }
package/package.json CHANGED
@@ -1,21 +1,16 @@
1
1
  {
2
2
  "name": "@algosail/lang",
3
- "version": "0.2.12",
3
+ "version": "0.5.1",
4
4
  "type": "module",
5
5
  "main": "index.js",
6
6
  "bin": {
7
- "sail": "cli/sail.js"
7
+ "sail-lang": "./bin/sail.mjs"
8
8
  },
9
9
  "scripts": {
10
- "test": "brittle \"test/**/*.test.js\""
10
+ "test": "brittle \"test/**/*.test.js\"",
11
+ "regen:fixture-ast": "node scripts/regen-demo-full-syntax-ast.mjs"
11
12
  },
12
13
  "devDependencies": {
13
14
  "brittle": "^3.19.1"
14
- },
15
- "dependencies": {
16
- "@algosail/builtins": "^0.0.6",
17
- "@algosail/compiler": "^0.0.3",
18
- "@algosail/parser": "^0.1.3",
19
- "@algosail/typecheck": "^0.1.2"
20
15
  }
21
16
  }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Перезаписывает test/fixtures/demo-full-syntax.ast.json из demo-full-syntax.sail.
3
+ * Запуск из каталога lang: `npm run regen:fixture-ast`
4
+ */
5
+ import fs from 'fs'
6
+ import path from 'path'
7
+ import { fileURLToPath } from 'url'
8
+ import { parseSource } from '../index.js'
9
+
10
+ const langRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), '..')
11
+ const fixtureDir = path.join(langRoot, 'test/fixtures')
12
+ const sailPath = path.join(fixtureDir, 'demo-full-syntax.sail')
13
+ const jsonPath = path.join(fixtureDir, 'demo-full-syntax.ast.json')
14
+
15
+ const sourceText = fs.readFileSync(sailPath, 'utf8')
16
+ const r = parseSource(sourceText)
17
+ if (!r.ok) {
18
+ console.error(r.diagnostics)
19
+ process.exit(1)
20
+ }
21
+ fs.writeFileSync(jsonPath, JSON.stringify(r.ast, null, 2) + '\n')
22
+ console.log('wrote', jsonPath)
@@ -0,0 +1,64 @@
1
+ /**
2
+ * CLI sail-lang: typecheck и compile (spawn node bin/sail.mjs).
3
+ */
4
+ import test from 'brittle'
5
+ import { spawnSync } from 'node:child_process'
6
+ import fs from 'node:fs'
7
+ import os from 'node:os'
8
+ import path from 'node:path'
9
+ import { fileURLToPath } from 'node:url'
10
+
11
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
12
+ const langRoot = path.resolve(__dirname, '..', '..')
13
+ const binPath = path.join(langRoot, 'bin', 'sail.mjs')
14
+ const miniEntry = path.join(langRoot, 'test', 'fixtures', 'io-node-mini', 'entry.sail')
15
+
16
+ function runCli (args, opts = {}) {
17
+ return spawnSync(process.execPath, [binPath, ...args], {
18
+ encoding: 'utf8',
19
+ cwd: opts.cwd ?? langRoot,
20
+ env: { ...process.env, ...opts.env }
21
+ })
22
+ }
23
+
24
+ test('sail-lang: typecheck ok на io-node-mini', function (t) {
25
+ const r = runCli(['typecheck', miniEntry])
26
+ t.is(r.status, 0, r.stderr)
27
+ t.ok(r.stdout.includes('ok'), 'stdout ok')
28
+ t.end()
29
+ })
30
+
31
+ test('sail-lang: typecheck fail — неизвестное слово', function (t) {
32
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'sail-cli-bad-'))
33
+ const bad = path.join(dir, 'bad.sail')
34
+ fs.writeFileSync(bad, ['@w ( -> )', '', ' /noSuchWordXyz', ''].join('\n'), 'utf8')
35
+ const r = runCli(['typecheck', bad])
36
+ t.is(r.status, 1)
37
+ t.ok(r.stderr.includes('E1201'), r.stderr)
38
+ fs.rmSync(dir, { recursive: true, force: true })
39
+ t.end()
40
+ })
41
+
42
+ test('sail-lang: compile — entry + dep в out-dir', function (t) {
43
+ const outDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sail-cli-compile-'))
44
+ const r = runCli(['compile', '-o', outDir, miniEntry])
45
+ t.is(r.status, 0, r.stderr)
46
+ t.ok(fs.existsSync(path.join(outDir, 'entry.js')))
47
+ t.ok(fs.existsSync(path.join(outDir, 'dep.js')))
48
+ fs.rmSync(outDir, { recursive: true, force: true })
49
+ t.end()
50
+ })
51
+
52
+ test('sail-lang: compile без -o — код 1', function (t) {
53
+ const r = runCli(['compile', miniEntry])
54
+ t.is(r.status, 1)
55
+ t.ok(r.stderr.includes('out-dir') || r.stderr.includes('--out-dir'), r.stderr)
56
+ t.end()
57
+ })
58
+
59
+ test('sail-lang: --help код 0', function (t) {
60
+ const r = runCli(['--help'])
61
+ t.is(r.status, 0)
62
+ t.ok(r.stderr.includes('typecheck') && r.stderr.includes('compile'), r.stderr)
63
+ t.end()
64
+ })
@@ -0,0 +1,64 @@
1
+ /**
2
+ * L5: compileSailToOutDir — скобочный FFI, вложенные пути, исполнение Node ESM.
3
+ */
4
+ import test from 'brittle'
5
+ import fs from 'node:fs'
6
+ import os from 'node:os'
7
+ import path from 'node:path'
8
+ import { pathToFileURL } from 'node:url'
9
+ import { compileSailToOutDir } from '../../lib/codegen/index.js'
10
+
11
+ const virtualRoot = path.resolve('/virtual/sail-codegen-bracket-ffi')
12
+
13
+ function vfs (files) {
14
+ const norm = Object.fromEntries(
15
+ Object.entries(files).map(([k, v]) => [path.normalize(k), v])
16
+ )
17
+ return (p) => norm[path.normalize(p)] ?? null
18
+ }
19
+
20
+ test('compile: скобочный FFI + /add — зеркало .js и результат 5', async function (t) {
21
+ const sourceRoot = path.join(virtualRoot, 'nested')
22
+ const main = path.join(sourceRoot, 'app', 'entry', 'main.sail')
23
+ const math = path.join(sourceRoot, 'app', 'vendor', 'math.js')
24
+ const files = {
25
+ [main]: [
26
+ '+Math ( @add ) ../vendor/math.js',
27
+ '@main ( Num Num -> Num )',
28
+ '',
29
+ ' /add'
30
+ ].join('\n'),
31
+ [math]: [
32
+ '/**',
33
+ ' * @sail',
34
+ ' * @add ( Num Num -> Num )',
35
+ ' */',
36
+ 'export function add (a, b) { return a + b }'
37
+ ].join('\n')
38
+ }
39
+ const outDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sail-bracket-ffi-'))
40
+ const r = await compileSailToOutDir({
41
+ entryPath: main,
42
+ outDir,
43
+ sourceRoot,
44
+ readFile: vfs(files)
45
+ })
46
+ t.ok(r.ok === true, r.ok === false ? r.diagnostics?.[0]?.message : '')
47
+ const oMain = path.join(outDir, 'app', 'entry', 'main.js')
48
+ const oMath = path.join(outDir, 'app', 'vendor', 'math.js')
49
+ t.ok(fs.existsSync(oMain))
50
+ t.ok(fs.existsSync(oMath))
51
+ const mainSrc = fs.readFileSync(oMain, 'utf8')
52
+ t.ok(
53
+ /import \{\s*add\s*\} from ['"]\.\.\/vendor\/math\.js['"]/.test(mainSrc),
54
+ 'именованный import на зеркальный math.js'
55
+ )
56
+ t.ok(!/\bimport\s*\(/.test(mainSrc), 'без динамического import()')
57
+
58
+ const mainMod = await import(pathToFileURL(oMain).href)
59
+ t.ok(typeof mainMod.main === 'function')
60
+ t.is(mainMod.main(2, 3), 5)
61
+
62
+ fs.rmSync(outDir, { recursive: true, force: true })
63
+ t.end()
64
+ })
@@ -0,0 +1,128 @@
1
+ /**
2
+ * L5: драйвер compileSailToOutDir — typecheck; при ошибке без записи; при ok — ESM (этап 3).
3
+ */
4
+ import test from 'brittle'
5
+ import fs from 'node:fs'
6
+ import os from 'node:os'
7
+ import path from 'node:path'
8
+ import { pathToFileURL } from 'node:url'
9
+ import { compileSailToOutDir } from '../../lib/codegen/index.js'
10
+ import { resolveCompileLayout, sailModuleToOutputJsPath } from '../../lib/codegen/out-layout.js'
11
+
12
+ const virtualRoot = path.resolve('/virtual/sail-codegen-stage0')
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('этап 0: L3 error — ни одной записи, diagnostics', async function (t) {
22
+ const main = path.join(virtualRoot, 'bad.sail')
23
+ const src = ['@bad ( -> )', '', ' drop', ''].join('\n')
24
+ const outDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sail-cg0-'))
25
+ let writes = 0
26
+ const r = await compileSailToOutDir({
27
+ entryPath: main,
28
+ outDir,
29
+ readFile: vfs({ [main]: src }),
30
+ fs: {
31
+ mkdir: async () => {},
32
+ writeFile: async () => {
33
+ writes++
34
+ }
35
+ }
36
+ })
37
+ t.ok(r.ok === false)
38
+ t.ok(Array.isArray(r.diagnostics) && r.diagnostics.length > 0)
39
+ t.is(writes, 0)
40
+ const js = path.join(outDir, 'bad.js')
41
+ t.ok(!fs.existsSync(js), '.js не создан при ошибке L3')
42
+ fs.rmSync(outDir, { recursive: true, force: true })
43
+ t.end()
44
+ })
45
+
46
+ test('compile: ok один модуль — ESM с export function, путь O(S)', async function (t) {
47
+ const main = path.join(virtualRoot, 'one.sail')
48
+ const src = ['@w ( -> Num Num )', '', ' 1', '', ' dup', ''].join('\n')
49
+ const outDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sail-cg0-'))
50
+ const layout = resolveCompileLayout({
51
+ entryPath: main,
52
+ outDir,
53
+ sourceRoot: path.dirname(main)
54
+ })
55
+ const expectPath = sailModuleToOutputJsPath(layout, main)
56
+ t.ok(expectPath.ok)
57
+ t.is(expectPath.path, path.join(outDir, 'one.js'))
58
+
59
+ const r = await compileSailToOutDir({
60
+ entryPath: main,
61
+ outDir,
62
+ sourceRoot: path.dirname(main),
63
+ readFile: vfs({ [main]: src })
64
+ })
65
+ t.ok(r.ok === true)
66
+ t.is(r.diagnostics.length, 0)
67
+ t.is(r.emitted.length, 1)
68
+ t.is(r.emitted[0], expectPath.path)
69
+ t.ok(fs.existsSync(expectPath.path))
70
+ const txt = fs.readFileSync(expectPath.path, 'utf8')
71
+ t.ok(/\bexport function w\s*\(/.test(txt), 'export function w')
72
+ t.ok(!/\bimport\s*\(/.test(txt), 'без динамического import() в сгенерированном файле')
73
+
74
+ const mod = await import(pathToFileURL(expectPath.path).href)
75
+ t.ok(typeof mod.w === 'function')
76
+ const pair = mod.w()
77
+ t.ok(Array.isArray(pair) && pair.length === 2)
78
+ t.is(pair[0], 1)
79
+ t.is(pair[1], 1)
80
+ fs.rmSync(outDir, { recursive: true, force: true })
81
+ t.end()
82
+ })
83
+
84
+ test('compile: ok два .sail — статический import и оба модуля исполняются', async function (t) {
85
+ const sourceRoot = path.join(virtualRoot, 'repo')
86
+ const main = path.join(sourceRoot, 'main.sail')
87
+ const lib = path.join(sourceRoot, 'lib.sail')
88
+ const files = {
89
+ [main]: ['+Lib ./lib.sail', '@main ( -> )', '', ' ~Lib/hello'].join('\n'),
90
+ [lib]: ['@hello ( -> )', '', ''].join('\n')
91
+ }
92
+ const outDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sail-cg0-'))
93
+ const layout = resolveCompileLayout({ entryPath: main, outDir, sourceRoot })
94
+ const oMain = sailModuleToOutputJsPath(layout, main)
95
+ const oLib = sailModuleToOutputJsPath(layout, lib)
96
+ t.ok(oMain.ok && oLib.ok)
97
+
98
+ const r = await compileSailToOutDir({
99
+ entryPath: main,
100
+ outDir,
101
+ sourceRoot,
102
+ readFile: vfs(files)
103
+ })
104
+ t.ok(r.ok === true)
105
+ t.is(r.emitted.length, 2)
106
+ t.ok(fs.existsSync(oMain.path))
107
+ t.ok(fs.existsSync(oLib.path))
108
+
109
+ const mainSrc = fs.readFileSync(oMain.path, 'utf8')
110
+ const libSrc = fs.readFileSync(oLib.path, 'utf8')
111
+ t.ok(
112
+ mainSrc.includes('import * as Lib from "./lib.js"'),
113
+ 'main импортирует lib.js'
114
+ )
115
+ t.ok(/\bLib\.hello\s*\(/.test(mainSrc), 'вызов Lib.hello')
116
+ t.ok(/\bexport function main\s*\(/.test(mainSrc), 'export main')
117
+ t.ok(/\bexport function hello\s*\(/.test(libSrc), 'export hello в lib')
118
+ t.ok(!/\bimport\s*\(/.test(mainSrc), 'без import()')
119
+
120
+ const libMod = await import(pathToFileURL(oLib.path).href)
121
+ const mainMod = await import(pathToFileURL(oMain.path).href)
122
+ t.ok(typeof libMod.hello === 'function')
123
+ t.ok(typeof mainMod.main === 'function')
124
+ mainMod.main()
125
+
126
+ fs.rmSync(outDir, { recursive: true, force: true })
127
+ t.end()
128
+ })
@@ -0,0 +1,124 @@
1
+ /**
2
+ * L5 этап 4: зеркало FFI `.js` в out-dir, относительные import к O(S) для `.js`.
3
+ */
4
+ import test from 'brittle'
5
+ import fs from 'node:fs'
6
+ import os from 'node:os'
7
+ import path from 'node:path'
8
+ import { pathToFileURL } from 'node:url'
9
+ import { compileSailToOutDir } from '../../lib/codegen/index.js'
10
+ import {
11
+ projectJsFileToOutputPath,
12
+ resolveCompileLayout,
13
+ sailModuleToOutputJsPath
14
+ } from '../../lib/codegen/out-layout.js'
15
+
16
+ const virtualRoot = path.resolve('/virtual/sail-codegen-stage4')
17
+
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
+ test('этап 4: вложенные каталоги — O(S) и import ../lib/dep.js', async function (t) {
26
+ const sourceRoot = path.join(virtualRoot, 'nested')
27
+ const main = path.join(sourceRoot, 'app', 'entry', 'main.sail')
28
+ const dep = path.join(sourceRoot, 'app', 'lib', 'dep.sail')
29
+ const files = {
30
+ [main]: ['+Dep ../lib/dep.sail', '@main ( -> )', '', ' ~Dep/ping'].join('\n'),
31
+ [dep]: ['@ping ( -> )', '', ''].join('\n')
32
+ }
33
+ const outDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sail-cg4-nested-'))
34
+ const layout = resolveCompileLayout({ entryPath: main, outDir, sourceRoot })
35
+ const oMain = sailModuleToOutputJsPath(layout, main)
36
+ const oDep = sailModuleToOutputJsPath(layout, dep)
37
+ t.ok(oMain.ok && oDep.ok)
38
+ t.is(
39
+ oMain.path,
40
+ path.join(outDir, 'app', 'entry', 'main.js')
41
+ )
42
+ t.is(oDep.path, path.join(outDir, 'app', 'lib', 'dep.js'))
43
+
44
+ const r = await compileSailToOutDir({
45
+ entryPath: main,
46
+ outDir,
47
+ sourceRoot,
48
+ readFile: vfs(files)
49
+ })
50
+ t.ok(r.ok === true, r.ok === false ? r.diagnostics?.[0]?.message : '')
51
+ t.is(r.emitted.length, 2)
52
+ t.ok(fs.existsSync(oMain.path))
53
+ t.ok(fs.existsSync(oDep.path))
54
+ const mainSrc = fs.readFileSync(oMain.path, 'utf8')
55
+ t.ok(
56
+ mainSrc.includes('import * as Dep from "../lib/dep.js"'),
57
+ 'относительный путь к dep в out-dir'
58
+ )
59
+
60
+ const depMod = await import(pathToFileURL(oDep.path).href)
61
+ const mainMod = await import(pathToFileURL(oMain.path).href)
62
+ t.ok(typeof depMod.ping === 'function')
63
+ t.ok(typeof mainMod.main === 'function')
64
+ mainMod.main()
65
+
66
+ fs.rmSync(outDir, { recursive: true, force: true })
67
+ t.end()
68
+ })
69
+
70
+ test('этап 4: скобочный FFI + JSDoc-слово в helper — зеркало и import { add }', async function (t) {
71
+ const sourceRoot = path.join(virtualRoot, 'ffi-nested')
72
+ const main = path.join(sourceRoot, 'app', 'entry', 'main.sail')
73
+ const helper = path.join(sourceRoot, 'app', 'vendor', 'helper.js')
74
+ const helperText = [
75
+ '/**',
76
+ ' * @sail',
77
+ ' * @add ( Num Num -> Num )',
78
+ ' */',
79
+ 'export function add (a, b) { return a + b }'
80
+ ].join('\n')
81
+ const files = {
82
+ [main]: [
83
+ '+Helper ( @add ) ../vendor/helper.js',
84
+ '@main ( Num Num -> Num )',
85
+ '',
86
+ ' /add'
87
+ ].join('\n'),
88
+ [helper]: helperText
89
+ }
90
+ const outDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sail-cg4-ffi-'))
91
+ const layout = resolveCompileLayout({ entryPath: main, outDir, sourceRoot })
92
+ const oMain = sailModuleToOutputJsPath(layout, main)
93
+ const oHelper = projectJsFileToOutputPath(layout, helper)
94
+ t.ok(oMain.ok && oHelper.ok)
95
+ t.is(
96
+ oHelper.path,
97
+ path.join(outDir, 'app', 'vendor', 'helper.js')
98
+ )
99
+
100
+ const r = await compileSailToOutDir({
101
+ entryPath: main,
102
+ outDir,
103
+ sourceRoot,
104
+ readFile: vfs(files)
105
+ })
106
+ t.ok(r.ok === true, r.ok === false ? r.diagnostics?.[0]?.message : '')
107
+ t.is(r.emitted.length, 2)
108
+ t.ok(fs.existsSync(oHelper.path))
109
+ const copied = fs.readFileSync(oHelper.path, 'utf8')
110
+ t.is(copied, helperText, 'FFI .js скопирован без изменений')
111
+ const mainSrc = fs.readFileSync(oMain.path, 'utf8')
112
+ t.ok(
113
+ /import \{\s*add\s*\} from ['"]\.\.\/vendor\/helper\.js['"]/.test(mainSrc),
114
+ 'именованный import на зеркальный helper.js'
115
+ )
116
+ t.ok(!/\bimport\s*\(/.test(mainSrc), 'без динамического import()')
117
+
118
+ const mainMod = await import(pathToFileURL(oMain.path).href)
119
+ t.ok(typeof mainMod.main === 'function')
120
+ t.is(mainMod.main(2, 3), 5)
121
+
122
+ fs.rmSync(outDir, { recursive: true, force: true })
123
+ t.end()
124
+ })
@@ -0,0 +1,6 @@
1
+ -- Третий модуль: локальное слово поверх prelude (dup + add = удвоение). --
2
+ +Nums @algosail/prelude/num
3
+
4
+ @double ( Num -> Num )
5
+ dup
6
+ ~Nums/add
@@ -0,0 +1,34 @@
1
+ -- Point; rot/add; list prelude; over+nip (доп. shuffle). --
2
+ +Nums @algosail/prelude/num
3
+ +Lists @algosail/prelude/list
4
+
5
+ & Point
6
+ : x Num
7
+ : y Num
8
+
9
+ @sumCoords ( Point -> Num )
10
+ -- Sasd --
11
+ dup
12
+ /xPoint
13
+ swap
14
+ /yPoint
15
+ ~Nums/add
16
+
17
+ @rotSum ( -> Num )
18
+ 2
19
+ 3
20
+ 4
21
+ rot
22
+ ~Nums/add
23
+ ~Nums/add
24
+
25
+ @listMarker ( -> Num )
26
+ 1
27
+ ~Lists/listOfOne
28
+ ~Lists/listLength
29
+
30
+ @secondPlusTop ( Num Num -> Num )
31
+ swap
32
+ over
33
+ ~Nums/add
34
+ nip