@algosail/lang 0.2.11 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/bin/sail.mjs +4 -0
  2. package/cli/run-cli.js +176 -0
  3. package/index.js +11 -2
  4. package/lib/codegen/README.md +230 -0
  5. package/lib/codegen/codegen-diagnostics.js +164 -0
  6. package/lib/codegen/compile-graph.js +107 -0
  7. package/lib/codegen/emit-adt.js +177 -0
  8. package/lib/codegen/emit-body.js +1265 -0
  9. package/lib/codegen/emit-builtin.js +371 -0
  10. package/lib/codegen/emit-jsdoc-sail.js +383 -0
  11. package/lib/codegen/emit-module.js +498 -0
  12. package/lib/codegen/esm-imports.js +26 -0
  13. package/lib/codegen/index.js +69 -0
  14. package/lib/codegen/out-layout.js +102 -0
  15. package/lib/ffi/extract-jsdoc-sail.js +34 -0
  16. package/lib/io-node/index.js +4 -0
  17. package/lib/io-node/package-root.js +18 -0
  18. package/lib/io-node/read-file.js +12 -0
  19. package/lib/io-node/resolve-package.js +24 -0
  20. package/lib/io-node/resolve-sail-names-from-disk.js +21 -0
  21. package/lib/ir/assert-json-serializable.js +30 -0
  22. package/lib/ir/attach-call-effects.js +108 -0
  23. package/lib/ir/bind-values.js +594 -0
  24. package/lib/ir/build-module-ir.js +290 -0
  25. package/lib/ir/index.js +31 -0
  26. package/lib/ir/lower-body-steps.js +170 -0
  27. package/lib/ir/module-metadata.js +65 -0
  28. package/lib/ir/schema-version.js +15 -0
  29. package/lib/ir/serialize.js +202 -0
  30. package/lib/ir/stitch-types.js +92 -0
  31. package/lib/names/adt-autogen.js +22 -0
  32. package/lib/names/import-path.js +28 -0
  33. package/lib/names/index.js +1 -0
  34. package/lib/names/local-declarations.js +127 -0
  35. package/lib/names/lower-first.js +6 -0
  36. package/lib/names/module-scope.js +120 -0
  37. package/lib/names/resolve-sail.js +365 -0
  38. package/lib/names/walk-ast-refs.js +91 -0
  39. package/lib/parse/ast-build.js +51 -0
  40. package/lib/parse/ast-spec.js +212 -0
  41. package/lib/parse/builtins-set.js +12 -0
  42. package/lib/parse/diagnostics.js +180 -0
  43. package/lib/parse/index.js +46 -0
  44. package/lib/parse/lexer.js +390 -0
  45. package/lib/parse/parse-source.js +912 -0
  46. package/lib/typecheck/adt-autogen-sigs.js +345 -0
  47. package/lib/typecheck/build-type-env.js +148 -0
  48. package/lib/typecheck/builtin-signatures.js +183 -0
  49. package/lib/typecheck/check-word-body.js +1021 -0
  50. package/lib/typecheck/effect-decl.js +124 -0
  51. package/lib/typecheck/index.js +55 -0
  52. package/lib/typecheck/normalize-sig.js +369 -0
  53. package/lib/typecheck/stack-step-snapshots.js +56 -0
  54. package/lib/typecheck/unify-type.js +665 -0
  55. package/lib/typecheck/validate-adt.js +201 -0
  56. package/package.json +4 -9
  57. package/scripts/regen-demo-full-syntax-ast.mjs +22 -0
  58. package/test/cli/sail-cli.test.js +64 -0
  59. package/test/codegen/compile-bracket-ffi-e2e.test.js +64 -0
  60. package/test/codegen/compile-stage0.test.js +128 -0
  61. package/test/codegen/compile-stage4-layout.test.js +124 -0
  62. package/test/codegen/e2e-prelude-ffi-adt/app/extra.sail +6 -0
  63. package/test/codegen/e2e-prelude-ffi-adt/app/lib.sail +33 -0
  64. package/test/codegen/e2e-prelude-ffi-adt/app/main.sail +28 -0
  65. package/test/codegen/e2e-prelude-ffi-adt/artifacts/.gitignore +2 -0
  66. package/test/codegen/e2e-prelude-ffi-adt/ffi/helpers.js +27 -0
  67. package/test/codegen/e2e-prelude-ffi-adt.test.js +100 -0
  68. package/test/codegen/emit-adt-stage6.test.js +168 -0
  69. package/test/codegen/emit-async-stage5.test.js +164 -0
  70. package/test/codegen/emit-body-stage2.test.js +139 -0
  71. package/test/codegen/emit-body.test.js +163 -0
  72. package/test/codegen/emit-builtins-stage7.test.js +258 -0
  73. package/test/codegen/emit-diagnostics-stage9.test.js +90 -0
  74. package/test/codegen/emit-jsdoc-stage8.test.js +113 -0
  75. package/test/codegen/emit-module-stage3.test.js +78 -0
  76. package/test/conformance/conformance-ir-l4.test.js +38 -0
  77. package/test/conformance/conformance-l5-codegen.test.js +111 -0
  78. package/test/conformance/conformance-runner.js +91 -0
  79. package/test/conformance/conformance-suite-l3.test.js +32 -0
  80. package/test/ffi/prelude-jsdoc.test.js +49 -0
  81. package/test/fixtures/demo-full-syntax.ast.json +1471 -0
  82. package/test/fixtures/demo-full-syntax.sail +35 -0
  83. package/test/fixtures/io-node-ffi-adt/ffi.js +7 -0
  84. package/test/fixtures/io-node-ffi-adt/use.sail +4 -0
  85. package/test/fixtures/io-node-mini/dep.sail +2 -0
  86. package/test/fixtures/io-node-mini/entry.sail +4 -0
  87. package/test/fixtures/io-node-prelude/entry.sail +4 -0
  88. package/test/fixtures/io-node-reexport-chain/a.sail +4 -0
  89. package/test/fixtures/io-node-reexport-chain/b.sail +2 -0
  90. package/test/fixtures/io-node-reexport-chain/c.sail +2 -0
  91. package/test/io-node/resolve-disk.test.js +59 -0
  92. package/test/ir/bind-values.test.js +84 -0
  93. package/test/ir/build-module-ir.test.js +100 -0
  94. package/test/ir/call-effects.test.js +97 -0
  95. package/test/ir/ffi-bracket-ir.test.js +59 -0
  96. package/test/ir/full-ir-document.test.js +51 -0
  97. package/test/ir/ir-document-assert.js +67 -0
  98. package/test/ir/lower-body-steps.test.js +90 -0
  99. package/test/ir/module-metadata.test.js +42 -0
  100. package/test/ir/serialization-model.test.js +172 -0
  101. package/test/ir/stitch-types.test.js +74 -0
  102. package/test/names/l2-resolve-adt-autogen.test.js +155 -0
  103. package/test/names/l2-resolve-bracket-ffi.test.js +108 -0
  104. package/test/names/l2-resolve-declaration-and-bracket-errors.test.js +276 -0
  105. package/test/names/l2-resolve-graph.test.js +105 -0
  106. package/test/names/l2-resolve-single-file.test.js +79 -0
  107. package/test/parse/ast-spec.test.js +56 -0
  108. package/test/parse/ast.test.js +476 -0
  109. package/test/parse/contract.test.js +37 -0
  110. package/test/parse/fixtures-full-syntax.test.js +24 -0
  111. package/test/parse/helpers.js +27 -0
  112. package/test/parse/l0-lex-diagnostics-matrix.test.js +59 -0
  113. package/test/parse/l0-lex.test.js +40 -0
  114. package/test/parse/l1-diagnostics.test.js +77 -0
  115. package/test/parse/l1-import.test.js +28 -0
  116. package/test/parse/l1-parse-diagnostics-matrix.test.js +32 -0
  117. package/test/parse/l1-top-level.test.js +47 -0
  118. package/test/parse/l1-types.test.js +31 -0
  119. package/test/parse/l1-words.test.js +49 -0
  120. package/test/parse/l2-diagnostics-contract.test.js +67 -0
  121. package/test/parse/l3-diagnostics-contract.test.js +66 -0
  122. package/test/typecheck/adt-decl-stage2.test.js +83 -0
  123. package/test/typecheck/container-contract-e1309.test.js +258 -0
  124. package/test/typecheck/ffi-bracket-l3.test.js +61 -0
  125. package/test/typecheck/l3-diagnostics-matrix.test.js +248 -0
  126. package/test/typecheck/l3-partial-pipeline.test.js +74 -0
  127. package/test/typecheck/opaque-ffi-type.test.js +78 -0
  128. package/test/typecheck/sig-type-stage3.test.js +190 -0
  129. package/test/typecheck/stack-check-stage4.test.js +149 -0
  130. package/test/typecheck/stack-check-stage5.test.js +74 -0
  131. package/test/typecheck/stack-check-stage6.test.js +56 -0
  132. package/test/typecheck/stack-check-stage7.test.js +160 -0
  133. package/test/typecheck/stack-check-stage8.test.js +146 -0
  134. package/test/typecheck/stack-check-stage9.test.js +105 -0
  135. package/test/typecheck/typecheck-env.test.js +53 -0
  136. package/test/typecheck/typecheck-pipeline.test.js +37 -0
  137. package/README.md +0 -37
  138. package/cli/sail.js +0 -151
  139. package/cli/typecheck.js +0 -39
  140. package/docs/ARCHITECTURE.md +0 -50
  141. package/docs/CHANGELOG.md +0 -18
  142. package/docs/FFI-GUIDE.md +0 -65
  143. package/docs/RELEASE.md +0 -36
  144. package/docs/TESTING.md +0 -86
  145. package/test/integration.test.js +0 -61
@@ -0,0 +1,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,2 @@
1
+ *
2
+ !.gitignore
@@ -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
+ })