@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.
- package/bin/sail.mjs +4 -0
- package/cli/run-cli.js +176 -0
- package/index.js +12 -2
- package/lib/codegen/README.md +230 -0
- package/lib/codegen/codegen-diagnostics.js +164 -0
- package/lib/codegen/compile-graph.js +107 -0
- package/lib/codegen/emit-adt.js +177 -0
- package/lib/codegen/emit-body.js +1265 -0
- package/lib/codegen/emit-builtin.js +371 -0
- package/lib/codegen/emit-jsdoc-sail.js +383 -0
- package/lib/codegen/emit-module.js +498 -0
- package/lib/codegen/esm-imports.js +26 -0
- package/lib/codegen/index.js +69 -0
- package/lib/codegen/out-layout.js +102 -0
- package/lib/ffi/extract-jsdoc-sail.js +34 -0
- package/lib/io-node/index.js +4 -0
- package/lib/io-node/package-root.js +18 -0
- package/lib/io-node/read-file.js +12 -0
- package/lib/io-node/resolve-package.js +24 -0
- package/lib/io-node/resolve-sail-names-from-disk.js +21 -0
- package/lib/ir/assert-json-serializable.js +30 -0
- package/lib/ir/attach-call-effects.js +108 -0
- package/lib/ir/bind-values.js +594 -0
- package/lib/ir/build-module-ir.js +290 -0
- package/lib/ir/index.js +31 -0
- package/lib/ir/lower-body-steps.js +170 -0
- package/lib/ir/module-metadata.js +65 -0
- package/lib/ir/schema-version.js +15 -0
- package/lib/ir/serialize.js +202 -0
- package/lib/ir/stitch-types.js +92 -0
- package/lib/names/adt-autogen.js +22 -0
- package/lib/names/import-path.js +28 -0
- package/lib/names/index.js +1 -0
- package/lib/names/local-declarations.js +127 -0
- package/lib/names/lower-first.js +6 -0
- package/lib/names/module-scope.js +120 -0
- package/lib/names/resolve-sail.js +365 -0
- package/lib/names/walk-ast-refs.js +91 -0
- package/lib/parse/ast-build.js +51 -0
- package/lib/parse/ast-spec.js +212 -0
- package/lib/parse/builtins-set.js +12 -0
- package/lib/parse/diagnostics.js +180 -0
- package/lib/parse/index.js +46 -0
- package/lib/parse/lexer.js +390 -0
- package/lib/parse/parse-source.js +912 -0
- package/lib/typecheck/adt-autogen-sigs.js +345 -0
- package/lib/typecheck/build-type-env.js +148 -0
- package/lib/typecheck/builtin-signatures.js +183 -0
- package/lib/typecheck/check-word-body.js +1021 -0
- package/lib/typecheck/effect-decl.js +124 -0
- package/lib/typecheck/index.js +55 -0
- package/lib/typecheck/normalize-sig.js +369 -0
- package/lib/typecheck/stack-step-snapshots.js +56 -0
- package/lib/typecheck/unify-type.js +665 -0
- package/lib/typecheck/validate-adt.js +201 -0
- package/package.json +4 -9
- package/scripts/regen-demo-full-syntax-ast.mjs +22 -0
- package/test/cli/sail-cli.test.js +64 -0
- package/test/codegen/compile-bracket-ffi-e2e.test.js +64 -0
- package/test/codegen/compile-stage0.test.js +128 -0
- package/test/codegen/compile-stage4-layout.test.js +124 -0
- package/test/codegen/e2e-prelude-ffi-adt/app/extra.sail +6 -0
- package/test/codegen/e2e-prelude-ffi-adt/app/lib.sail +34 -0
- package/test/codegen/e2e-prelude-ffi-adt/app/main.sail +28 -0
- package/test/codegen/e2e-prelude-ffi-adt/artifacts/.gitignore +2 -0
- package/test/codegen/e2e-prelude-ffi-adt/ffi/helpers.js +27 -0
- package/test/codegen/e2e-prelude-ffi-adt.test.js +100 -0
- package/test/codegen/emit-adt-stage6.test.js +168 -0
- package/test/codegen/emit-async-stage5.test.js +164 -0
- package/test/codegen/emit-body-stage2.test.js +139 -0
- package/test/codegen/emit-body.test.js +163 -0
- package/test/codegen/emit-builtins-stage7.test.js +258 -0
- package/test/codegen/emit-diagnostics-stage9.test.js +90 -0
- package/test/codegen/emit-jsdoc-stage8.test.js +113 -0
- package/test/codegen/emit-module-stage3.test.js +78 -0
- package/test/conformance/conformance-ir-l4.test.js +38 -0
- package/test/conformance/conformance-l5-codegen.test.js +111 -0
- package/test/conformance/conformance-runner.js +91 -0
- package/test/conformance/conformance-suite-l3.test.js +32 -0
- package/test/ffi/prelude-jsdoc.test.js +49 -0
- package/test/fixtures/demo-full-syntax.ast.json +1471 -0
- package/test/fixtures/demo-full-syntax.sail +35 -0
- package/test/fixtures/io-node-ffi-adt/ffi.js +7 -0
- package/test/fixtures/io-node-ffi-adt/use.sail +4 -0
- package/test/fixtures/io-node-mini/dep.sail +2 -0
- package/test/fixtures/io-node-mini/entry.sail +4 -0
- package/test/fixtures/io-node-prelude/entry.sail +4 -0
- package/test/fixtures/io-node-reexport-chain/a.sail +4 -0
- package/test/fixtures/io-node-reexport-chain/b.sail +2 -0
- package/test/fixtures/io-node-reexport-chain/c.sail +2 -0
- package/test/io-node/resolve-disk.test.js +59 -0
- package/test/ir/bind-values.test.js +84 -0
- package/test/ir/build-module-ir.test.js +100 -0
- package/test/ir/call-effects.test.js +97 -0
- package/test/ir/ffi-bracket-ir.test.js +59 -0
- package/test/ir/full-ir-document.test.js +51 -0
- package/test/ir/ir-document-assert.js +67 -0
- package/test/ir/lower-body-steps.test.js +90 -0
- package/test/ir/module-metadata.test.js +42 -0
- package/test/ir/serialization-model.test.js +172 -0
- package/test/ir/stitch-types.test.js +74 -0
- package/test/names/l2-resolve-adt-autogen.test.js +155 -0
- package/test/names/l2-resolve-bracket-ffi.test.js +108 -0
- package/test/names/l2-resolve-declaration-and-bracket-errors.test.js +276 -0
- package/test/names/l2-resolve-graph.test.js +105 -0
- package/test/names/l2-resolve-single-file.test.js +79 -0
- package/test/parse/ast-spec.test.js +56 -0
- package/test/parse/ast.test.js +476 -0
- package/test/parse/contract.test.js +37 -0
- package/test/parse/fixtures-full-syntax.test.js +24 -0
- package/test/parse/helpers.js +27 -0
- package/test/parse/l0-lex-diagnostics-matrix.test.js +59 -0
- package/test/parse/l0-lex.test.js +40 -0
- package/test/parse/l1-diagnostics.test.js +77 -0
- package/test/parse/l1-import.test.js +28 -0
- package/test/parse/l1-parse-diagnostics-matrix.test.js +32 -0
- package/test/parse/l1-top-level.test.js +47 -0
- package/test/parse/l1-types.test.js +31 -0
- package/test/parse/l1-words.test.js +49 -0
- package/test/parse/l2-diagnostics-contract.test.js +67 -0
- package/test/parse/l3-diagnostics-contract.test.js +66 -0
- package/test/typecheck/adt-decl-stage2.test.js +83 -0
- package/test/typecheck/container-contract-e1309.test.js +258 -0
- package/test/typecheck/ffi-bracket-l3.test.js +61 -0
- package/test/typecheck/l3-diagnostics-matrix.test.js +248 -0
- package/test/typecheck/l3-partial-pipeline.test.js +74 -0
- package/test/typecheck/opaque-ffi-type.test.js +78 -0
- package/test/typecheck/sig-type-stage3.test.js +190 -0
- package/test/typecheck/stack-check-stage4.test.js +149 -0
- package/test/typecheck/stack-check-stage5.test.js +74 -0
- package/test/typecheck/stack-check-stage6.test.js +56 -0
- package/test/typecheck/stack-check-stage7.test.js +160 -0
- package/test/typecheck/stack-check-stage8.test.js +146 -0
- package/test/typecheck/stack-check-stage9.test.js +105 -0
- package/test/typecheck/typecheck-env.test.js +53 -0
- package/test/typecheck/typecheck-pipeline.test.js +37 -0
- package/README.md +0 -37
- package/cli/sail.js +0 -151
- package/cli/typecheck.js +0 -39
- package/docs/ARCHITECTURE.md +0 -50
- package/docs/CHANGELOG.md +0 -18
- package/docs/FFI-GUIDE.md +0 -65
- package/docs/RELEASE.md +0 -36
- package/docs/TESTING.md +0 -86
- 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.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"sail": "
|
|
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,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
|