@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,35 @@
|
|
|
1
|
+
-- import doc --
|
|
2
|
+
+Base ./vendor/base.sail
|
|
3
|
+
+Num @algosail/prelude/num
|
|
4
|
+
+Std ( @map @filter &Vec &Map ) ./std/lib.sail
|
|
5
|
+
|
|
6
|
+
&Maybe a -- sum header doc --
|
|
7
|
+
| None -- tag None --
|
|
8
|
+
| Just a
|
|
9
|
+
|
|
10
|
+
&Vec2 -- product doc --
|
|
11
|
+
:x Num -- field x --
|
|
12
|
+
:y Num
|
|
13
|
+
|
|
14
|
+
@stacky ( ~s Num -> ~s Num )
|
|
15
|
+
-- word doc --
|
|
16
|
+
dup
|
|
17
|
+
|
|
18
|
+
@effects ( Num -> Str +Async +Fail -Async )
|
|
19
|
+
|
|
20
|
+
@modType ( ~Lib/Point -> )
|
|
21
|
+
|
|
22
|
+
@maybeSlot ( ( Maybe Num ) -> )
|
|
23
|
+
|
|
24
|
+
@dictsig ( Dict Str -> )
|
|
25
|
+
|
|
26
|
+
@mapSig ( ( Map Str Num ) -> )
|
|
27
|
+
|
|
28
|
+
@named ( q:( ~s a -> ~s b ) -> )
|
|
29
|
+
/drop
|
|
30
|
+
|
|
31
|
+
@demoBody ( -> )
|
|
32
|
+
dup swap ~Net/fetch
|
|
33
|
+
( nip ) [ 0 true 2n ]
|
|
34
|
+
:tmp ;tmp "ok" 7 /x/
|
|
35
|
+
42n nil true false
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Интеграция L2 с диском: io-node + resolveSailNamesFromDisk.
|
|
3
|
+
*/
|
|
4
|
+
import test from 'brittle'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
7
|
+
import {
|
|
8
|
+
createReadFileUtf8,
|
|
9
|
+
createResolvePackage,
|
|
10
|
+
resolveSailNames,
|
|
11
|
+
resolveSailNamesFromDisk
|
|
12
|
+
} from '../../index.js'
|
|
13
|
+
|
|
14
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
15
|
+
const fixtures = path.join(__dirname, '../fixtures')
|
|
16
|
+
/** Корень монорепозитория sail (родитель каталога lang). */
|
|
17
|
+
const monorepoRoot = path.resolve(__dirname, '../../..')
|
|
18
|
+
|
|
19
|
+
test('io-node: относительный импорт ./dep.sail', function (t) {
|
|
20
|
+
const entry = path.join(fixtures, 'io-node-mini/entry.sail')
|
|
21
|
+
const r = resolveSailNamesFromDisk({ entryPath: entry })
|
|
22
|
+
t.ok(r.ok, r.diagnostics.map(d => `${d.code} ${d.message}`).join('; '))
|
|
23
|
+
t.end()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('io-node: цепочка реэкспорта a ← b ← c (@origin через b)', function (t) {
|
|
27
|
+
const entry = path.join(fixtures, 'io-node-reexport-chain/a.sail')
|
|
28
|
+
const r = resolveSailNamesFromDisk({ entryPath: entry })
|
|
29
|
+
t.ok(r.ok, r.diagnostics.map(d => `${d.code} ${d.message}`).join('; '))
|
|
30
|
+
t.end()
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('io-node: ADT только в JS @sail, autogen в соседнем .sail', function (t) {
|
|
34
|
+
const entry = path.join(fixtures, 'io-node-ffi-adt/use.sail')
|
|
35
|
+
const r = resolveSailNamesFromDisk({ entryPath: entry })
|
|
36
|
+
t.ok(r.ok, r.diagnostics.map(d => `${d.code} ${d.message}`).join('; '))
|
|
37
|
+
t.end()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('io-node: @algosail/prelude/str с projectRoot монорепо', function (t) {
|
|
41
|
+
const entry = path.join(fixtures, 'io-node-prelude/entry.sail')
|
|
42
|
+
const r = resolveSailNamesFromDisk({
|
|
43
|
+
entryPath: entry,
|
|
44
|
+
projectRoot: monorepoRoot
|
|
45
|
+
})
|
|
46
|
+
t.ok(r.ok, r.diagnostics.map(d => `${d.code} ${d.message}`).join('; '))
|
|
47
|
+
t.end()
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('io-node: createReadFileUtf8 + createResolvePackage + resolveSailNames', function (t) {
|
|
51
|
+
const entry = path.join(fixtures, 'io-node-mini/entry.sail')
|
|
52
|
+
const r = resolveSailNames({
|
|
53
|
+
entryPath: entry,
|
|
54
|
+
readFile: createReadFileUtf8(),
|
|
55
|
+
resolvePackage: createResolvePackage()
|
|
56
|
+
})
|
|
57
|
+
t.ok(r.ok, r.diagnostics.map(d => `${d.code} ${d.message}`).join('; '))
|
|
58
|
+
t.end()
|
|
59
|
+
})
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L4 фаза 4: valueBindings (RFC-IR section 4).
|
|
3
|
+
*/
|
|
4
|
+
import test from 'brittle'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import { typecheckSail } from '../../index.js'
|
|
7
|
+
import { assertJsonSerializable, buildModuleIr } from '../../lib/ir/index.js'
|
|
8
|
+
|
|
9
|
+
const root = path.resolve('/virtual/sail-ir-phase4')
|
|
10
|
+
function vfs (files) {
|
|
11
|
+
const norm = Object.fromEntries(
|
|
12
|
+
Object.entries(files).map(([k, v]) => [path.normalize(k), v])
|
|
13
|
+
)
|
|
14
|
+
return (p) => norm[path.normalize(p)] ?? null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
test('фаза 4: dup — два resultIds с одинаковым id', function (t) {
|
|
18
|
+
const main = path.join(root, 'dup.sail')
|
|
19
|
+
const src = ['@w ( -> Num Num )', '', ' 1', '', ' dup', ''].join('\n')
|
|
20
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
|
|
21
|
+
t.ok(r.ok, r.diagnostics?.map((d) => d.message).join('; '))
|
|
22
|
+
const norm = path.normalize(main)
|
|
23
|
+
const payload = buildModuleIr(r.env, norm, {
|
|
24
|
+
moduleStatus: 'ok',
|
|
25
|
+
valueBindings: true
|
|
26
|
+
})
|
|
27
|
+
const w = payload.words.find((x) => x.name === 'w')
|
|
28
|
+
const lit = w.irSteps[0]
|
|
29
|
+
const dup = w.irSteps[1]
|
|
30
|
+
t.is(dup.kind, 'Builtin')
|
|
31
|
+
t.is(dup.name, 'dup')
|
|
32
|
+
const vb = dup.valueBindings
|
|
33
|
+
t.ok(vb)
|
|
34
|
+
t.is(vb.argIds.length, 1)
|
|
35
|
+
t.is(vb.resultIds.length, 2)
|
|
36
|
+
t.is(vb.resultIds[0], vb.resultIds[1])
|
|
37
|
+
t.is(vb.argIds[vb.argIds.length - 1], vb.resultIds[0])
|
|
38
|
+
assertJsonSerializable(payload)
|
|
39
|
+
t.end()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('фаза 4: swap — перестановка двух id на вершине', function (t) {
|
|
43
|
+
const main = path.join(root, 'swap.sail')
|
|
44
|
+
const src = ['@w ( -> Num Num )', '', ' 1', '', ' 2', '', ' swap', ''].join(
|
|
45
|
+
'\n'
|
|
46
|
+
)
|
|
47
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
|
|
48
|
+
t.ok(r.ok, r.diagnostics?.map((d) => d.message).join('; '))
|
|
49
|
+
const norm = path.normalize(main)
|
|
50
|
+
const payload = buildModuleIr(r.env, norm, {
|
|
51
|
+
moduleStatus: 'ok',
|
|
52
|
+
valueBindings: true
|
|
53
|
+
})
|
|
54
|
+
const w = payload.words.find((x) => x.name === 'w')
|
|
55
|
+
const id1 = w.irSteps[0].valueBindings.resultIds[0]
|
|
56
|
+
const id2 = w.irSteps[1].valueBindings.resultIds[1]
|
|
57
|
+
const sw = w.irSteps[2]
|
|
58
|
+
t.is(sw.name, 'swap')
|
|
59
|
+
const vb = sw.valueBindings
|
|
60
|
+
t.alike(vb.argIds, [id1, id2])
|
|
61
|
+
t.alike(vb.resultIds, [id2, id1])
|
|
62
|
+
assertJsonSerializable(payload)
|
|
63
|
+
t.end()
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('фаза 4: drop — один argId, пустые resultIds', function (t) {
|
|
67
|
+
const main = path.join(root, 'drop.sail')
|
|
68
|
+
const src = ['@w ( -> )', '', ' 1', '', ' drop', ''].join('\n')
|
|
69
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
|
|
70
|
+
t.ok(r.ok, r.diagnostics?.map((d) => d.message).join('; '))
|
|
71
|
+
const norm = path.normalize(main)
|
|
72
|
+
const payload = buildModuleIr(r.env, norm, {
|
|
73
|
+
moduleStatus: 'ok',
|
|
74
|
+
valueBindings: true
|
|
75
|
+
})
|
|
76
|
+
const w = payload.words.find((x) => x.name === 'w')
|
|
77
|
+
const dr = w.irSteps[1]
|
|
78
|
+
t.is(dr.name, 'drop')
|
|
79
|
+
const vb = dr.valueBindings
|
|
80
|
+
t.is(vb.argIds.length, 1)
|
|
81
|
+
t.is(vb.resultIds.length, 0)
|
|
82
|
+
assertJsonSerializable(payload)
|
|
83
|
+
t.end()
|
|
84
|
+
})
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L4 фаза 1: buildModuleIr / buildSerializedIrDocumentFromEnv из L3.
|
|
3
|
+
*/
|
|
4
|
+
import test from 'brittle'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import { typecheckSail } from '../../index.js'
|
|
7
|
+
import {
|
|
8
|
+
assertJsonSerializable,
|
|
9
|
+
buildModuleIr,
|
|
10
|
+
buildSerializedIrDocumentFromEnv,
|
|
11
|
+
parseIrDocumentFull
|
|
12
|
+
} from '../../lib/ir/index.js'
|
|
13
|
+
|
|
14
|
+
const root = path.resolve('/virtual/sail-ir-phase1')
|
|
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('buildSerializedIrDocumentFromEnv: ok — JSON, round-trip, stackSteps pre/post', function (t) {
|
|
23
|
+
const main = path.join(root, 'ok-dup.sail')
|
|
24
|
+
const src = ['@w ( -> Num Num )', '', ' 1', '', ' dup', ''].join('\n')
|
|
25
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
|
|
26
|
+
t.ok(r.ok, r.diagnostics?.map((d) => d.message).join('; '))
|
|
27
|
+
|
|
28
|
+
const doc = buildSerializedIrDocumentFromEnv(r.env, main, { moduleStatus: 'ok' })
|
|
29
|
+
assertJsonSerializable(doc)
|
|
30
|
+
t.is(doc.meta?.status, 'ok')
|
|
31
|
+
|
|
32
|
+
const round = parseIrDocumentFull(JSON.stringify(doc))
|
|
33
|
+
t.is(round.meta?.status, 'ok')
|
|
34
|
+
t.is(round.module.path, path.normalize(main))
|
|
35
|
+
|
|
36
|
+
const wEntry = round.module.words.find((x) => x.name === 'w')
|
|
37
|
+
t.ok(wEntry, 'слово w в payload')
|
|
38
|
+
t.is(wEntry.irSteps?.length, 2, 'фаза 2: два IR-шага (1 и dup)')
|
|
39
|
+
t.is(wEntry.irSteps[0].kind, 'Literal')
|
|
40
|
+
t.is(wEntry.irSteps[1].kind, 'Builtin')
|
|
41
|
+
t.ok(Array.isArray(wEntry.irSteps[0].postTypes), 'фаза 3: типы на irSteps')
|
|
42
|
+
const steps = wEntry.stackSteps?.steps
|
|
43
|
+
t.ok(steps && steps.length === 2, 'два шага снимка')
|
|
44
|
+
t.ok(Array.isArray(steps[0].preTypes))
|
|
45
|
+
t.ok(Array.isArray(steps[0].postTypes))
|
|
46
|
+
t.is(steps[0].postTypes[0].kind, 'prim')
|
|
47
|
+
t.is(steps[0].postTypes[0].name, 'Num')
|
|
48
|
+
t.end()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('buildModuleIr: ok — вложенная quotation в nestedByParentStep', function (t) {
|
|
52
|
+
const main = path.join(root, 'ok-nested.sail')
|
|
53
|
+
const src = ['@w ( -> ( -> Num ) )', '', ' ( 1 )', ''].join('\n')
|
|
54
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
|
|
55
|
+
t.ok(r.ok, r.diagnostics?.map((d) => d.message).join('; '))
|
|
56
|
+
const payload = buildModuleIr(r.env, main, { moduleStatus: 'ok' })
|
|
57
|
+
const wEntry = payload.words.find((x) => x.name === 'w')
|
|
58
|
+
t.ok(wEntry?.stackSteps?.steps?.length === 1)
|
|
59
|
+
const nested = wEntry.stackSteps.nestedByParentStep
|
|
60
|
+
t.ok(Array.isArray(nested) && nested.length === 1)
|
|
61
|
+
t.is(nested[0].parentStepIndex, 0)
|
|
62
|
+
t.ok(nested[0].record.steps.length === 1)
|
|
63
|
+
assertJsonSerializable(payload)
|
|
64
|
+
t.end()
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('buildSerializedIrDocumentFromEnv: error — частичный снимок у bad, пустой у ok', function (t) {
|
|
68
|
+
const main = path.join(root, 'fail-two.sail')
|
|
69
|
+
const src = [
|
|
70
|
+
'@bad ( -> )',
|
|
71
|
+
'',
|
|
72
|
+
' drop',
|
|
73
|
+
'',
|
|
74
|
+
'@ok ( -> )',
|
|
75
|
+
'',
|
|
76
|
+
''
|
|
77
|
+
].join('\n')
|
|
78
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
|
|
79
|
+
t.ok(r.ok === false)
|
|
80
|
+
|
|
81
|
+
const doc = buildSerializedIrDocumentFromEnv(r.env, main, { moduleStatus: 'error' })
|
|
82
|
+
assertJsonSerializable(doc)
|
|
83
|
+
t.is(doc.meta?.status, 'error')
|
|
84
|
+
t.is(doc.module.status, 'error')
|
|
85
|
+
|
|
86
|
+
const round = parseIrDocumentFull(JSON.stringify(doc))
|
|
87
|
+
t.is(round.meta?.status, 'error')
|
|
88
|
+
|
|
89
|
+
const badEntry = round.module.words.find((x) => x.name === 'bad')
|
|
90
|
+
const okEntry = round.module.words.find((x) => x.name === 'ok')
|
|
91
|
+
t.ok(badEntry?.stackSteps?.steps?.length >= 1)
|
|
92
|
+
const lastBad = badEntry.stackSteps.steps[badEntry.stackSteps.steps.length - 1]
|
|
93
|
+
t.ok(Array.isArray(lastBad.preTypes))
|
|
94
|
+
t.ok(lastBad.postTypes === undefined, 'на шаге сбоя postTypes может отсутствовать')
|
|
95
|
+
|
|
96
|
+
t.ok(okEntry)
|
|
97
|
+
t.is(okEntry.stackSteps?.steps?.length ?? 0, 0)
|
|
98
|
+
|
|
99
|
+
t.end()
|
|
100
|
+
})
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L4 фаза 5: +Async / +Fail на IR (RFC-IR section 6).
|
|
3
|
+
*/
|
|
4
|
+
import test from 'brittle'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import { typecheckSail } from '../../index.js'
|
|
7
|
+
import { assertJsonSerializable, buildModuleIr } from '../../lib/ir/index.js'
|
|
8
|
+
|
|
9
|
+
const root = path.resolve('/virtual/sail-ir-phase5')
|
|
10
|
+
function vfs (files) {
|
|
11
|
+
const norm = Object.fromEntries(
|
|
12
|
+
Object.entries(files).map(([k, v]) => [path.normalize(k), v])
|
|
13
|
+
)
|
|
14
|
+
return (p) => norm[path.normalize(p)] ?? null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
test('фаза 5: caller + asyncCallee — asyncDefinition и calleeAsync', function (t) {
|
|
18
|
+
const main = path.join(root, 'async-caller.sail')
|
|
19
|
+
const src = [
|
|
20
|
+
'@asyncCallee ( -> +Async )',
|
|
21
|
+
'',
|
|
22
|
+
'@caller ( -> +Async )',
|
|
23
|
+
' /asyncCallee',
|
|
24
|
+
''
|
|
25
|
+
].join('\n')
|
|
26
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
|
|
27
|
+
t.ok(r.ok, r.diagnostics?.map((d) => d.message).join('; '))
|
|
28
|
+
const norm = path.normalize(main)
|
|
29
|
+
const payload = buildModuleIr(r.env, norm, { moduleStatus: 'ok' })
|
|
30
|
+
const caller = payload.words.find((w) => w.name === 'caller')
|
|
31
|
+
const calle = payload.words.find((w) => w.name === 'asyncCallee')
|
|
32
|
+
t.ok(caller)
|
|
33
|
+
t.ok(calle)
|
|
34
|
+
t.is(caller.asyncDefinition, true)
|
|
35
|
+
t.is(caller.mayFail, false)
|
|
36
|
+
t.is(calle.asyncDefinition, true)
|
|
37
|
+
t.is(calle.mayFail, false)
|
|
38
|
+
const step = caller.irSteps[0]
|
|
39
|
+
t.is(step.kind, 'Word')
|
|
40
|
+
t.is(step.name, 'asyncCallee')
|
|
41
|
+
t.is(step.calleeAsync, true)
|
|
42
|
+
t.ok(step.calleeMayFail !== true)
|
|
43
|
+
assertJsonSerializable(payload)
|
|
44
|
+
t.end()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('фаза 5: wrap вызывает risky (+Fail) — calleeMayFail не в IR (вариант B)', function (t) {
|
|
48
|
+
const main = path.join(root, 'fail-wrap-ir.sail')
|
|
49
|
+
const src = [
|
|
50
|
+
'@risky ( -> +Fail )',
|
|
51
|
+
'',
|
|
52
|
+
'@wrap ( -> -Fail )',
|
|
53
|
+
' /risky',
|
|
54
|
+
''
|
|
55
|
+
].join('\n')
|
|
56
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
|
|
57
|
+
t.ok(r.ok, r.diagnostics?.map((d) => d.message).join('; '))
|
|
58
|
+
const norm = path.normalize(main)
|
|
59
|
+
const payload = buildModuleIr(r.env, norm, { moduleStatus: 'ok' })
|
|
60
|
+
const risky = payload.words.find((w) => w.name === 'risky')
|
|
61
|
+
const wrap = payload.words.find((w) => w.name === 'wrap')
|
|
62
|
+
t.ok(risky)
|
|
63
|
+
t.ok(wrap)
|
|
64
|
+
t.is(risky.mayFail, true)
|
|
65
|
+
t.is(risky.asyncDefinition, false)
|
|
66
|
+
t.is(wrap.mayFail, false)
|
|
67
|
+
const step = wrap.irSteps[0]
|
|
68
|
+
t.is(step.kind, 'Word')
|
|
69
|
+
t.is(step.name, 'risky')
|
|
70
|
+
t.ok(step.calleeMayFail !== true)
|
|
71
|
+
assertJsonSerializable(payload)
|
|
72
|
+
t.end()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('фаза 5: wrap (-Async) + asyncCallee — calleeAsync на шаге нет (вариант B)', function (t) {
|
|
76
|
+
const main = path.join(root, 'async-wrap-ir.sail')
|
|
77
|
+
const src = [
|
|
78
|
+
'@asyncCallee ( -> +Async )',
|
|
79
|
+
'',
|
|
80
|
+
'@wrap ( -> -Async )',
|
|
81
|
+
' /asyncCallee',
|
|
82
|
+
''
|
|
83
|
+
].join('\n')
|
|
84
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
|
|
85
|
+
t.ok(r.ok, r.diagnostics?.map((d) => d.message).join('; '))
|
|
86
|
+
const norm = path.normalize(main)
|
|
87
|
+
const payload = buildModuleIr(r.env, norm, { moduleStatus: 'ok' })
|
|
88
|
+
const wrap = payload.words.find((w) => w.name === 'wrap')
|
|
89
|
+
t.ok(wrap)
|
|
90
|
+
t.is(wrap.asyncDefinition, false)
|
|
91
|
+
const step = wrap.irSteps[0]
|
|
92
|
+
t.is(step.kind, 'Word')
|
|
93
|
+
t.is(step.name, 'asyncCallee')
|
|
94
|
+
t.ok(step.calleeAsync !== true)
|
|
95
|
+
assertJsonSerializable(payload)
|
|
96
|
+
t.end()
|
|
97
|
+
})
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L4: IR для потребителя со скобочным импортом слова из FFI `.js`.
|
|
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
|
+
|
|
9
|
+
const root = path.resolve('/virtual/sail-ir-ffi-bracket')
|
|
10
|
+
|
|
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
|
+
test('buildModuleIr: скобочный FFI — resolvedPath на .js и valueBindings у /add', function (t) {
|
|
19
|
+
const main = path.join(root, 'main.sail')
|
|
20
|
+
const num = path.join(root, 'num.js')
|
|
21
|
+
const files = {
|
|
22
|
+
[main]: [
|
|
23
|
+
'+Num ( @add ) ./num.js',
|
|
24
|
+
'@main ( Num Num -> Num )',
|
|
25
|
+
'',
|
|
26
|
+
' /add'
|
|
27
|
+
].join('\n'),
|
|
28
|
+
[num]: [
|
|
29
|
+
'/**',
|
|
30
|
+
' * @sail',
|
|
31
|
+
' * @add ( Num Num -> Num )',
|
|
32
|
+
' */',
|
|
33
|
+
'export function add (a, b) { return a + b }'
|
|
34
|
+
].join('\n')
|
|
35
|
+
}
|
|
36
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs(files) })
|
|
37
|
+
t.ok(r.ok, r.diagnostics?.map((d) => d.message).join('; '))
|
|
38
|
+
|
|
39
|
+
const payload = buildModuleIr(r.env, main, {
|
|
40
|
+
moduleStatus: 'ok',
|
|
41
|
+
valueBindings: true
|
|
42
|
+
})
|
|
43
|
+
const imp = payload.imports.find((i) => i.module === 'Num')
|
|
44
|
+
t.ok(imp, 'импорт Num')
|
|
45
|
+
t.ok(
|
|
46
|
+
typeof imp.resolvedPath === 'string' && imp.resolvedPath.endsWith('num.js'),
|
|
47
|
+
'resolvedPath указывает на .js'
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
const mainW = payload.words.find((w) => w.name === 'main')
|
|
51
|
+
t.ok(mainW?.irSteps?.length === 1, 'один шаг тела')
|
|
52
|
+
const step = mainW.irSteps[0]
|
|
53
|
+
t.is(step.kind, 'Word')
|
|
54
|
+
t.is(step.ref, 'local')
|
|
55
|
+
t.is(step.name, 'add')
|
|
56
|
+
t.ok(step.valueBindings?.argIds?.length === 2, 'два аргумента FFI')
|
|
57
|
+
t.ok(step.valueBindings?.resultIds?.length === 1, 'один результат')
|
|
58
|
+
t.end()
|
|
59
|
+
})
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L4 фаза 7: полный сериализованный документ и round-trip JSON (RFC-IR §8).
|
|
3
|
+
*/
|
|
4
|
+
import test from 'brittle'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import { typecheckSail } from '../../index.js'
|
|
7
|
+
import {
|
|
8
|
+
buildSerializedIrDocumentFromEnv,
|
|
9
|
+
stringifyIrDocument,
|
|
10
|
+
parseIrDocumentFull
|
|
11
|
+
} from '../../lib/ir/index.js'
|
|
12
|
+
import { assertSerializedIrDocumentOk } from './ir-document-assert.js'
|
|
13
|
+
|
|
14
|
+
const root = path.resolve('/virtual/sail-ir-phase7-full')
|
|
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('фаза 7: полный IR с valueBindings — stringify/parseFull и инварианты', function (t) {
|
|
23
|
+
const main = path.join(root, 'dup.sail')
|
|
24
|
+
const src = ['@w ( -> Num Num )', '', ' 1', '', ' dup', ''].join('\n')
|
|
25
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
|
|
26
|
+
t.ok(r.ok, r.diagnostics?.map((d) => d.message).join('; '))
|
|
27
|
+
const norm = path.normalize(main)
|
|
28
|
+
const doc = buildSerializedIrDocumentFromEnv(r.env, norm, {
|
|
29
|
+
moduleStatus: 'ok',
|
|
30
|
+
valueBindings: true
|
|
31
|
+
})
|
|
32
|
+
assertSerializedIrDocumentOk(t, doc, {
|
|
33
|
+
expectedModulePath: norm,
|
|
34
|
+
expectMetaStatus: 'ok',
|
|
35
|
+
requireNonEmptyWords: true
|
|
36
|
+
})
|
|
37
|
+
const w = doc.module.words.find((x) => x.name === 'w')
|
|
38
|
+
t.ok(w?.irSteps?.length === 2)
|
|
39
|
+
const dupStep = w.irSteps[1]
|
|
40
|
+
t.is(dupStep.kind, 'Builtin')
|
|
41
|
+
t.ok(dupStep.valueBindings)
|
|
42
|
+
t.is(dupStep.valueBindings.resultIds[0], dupStep.valueBindings.resultIds[1])
|
|
43
|
+
|
|
44
|
+
const text = stringifyIrDocument(doc.module, { meta: doc.meta })
|
|
45
|
+
const round = parseIrDocumentFull(text)
|
|
46
|
+
t.is(round.module.path, doc.module.path)
|
|
47
|
+
t.is(round.module.words.length, doc.module.words.length)
|
|
48
|
+
const w2 = round.module.words.find((x) => x.name === 'w')
|
|
49
|
+
t.ok(w2?.irSteps?.[1]?.valueBindings)
|
|
50
|
+
t.end()
|
|
51
|
+
})
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Утилиты фазы 7 (RFC-IR §8): инварианты сериализованного IR-документа в brittle-тестах.
|
|
3
|
+
*/
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { assertJsonSerializable } from '../../lib/ir/assert-json-serializable.js'
|
|
6
|
+
import { IR_SCHEMA_VERSION } from '../../lib/ir/schema-version.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {*} t экземпляр brittle (`test` callback)
|
|
10
|
+
* @param {unknown} doc корень после {@link createSerializedIrDocument}
|
|
11
|
+
* @param {{
|
|
12
|
+
* expectedModulePath?: string
|
|
13
|
+
* expectMetaStatus?: 'ok' | 'error'
|
|
14
|
+
* requireNonEmptyWords?: boolean
|
|
15
|
+
* requireResolvedImportsWhenPresent?: boolean
|
|
16
|
+
* }} [options]
|
|
17
|
+
*/
|
|
18
|
+
export function assertSerializedIrDocumentOk (t, doc, options = {}) {
|
|
19
|
+
assertJsonSerializable(doc)
|
|
20
|
+
t.ok(doc !== null && typeof doc === 'object' && !Array.isArray(doc), 'doc is object')
|
|
21
|
+
const d = /** @type {Record<string, unknown>} */ (doc)
|
|
22
|
+
t.is(d.irSchemaVersion, IR_SCHEMA_VERSION, 'irSchemaVersion matches IR_SCHEMA_VERSION')
|
|
23
|
+
const mod = d.module
|
|
24
|
+
t.ok(mod !== null && typeof mod === 'object' && !Array.isArray(mod), 'module is object')
|
|
25
|
+
const m = /** @type {Record<string, unknown>} */ (mod)
|
|
26
|
+
t.ok(typeof m.path === 'string' && m.path.length > 0, 'module.path is non-empty string')
|
|
27
|
+
if (options.expectedModulePath != null) {
|
|
28
|
+
t.is(
|
|
29
|
+
path.normalize(m.path),
|
|
30
|
+
path.normalize(options.expectedModulePath),
|
|
31
|
+
'module.path matches entry'
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
if (options.expectMetaStatus != null) {
|
|
35
|
+
const meta = d.meta
|
|
36
|
+
t.ok(meta != null && typeof meta === 'object', 'meta present')
|
|
37
|
+
t.is(
|
|
38
|
+
/** @type {Record<string, unknown>} */ (meta).status,
|
|
39
|
+
options.expectMetaStatus,
|
|
40
|
+
'meta.status'
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
t.ok(Array.isArray(m.words), 'module.words is array')
|
|
44
|
+
if (options.requireNonEmptyWords === true) {
|
|
45
|
+
t.ok(m.words.length >= 1, 'module.words non-empty')
|
|
46
|
+
}
|
|
47
|
+
for (const w of m.words ?? []) {
|
|
48
|
+
if (w == null || typeof w !== 'object') continue
|
|
49
|
+
const entry = /** @type {Record<string, unknown>} */ (w)
|
|
50
|
+
if (Object.prototype.hasOwnProperty.call(entry, 'irSteps')) {
|
|
51
|
+
t.ok(Array.isArray(entry.irSteps), `word ${entry.name}: irSteps is array`)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (options.requireResolvedImportsWhenPresent === true) {
|
|
55
|
+
const imports = m.imports
|
|
56
|
+
if (Array.isArray(imports) && imports.length > 0) {
|
|
57
|
+
for (let i = 0; i < imports.length; i++) {
|
|
58
|
+
const row = imports[i]
|
|
59
|
+
t.ok(row != null && typeof row === 'object', `import[${i}] is object`)
|
|
60
|
+
t.ok(
|
|
61
|
+
typeof /** @type {Record<string, unknown>} */ (row).resolvedPath === 'string',
|
|
62
|
+
`import[${i}].resolvedPath is string`
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|