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