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