@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,28 @@
|
|
|
1
|
+
-- E2E: три .sail, FFI, prelude (num/str/list); Point, rot, list, over/nip, quotation+call. --
|
|
2
|
+
+Lib ./lib.sail
|
|
3
|
+
+Extra ./extra.sail
|
|
4
|
+
+Nums @algosail/prelude/num
|
|
5
|
+
+Str @algosail/prelude/str
|
|
6
|
+
+Help ( @prefix @numToStr @tagPrefix ) ../ffi/helpers.js
|
|
7
|
+
|
|
8
|
+
@main ( -> Str )
|
|
9
|
+
3 4 ~Lib/point
|
|
10
|
+
~Lib/sumCoords
|
|
11
|
+
( ~Nums/inc )
|
|
12
|
+
call
|
|
13
|
+
( ~Nums/inc )
|
|
14
|
+
call
|
|
15
|
+
~Extra/double
|
|
16
|
+
~Lib/rotSum
|
|
17
|
+
~Nums/add
|
|
18
|
+
~Lib/listMarker
|
|
19
|
+
~Nums/add
|
|
20
|
+
10
|
|
21
|
+
3
|
|
22
|
+
~Lib/secondPlusTop
|
|
23
|
+
~Nums/add
|
|
24
|
+
~Help/numToStr
|
|
25
|
+
~Help/tagPrefix
|
|
26
|
+
swap
|
|
27
|
+
~Str/concat
|
|
28
|
+
~Help/prefix
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Локальный FFI для e2e: префикс строки и Num → Str.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @sail
|
|
7
|
+
* @prefix ( Str -> Str )
|
|
8
|
+
*/
|
|
9
|
+
export function prefix (s) {
|
|
10
|
+
return `v:${s}`
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @sail
|
|
15
|
+
* @numToStr ( Num -> Str )
|
|
16
|
+
*/
|
|
17
|
+
export function numToStr (n) {
|
|
18
|
+
return String(n)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @sail
|
|
23
|
+
* @tagPrefix ( -> Str )
|
|
24
|
+
*/
|
|
25
|
+
export function tagPrefix () {
|
|
26
|
+
return 'n='
|
|
27
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E2E: три .sail, локальный FFI, prelude num/str/list; Point, rot, list, swap/over/nip/dup,
|
|
3
|
+
* quotation+call (эмиттер может сжать в Nums.inc), второй sail-модуль + extra.sail.
|
|
4
|
+
*/
|
|
5
|
+
import test from 'brittle'
|
|
6
|
+
import fs from 'node:fs'
|
|
7
|
+
import path from 'node:path'
|
|
8
|
+
import { fileURLToPath } from 'node:url'
|
|
9
|
+
import { pathToFileURL } from 'node:url'
|
|
10
|
+
import {
|
|
11
|
+
compileSailToOutDir,
|
|
12
|
+
createReadFileUtf8,
|
|
13
|
+
createResolvePackage
|
|
14
|
+
} from '../../index.js'
|
|
15
|
+
|
|
16
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
17
|
+
/** Корень монорепозитория `sail/` (родитель `lang/`). */
|
|
18
|
+
const monorepoRoot = path.resolve(__dirname, '..', '..', '..')
|
|
19
|
+
const fixtureRoot = path.join(__dirname, 'e2e-prelude-ffi-adt')
|
|
20
|
+
const entryPath = path.join(fixtureRoot, 'app', 'main.sail')
|
|
21
|
+
const artifactsDir = path.join(fixtureRoot, 'artifacts')
|
|
22
|
+
|
|
23
|
+
test('e2e: prelude + FFI + ADT — compile в artifacts/ и исполнение main()', async function (t) {
|
|
24
|
+
fs.rmSync(artifactsDir, { recursive: true, force: true })
|
|
25
|
+
fs.mkdirSync(artifactsDir, { recursive: true })
|
|
26
|
+
fs.writeFileSync(
|
|
27
|
+
path.join(artifactsDir, '.gitignore'),
|
|
28
|
+
'*\n!.gitignore\n',
|
|
29
|
+
'utf8'
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
const readFile = createReadFileUtf8()
|
|
33
|
+
const resolvePackage = createResolvePackage({ projectRoot: monorepoRoot })
|
|
34
|
+
|
|
35
|
+
const r = await compileSailToOutDir({
|
|
36
|
+
entryPath,
|
|
37
|
+
outDir: artifactsDir,
|
|
38
|
+
sourceRoot: monorepoRoot,
|
|
39
|
+
readFile,
|
|
40
|
+
resolvePackage
|
|
41
|
+
})
|
|
42
|
+
t.ok(r.ok === true, r.ok === false ? JSON.stringify(r.diagnostics) : 'compile ok')
|
|
43
|
+
if (!r.ok || !Array.isArray(r.emitted)) {
|
|
44
|
+
t.end()
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
t.ok(r.emitted.length >= 4, 'main + lib + extra + проектный FFI')
|
|
49
|
+
|
|
50
|
+
const norm = (p) => p.replace(/\\/g, '/')
|
|
51
|
+
const mainJs = r.emitted.find((p) => norm(p).endsWith('e2e-prelude-ffi-adt/app/main.js'))
|
|
52
|
+
const libJs = r.emitted.find((p) => norm(p).endsWith('e2e-prelude-ffi-adt/app/lib.js'))
|
|
53
|
+
const extraJs = r.emitted.find((p) => norm(p).endsWith('e2e-prelude-ffi-adt/app/extra.js'))
|
|
54
|
+
t.ok(mainJs != null && fs.existsSync(mainJs), 'main.js в emitted')
|
|
55
|
+
t.ok(libJs != null && fs.existsSync(libJs), 'lib.js в emitted')
|
|
56
|
+
t.ok(extraJs != null && fs.existsSync(extraJs), 'extra.js в emitted')
|
|
57
|
+
|
|
58
|
+
const mainSrc = fs.readFileSync(/** @type {string} */ (mainJs), 'utf8')
|
|
59
|
+
t.ok(mainSrc.includes('__adt_Lib__point'), 'product ADT → конструктор')
|
|
60
|
+
t.ok(
|
|
61
|
+
/import\s+\*\s+as\s+Nums\s+from\s+['"]@algosail\/prelude\/num['"]/.test(mainSrc),
|
|
62
|
+
'prelude num — npm-спецификатор'
|
|
63
|
+
)
|
|
64
|
+
t.ok(
|
|
65
|
+
/import\s+\*\s+as\s+Str\s+from\s+['"]@algosail\/prelude\/str['"]/.test(mainSrc),
|
|
66
|
+
'prelude str'
|
|
67
|
+
)
|
|
68
|
+
t.ok(
|
|
69
|
+
!r.emitted.some((p) => norm(p).includes('/prelude/lib/')),
|
|
70
|
+
'пакет @algosail/prelude не копируется в out-dir'
|
|
71
|
+
)
|
|
72
|
+
t.ok(mainSrc.includes('Nums.inc'), 'quotation/call → inc (или цепочка inc)')
|
|
73
|
+
t.ok(mainSrc.includes('Lib.sumCoords'), 'импорт второго sail-модуля')
|
|
74
|
+
t.ok(mainSrc.includes('Extra.double'), 'третий модуль extra.sail')
|
|
75
|
+
t.ok(mainSrc.includes('Lib.rotSum'), 'rot + add в lib')
|
|
76
|
+
t.ok(mainSrc.includes('Lib.listMarker'), 'listMarker в lib (внутри — prelude list)')
|
|
77
|
+
t.ok(mainSrc.includes('Lib.secondPlusTop'), 'swap/over/add/nip')
|
|
78
|
+
|
|
79
|
+
const libSrc = fs.readFileSync(/** @type {string} */ (libJs), 'utf8')
|
|
80
|
+
t.ok(libSrc.includes('Nums.add'), 'геттеры Point + add')
|
|
81
|
+
t.ok(
|
|
82
|
+
/import\s+\*\s+as\s+Lists\s+from\s+['"]@algosail\/prelude\/list['"]/.test(libSrc),
|
|
83
|
+
'lib: npm @algosail/prelude/list'
|
|
84
|
+
)
|
|
85
|
+
t.ok(
|
|
86
|
+
/Lists\.(listOfOne|listLength)/.test(libSrc),
|
|
87
|
+
'lib: listOfOne / listLength'
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
const extraSrc = fs.readFileSync(/** @type {string} */ (extraJs), 'utf8')
|
|
91
|
+
t.ok(extraSrc.includes('Nums.add'), 'extra: dup → add (double)')
|
|
92
|
+
|
|
93
|
+
const mod = await import(pathToFileURL(/** @type {string} */ (mainJs)).href)
|
|
94
|
+
t.is(
|
|
95
|
+
mod.main(),
|
|
96
|
+
'v:n=41',
|
|
97
|
+
'цепочка: point+sum=7, +2 inc=9, double=18, +rotSum=27, +list=28, +secondPlusTop(10,3)=41'
|
|
98
|
+
)
|
|
99
|
+
t.end()
|
|
100
|
+
})
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L5 этап 6: ADT autogen, list literal, слоты (RFC-0.1 §7–8, RFC-compile §8).
|
|
3
|
+
*/
|
|
4
|
+
import test from 'brittle'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import { typecheckSail } from '../../index.js'
|
|
7
|
+
import {
|
|
8
|
+
emitModuleEsmSource,
|
|
9
|
+
resolveCompileLayout
|
|
10
|
+
} from '../../lib/codegen/index.js'
|
|
11
|
+
|
|
12
|
+
const root = path.resolve('/virtual/sail-codegen-adt6')
|
|
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('stage6: Maybe — локальный autogen, maybe без export, вызов w', function (t) {
|
|
22
|
+
const sourceRoot = path.join(root, 'maybe')
|
|
23
|
+
const main = path.join(sourceRoot, 'main.sail')
|
|
24
|
+
const src = [
|
|
25
|
+
'&Maybe a',
|
|
26
|
+
'| Nothing',
|
|
27
|
+
'| Just a',
|
|
28
|
+
'',
|
|
29
|
+
'@nothingQuot ( -> q:( ~s -> ~s ) )',
|
|
30
|
+
' ( )',
|
|
31
|
+
'',
|
|
32
|
+
'@justQuot ( -> q:( ~s a -> ~s ) )',
|
|
33
|
+
' ( drop )',
|
|
34
|
+
'',
|
|
35
|
+
'@w ( ~s (Maybe a) -> ~s )',
|
|
36
|
+
' /nothingQuot',
|
|
37
|
+
' /justQuot',
|
|
38
|
+
' /maybe',
|
|
39
|
+
''
|
|
40
|
+
].join('\n')
|
|
41
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
|
|
42
|
+
t.ok(r.ok, r.diagnostics?.map((d) => d.message).join('; '))
|
|
43
|
+
const layout = resolveCompileLayout({
|
|
44
|
+
entryPath: main,
|
|
45
|
+
outDir: path.join(sourceRoot, '_out'),
|
|
46
|
+
sourceRoot
|
|
47
|
+
})
|
|
48
|
+
const g = emitModuleEsmSource({ env: r.env, modulePath: main, layout })
|
|
49
|
+
t.ok(g.ok, g.ok === false ? g.diagnostics?.[0]?.message : '')
|
|
50
|
+
const s = g.source
|
|
51
|
+
t.ok(/\bfunction maybe\s*\(/.test(s), 'локальная function maybe')
|
|
52
|
+
t.ok(!/\bexport function maybe\b/.test(s), 'maybe не экспортируется')
|
|
53
|
+
t.ok(/\bexport function w\b/.test(s), 'w экспортируется')
|
|
54
|
+
t.ok(/maybe\(p0, p1,/.test(s), 'maybe с префиксом стека s + дискриминант')
|
|
55
|
+
t.end()
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('stage6: только /just — в файле нет function maybe', function (t) {
|
|
59
|
+
const sourceRoot = path.join(root, 'just-only')
|
|
60
|
+
const main = path.join(sourceRoot, 'main.sail')
|
|
61
|
+
const src = [
|
|
62
|
+
'&Maybe a',
|
|
63
|
+
'| Nothing',
|
|
64
|
+
'| Just a',
|
|
65
|
+
'',
|
|
66
|
+
'@main ( Num -> (Maybe a) )',
|
|
67
|
+
' /just',
|
|
68
|
+
''
|
|
69
|
+
].join('\n')
|
|
70
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
|
|
71
|
+
t.ok(r.ok, r.diagnostics?.map((d) => d.message).join('; '))
|
|
72
|
+
const layout = resolveCompileLayout({
|
|
73
|
+
entryPath: main,
|
|
74
|
+
outDir: path.join(sourceRoot, '_out'),
|
|
75
|
+
sourceRoot
|
|
76
|
+
})
|
|
77
|
+
const g = emitModuleEsmSource({ env: r.env, modulePath: main, layout })
|
|
78
|
+
t.ok(g.ok)
|
|
79
|
+
t.ok(/\bfunction just\b/.test(g.source), 'just autogen')
|
|
80
|
+
t.ok(!/\bfunction maybe\b/.test(g.source), 'неиспользуемый maybe не эмитится')
|
|
81
|
+
t.ok(!/\bexport function just\b/.test(g.source), 'just не export')
|
|
82
|
+
t.end()
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
test('stage6: Point — point, withPoint, геттер', function (t) {
|
|
86
|
+
const sourceRoot = path.join(root, 'point')
|
|
87
|
+
const main = path.join(sourceRoot, 'main.sail')
|
|
88
|
+
const src = [
|
|
89
|
+
'&Point',
|
|
90
|
+
':x Num',
|
|
91
|
+
':y Num',
|
|
92
|
+
'',
|
|
93
|
+
'@mk ( -> Point )',
|
|
94
|
+
' 1 2 /point',
|
|
95
|
+
'',
|
|
96
|
+
'@q ( -> q:( ~s Num Num -> ~s ) )',
|
|
97
|
+
' ( nip drop )',
|
|
98
|
+
'',
|
|
99
|
+
'@w ( Point -> )',
|
|
100
|
+
' /q',
|
|
101
|
+
' /withPoint',
|
|
102
|
+
'',
|
|
103
|
+
'@getx ( Point -> Num )',
|
|
104
|
+
' /xPoint',
|
|
105
|
+
''
|
|
106
|
+
].join('\n')
|
|
107
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
|
|
108
|
+
t.ok(r.ok, r.diagnostics?.map((d) => d.message).join('; '))
|
|
109
|
+
const layout = resolveCompileLayout({
|
|
110
|
+
entryPath: main,
|
|
111
|
+
outDir: path.join(sourceRoot, '_out'),
|
|
112
|
+
sourceRoot
|
|
113
|
+
})
|
|
114
|
+
const g = emitModuleEsmSource({ env: r.env, modulePath: main, layout })
|
|
115
|
+
t.ok(g.ok, g.ok === false ? g.diagnostics?.[0]?.message : '')
|
|
116
|
+
const s = g.source
|
|
117
|
+
t.ok(/\bfunction point\s*\(/.test(s), 'point')
|
|
118
|
+
t.ok(/\bfunction withPoint\s*\(/.test(s), 'withPoint')
|
|
119
|
+
t.ok(/\bfunction xPoint\s*\(/.test(s), 'xPoint')
|
|
120
|
+
t.ok(!/\bexport function point\b/.test(s), 'point не export')
|
|
121
|
+
t.ok(/return \{ "x": p\d+, "y": p\d+ \}/.test(s), 'product ctor — plain object')
|
|
122
|
+
t.end()
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
test('stage6: list literal → JS массив', function (t) {
|
|
126
|
+
const sourceRoot = path.join(root, 'listlit')
|
|
127
|
+
const main = path.join(sourceRoot, 'main.sail')
|
|
128
|
+
const src = ['@f ( -> (List Num) )', ' [ 1 2 3 ]', ''].join('\n')
|
|
129
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
|
|
130
|
+
t.ok(r.ok, r.diagnostics?.map((d) => d.message).join('; '))
|
|
131
|
+
const layout = resolveCompileLayout({
|
|
132
|
+
entryPath: main,
|
|
133
|
+
outDir: path.join(sourceRoot, '_out'),
|
|
134
|
+
sourceRoot
|
|
135
|
+
})
|
|
136
|
+
const g = emitModuleEsmSource({ env: r.env, modulePath: main, layout })
|
|
137
|
+
t.ok(g.ok)
|
|
138
|
+
t.ok(/\[1, 2, 3\]/.test(g.source), 'литерал списка')
|
|
139
|
+
t.end()
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
test('stage6: квалифицированный autogen ~Lib/just — локальная __adt_*', function (t) {
|
|
143
|
+
const sourceRoot = path.join(root, 'qual')
|
|
144
|
+
const main = path.join(sourceRoot, 'main.sail')
|
|
145
|
+
const lib = path.join(sourceRoot, 'lib.sail')
|
|
146
|
+
const files = {
|
|
147
|
+
[lib]: ['&Maybe a', '| Nothing', '| Just a', ''].join('\n'),
|
|
148
|
+
[main]: [
|
|
149
|
+
'+Lib ( &Maybe ) ./lib.sail',
|
|
150
|
+
'',
|
|
151
|
+
'@main ( -> (Maybe Num) )',
|
|
152
|
+
' 42 ~Lib/just',
|
|
153
|
+
''
|
|
154
|
+
].join('\n')
|
|
155
|
+
}
|
|
156
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs(files) })
|
|
157
|
+
t.ok(r.ok, r.diagnostics?.map((d) => d.message).join('; '))
|
|
158
|
+
const layout = resolveCompileLayout({
|
|
159
|
+
entryPath: main,
|
|
160
|
+
outDir: path.join(sourceRoot, '_out'),
|
|
161
|
+
sourceRoot
|
|
162
|
+
})
|
|
163
|
+
const g = emitModuleEsmSource({ env: r.env, modulePath: main, layout })
|
|
164
|
+
t.ok(g.ok, g.ok === false ? g.diagnostics?.[0]?.message : '')
|
|
165
|
+
t.ok(/function __adt_Lib__just/.test(g.source), 'mangled qualified just')
|
|
166
|
+
t.ok(g.source.includes('import * as Lib'), 'импорт Lib для резолва')
|
|
167
|
+
t.end()
|
|
168
|
+
})
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L5 этап 5: +Async / await (RFC-compile §10; IR вариант B для `-Async`, await по сигнатуре callee).
|
|
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 { typecheckSail } from '../../index.js'
|
|
10
|
+
import {
|
|
11
|
+
compileSailToOutDir,
|
|
12
|
+
emitModuleEsmSource,
|
|
13
|
+
resolveCompileLayout
|
|
14
|
+
} from '../../lib/codegen/index.js'
|
|
15
|
+
import { irStepsCallsPropagatingAsyncCallee } from '../../lib/codegen/emit-body.js'
|
|
16
|
+
import { createCalleeAsyncResolverForModule } from '../../lib/codegen/emit-module.js'
|
|
17
|
+
import { buildModuleIr } from '../../lib/ir/index.js'
|
|
18
|
+
|
|
19
|
+
const root = path.resolve('/virtual/sail-codegen-async5')
|
|
20
|
+
|
|
21
|
+
function vfs (files) {
|
|
22
|
+
const norm = Object.fromEntries(
|
|
23
|
+
Object.entries(files).map(([k, v]) => [path.normalize(k), v])
|
|
24
|
+
)
|
|
25
|
+
return (p) => norm[path.normalize(p)] ?? null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
test('irStepsCallsPropagatingAsyncCallee: обход call/quotation по сигнатурам (вариант B)', function (t) {
|
|
29
|
+
const main = path.join(root, 'scan.sail')
|
|
30
|
+
const src = [
|
|
31
|
+
'@asyncCallee ( -> +Async )',
|
|
32
|
+
'',
|
|
33
|
+
'@good ( -> +Async )',
|
|
34
|
+
' ( /asyncCallee )',
|
|
35
|
+
' call',
|
|
36
|
+
''
|
|
37
|
+
].join('\n')
|
|
38
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
|
|
39
|
+
t.ok(r.ok, r.diagnostics?.map((d) => d.message).join('; '))
|
|
40
|
+
const norm = path.normalize(main)
|
|
41
|
+
const payload = buildModuleIr(r.env, norm, {
|
|
42
|
+
moduleStatus: 'ok',
|
|
43
|
+
valueBindings: true
|
|
44
|
+
})
|
|
45
|
+
const good = payload.words.find((w) => w.name === 'good')
|
|
46
|
+
t.ok(good && Array.isArray(good.irSteps))
|
|
47
|
+
const resolver = createCalleeAsyncResolverForModule(r.env, norm, payload)
|
|
48
|
+
t.ok(
|
|
49
|
+
irStepsCallsPropagatingAsyncCallee(good.irSteps, resolver),
|
|
50
|
+
'по сигнатуре asyncCallee внутри call'
|
|
51
|
+
)
|
|
52
|
+
t.end()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('emit stage5: caller — async function и await asyncCallee', function (t) {
|
|
56
|
+
const main = path.join(root, 'caller.sail')
|
|
57
|
+
const src = [
|
|
58
|
+
'@asyncCallee ( -> +Async )',
|
|
59
|
+
'',
|
|
60
|
+
'@caller ( -> +Async )',
|
|
61
|
+
' /asyncCallee',
|
|
62
|
+
''
|
|
63
|
+
].join('\n')
|
|
64
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
|
|
65
|
+
t.ok(r.ok, r.diagnostics?.map((d) => d.message).join('; '))
|
|
66
|
+
const layout = resolveCompileLayout({
|
|
67
|
+
entryPath: main,
|
|
68
|
+
outDir: path.join(root, '_out'),
|
|
69
|
+
sourceRoot: root
|
|
70
|
+
})
|
|
71
|
+
const gen = emitModuleEsmSource({ env: r.env, modulePath: main, layout })
|
|
72
|
+
t.ok(gen.ok, gen.ok === false ? gen.diagnostics?.[0]?.message : '')
|
|
73
|
+
t.ok(
|
|
74
|
+
/\bexport async function caller\s*\(/.test(gen.source),
|
|
75
|
+
'caller: export async function'
|
|
76
|
+
)
|
|
77
|
+
t.ok(
|
|
78
|
+
/await\s+asyncCallee\s*\(/.test(gen.source),
|
|
79
|
+
'тело caller: await asyncCallee('
|
|
80
|
+
)
|
|
81
|
+
t.end()
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('emit stage5: good — quotation+call, await внутри async function good', function (t) {
|
|
85
|
+
const main = path.join(root, 'good.sail')
|
|
86
|
+
const src = [
|
|
87
|
+
'@asyncCallee ( -> +Async )',
|
|
88
|
+
'',
|
|
89
|
+
'@good ( -> +Async )',
|
|
90
|
+
' ( /asyncCallee )',
|
|
91
|
+
' call',
|
|
92
|
+
''
|
|
93
|
+
].join('\n')
|
|
94
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
|
|
95
|
+
t.ok(r.ok, r.diagnostics?.map((d) => d.message).join('; '))
|
|
96
|
+
const layout = resolveCompileLayout({
|
|
97
|
+
entryPath: main,
|
|
98
|
+
outDir: path.join(root, '_out'),
|
|
99
|
+
sourceRoot: root
|
|
100
|
+
})
|
|
101
|
+
const gen = emitModuleEsmSource({ env: r.env, modulePath: main, layout })
|
|
102
|
+
t.ok(gen.ok, gen.ok === false ? gen.diagnostics?.[0]?.message : '')
|
|
103
|
+
t.ok(/\bexport async function good\s*\(/.test(gen.source), 'good: async')
|
|
104
|
+
t.ok(/await\s+asyncCallee\s*\(/.test(gen.source), 'инлайн тело: await asyncCallee')
|
|
105
|
+
t.end()
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
test('emit stage5: wrap (-Async) — async + await по сигнатуре callee (§10.5, вариант B)', function (t) {
|
|
109
|
+
const main = path.join(root, 'wrap.sail')
|
|
110
|
+
const src = [
|
|
111
|
+
'@asyncCallee ( -> +Async )',
|
|
112
|
+
'',
|
|
113
|
+
'@wrap ( -> -Async )',
|
|
114
|
+
' /asyncCallee',
|
|
115
|
+
''
|
|
116
|
+
].join('\n')
|
|
117
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
|
|
118
|
+
t.ok(r.ok, r.diagnostics?.map((d) => d.message).join('; '))
|
|
119
|
+
const layout = resolveCompileLayout({
|
|
120
|
+
entryPath: main,
|
|
121
|
+
outDir: path.join(root, '_out'),
|
|
122
|
+
sourceRoot: root
|
|
123
|
+
})
|
|
124
|
+
const gen = emitModuleEsmSource({ env: r.env, modulePath: main, layout })
|
|
125
|
+
t.ok(gen.ok, gen.ok === false ? gen.diagnostics?.[0]?.message : '')
|
|
126
|
+
t.ok(/\bexport async function wrap\s*\(/.test(gen.source), 'wrap: async')
|
|
127
|
+
t.ok(/await\s+asyncCallee\s*\(/.test(gen.source), 'await asyncCallee')
|
|
128
|
+
t.end()
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
test('compile stage5: сгенерированный ESM парсится и asyncCaller завершает Promise', async function (t) {
|
|
132
|
+
const main = path.join(root, 'chain.sail')
|
|
133
|
+
const src = [
|
|
134
|
+
'@asyncCallee ( -> +Async Num )',
|
|
135
|
+
'',
|
|
136
|
+
' 7',
|
|
137
|
+
'',
|
|
138
|
+
'@caller ( -> +Async Num )',
|
|
139
|
+
'',
|
|
140
|
+
' /asyncCallee',
|
|
141
|
+
''
|
|
142
|
+
].join('\n')
|
|
143
|
+
const outDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sail-cg-async5-'))
|
|
144
|
+
const layout = resolveCompileLayout({
|
|
145
|
+
entryPath: main,
|
|
146
|
+
outDir,
|
|
147
|
+
sourceRoot: root
|
|
148
|
+
})
|
|
149
|
+
const r = await compileSailToOutDir({
|
|
150
|
+
entryPath: main,
|
|
151
|
+
outDir,
|
|
152
|
+
readFile: vfs({ [main]: src }),
|
|
153
|
+
layout
|
|
154
|
+
})
|
|
155
|
+
t.ok(r.ok, r.diagnostics?.map((d) => d.message).join('; '))
|
|
156
|
+
const jsPath = path.join(outDir, 'chain.js')
|
|
157
|
+
t.ok(fs.existsSync(jsPath))
|
|
158
|
+
const mod = await import(pathToFileURL(jsPath).href)
|
|
159
|
+
t.is(typeof mod.caller, 'function')
|
|
160
|
+
const val = await mod.caller()
|
|
161
|
+
t.is(val, 7)
|
|
162
|
+
fs.rmSync(outDir, { recursive: true, force: true })
|
|
163
|
+
t.end()
|
|
164
|
+
})
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L5 этап 2: quotation, `call`, значения Quote (RFC-compile §6.3).
|
|
3
|
+
*/
|
|
4
|
+
import test from 'brittle'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import { typecheckSail } from '../../index.js'
|
|
7
|
+
import { buildModuleIr } from '../../lib/ir/index.js'
|
|
8
|
+
import { emitWordBodyIr } from '../../lib/codegen/emit-body.js'
|
|
9
|
+
|
|
10
|
+
const root = path.resolve('/virtual/sail-codegen-stage2')
|
|
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('emit-body stage2: ( 1 ) call — инлайн, без const qN до вызова', function (t) {
|
|
19
|
+
const main = path.join(root, 'call-num.sail')
|
|
20
|
+
const src = ['@w ( -> Num )', '', ' ( 1 )', '', ' call', ''].join('\n')
|
|
21
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
|
|
22
|
+
t.ok(r.ok, r.diagnostics?.map((d) => d.message).join('; '))
|
|
23
|
+
const norm = path.normalize(main)
|
|
24
|
+
const payload = buildModuleIr(r.env, norm, {
|
|
25
|
+
moduleStatus: 'ok',
|
|
26
|
+
valueBindings: true
|
|
27
|
+
})
|
|
28
|
+
const w = payload.words.find((x) => x.name === 'w')
|
|
29
|
+
const { source } = emitWordBodyIr(w.irSteps, {
|
|
30
|
+
normalizedSig: w.normalizedSig,
|
|
31
|
+
wordName: 'w',
|
|
32
|
+
strict: true,
|
|
33
|
+
entryStackIds: w.entryStackIds
|
|
34
|
+
})
|
|
35
|
+
// Эвристика этапа 2: литеральная quotation не оборачивается в функцию до call
|
|
36
|
+
t.ok(
|
|
37
|
+
!/\bconst\s+q\d+\s*=/.test(source),
|
|
38
|
+
'не должно быть const qN = (отложенная эмиссия + инлайн при call)'
|
|
39
|
+
)
|
|
40
|
+
t.ok(/const v\d+ = 1/.test(source), 'литерал 1 внутри инлайна')
|
|
41
|
+
t.ok(/\breturn\s+v\d+/.test(source), 'return одного значения')
|
|
42
|
+
t.end()
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('emit-body stage2: передача Quote в локальное слово — const qN в точке вызова', function (t) {
|
|
46
|
+
const main = path.join(root, 'hof.sail')
|
|
47
|
+
const src = [
|
|
48
|
+
'@apply0 ( q:( -> Num ) -> Num )',
|
|
49
|
+
'',
|
|
50
|
+
' call',
|
|
51
|
+
'',
|
|
52
|
+
'@main ( -> Num )',
|
|
53
|
+
'',
|
|
54
|
+
' ( 7 )',
|
|
55
|
+
'',
|
|
56
|
+
' /apply0',
|
|
57
|
+
''
|
|
58
|
+
].join('\n')
|
|
59
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
|
|
60
|
+
t.ok(r.ok, r.diagnostics?.map((d) => d.message).join('; '))
|
|
61
|
+
const norm = path.normalize(main)
|
|
62
|
+
const payload = buildModuleIr(r.env, norm, {
|
|
63
|
+
moduleStatus: 'ok',
|
|
64
|
+
valueBindings: true
|
|
65
|
+
})
|
|
66
|
+
const mainWord = payload.words.find((x) => x.name === 'main')
|
|
67
|
+
const { source } = emitWordBodyIr(mainWord.irSteps, {
|
|
68
|
+
normalizedSig: mainWord.normalizedSig,
|
|
69
|
+
wordName: 'main',
|
|
70
|
+
strict: true,
|
|
71
|
+
entryStackIds: mainWord.entryStackIds
|
|
72
|
+
})
|
|
73
|
+
t.ok(/\bconst\s+q\d+\s*=/.test(source), 'именованная функция-quotation const qN')
|
|
74
|
+
t.ok(/apply0\s*\(\s*q\d+\s*\)/.test(source), 'вызов apply0(qN)')
|
|
75
|
+
t.end()
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('emit-body stage2: исполнение ( 1 ) call через new Function', function (t) {
|
|
79
|
+
const main = path.join(root, 'exec-call.sail')
|
|
80
|
+
const src = ['@w ( -> Num )', '', ' ( 1 )', '', ' call', ''].join('\n')
|
|
81
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
|
|
82
|
+
t.ok(r.ok, r.diagnostics?.map((d) => d.message).join('; '))
|
|
83
|
+
const norm = path.normalize(main)
|
|
84
|
+
const payload = buildModuleIr(r.env, norm, {
|
|
85
|
+
moduleStatus: 'ok',
|
|
86
|
+
valueBindings: true
|
|
87
|
+
})
|
|
88
|
+
const w = payload.words.find((x) => x.name === 'w')
|
|
89
|
+
const { source } = emitWordBodyIr(w.irSteps, {
|
|
90
|
+
normalizedSig: w.normalizedSig,
|
|
91
|
+
wordName: 'w',
|
|
92
|
+
strict: true,
|
|
93
|
+
entryStackIds: w.entryStackIds
|
|
94
|
+
})
|
|
95
|
+
const fn = new Function(`${source}`)
|
|
96
|
+
t.is(fn(), 1)
|
|
97
|
+
t.end()
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
test('emit-body stage2: исполнение main с apply0 и qN', function (t) {
|
|
101
|
+
const main = path.join(root, 'exec-hof.sail')
|
|
102
|
+
const src = [
|
|
103
|
+
'@apply0 ( q:( -> Num ) -> Num )',
|
|
104
|
+
'',
|
|
105
|
+
' call',
|
|
106
|
+
'',
|
|
107
|
+
'@main ( -> Num )',
|
|
108
|
+
'',
|
|
109
|
+
' ( 7 )',
|
|
110
|
+
'',
|
|
111
|
+
' /apply0',
|
|
112
|
+
''
|
|
113
|
+
].join('\n')
|
|
114
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
|
|
115
|
+
t.ok(r.ok, r.diagnostics?.map((d) => d.message).join('; '))
|
|
116
|
+
const norm = path.normalize(main)
|
|
117
|
+
const payload = buildModuleIr(r.env, norm, {
|
|
118
|
+
moduleStatus: 'ok',
|
|
119
|
+
valueBindings: true
|
|
120
|
+
})
|
|
121
|
+
const apply0w = payload.words.find((x) => x.name === 'apply0')
|
|
122
|
+
const mainWord = payload.words.find((x) => x.name === 'main')
|
|
123
|
+
const bodyApply0 = emitWordBodyIr(apply0w.irSteps, {
|
|
124
|
+
normalizedSig: apply0w.normalizedSig,
|
|
125
|
+
wordName: 'apply0',
|
|
126
|
+
strict: true,
|
|
127
|
+
entryStackIds: apply0w.entryStackIds
|
|
128
|
+
}).source
|
|
129
|
+
const bodyMain = emitWordBodyIr(mainWord.irSteps, {
|
|
130
|
+
normalizedSig: mainWord.normalizedSig,
|
|
131
|
+
wordName: 'main',
|
|
132
|
+
strict: true,
|
|
133
|
+
entryStackIds: mainWord.entryStackIds
|
|
134
|
+
}).source
|
|
135
|
+
const wrapped = `function apply0(q){${bodyApply0}}\nfunction main(){${bodyMain}}\nreturn main();`
|
|
136
|
+
const fn = new Function(wrapped)
|
|
137
|
+
t.is(fn(), 7)
|
|
138
|
+
t.end()
|
|
139
|
+
})
|