@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.
Files changed (145) hide show
  1. package/bin/sail.mjs +4 -0
  2. package/cli/run-cli.js +176 -0
  3. package/index.js +12 -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 +34 -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,107 @@
1
+ /**
2
+ * L5: обход модулей после успешного typecheck; эмиссия ESM по модулю (RFC-compile §4, §9).
3
+ */
4
+ import path from 'node:path'
5
+ import { e5301ReadFileRequired, e5302CannotReadFfi } from './codegen-diagnostics.js'
6
+ import { emitModuleEsmSource } from './emit-module.js'
7
+ import {
8
+ isPathInsideSourceRoot,
9
+ projectJsFileToOutputPath,
10
+ sailModuleToOutputJsPath
11
+ } from './out-layout.js'
12
+
13
+ /**
14
+ * Проектные FFI `.js` для зеркала в `out-dir`: только импорты с относительным `path` в `.sail`
15
+ * (`./` / `../`). Пакеты npm (`@scope/pkg/…`, bare) не копируются — в ESM остаётся тот же
16
+ * спецификатор, резолв через `node_modules` (RFC-compile §9.2).
17
+ *
18
+ * @param {object} env
19
+ * @param {string} sourceRoot
20
+ * @returns {Set<string>}
21
+ */
22
+ export function collectProjectFfiJsPathsForCodegenCopy (env, sourceRoot) {
23
+ /** @type {Set<string>} */
24
+ const set = new Set()
25
+ for (const modPath of env.modulePathsOrdered) {
26
+ const normMod = path.normalize(modPath)
27
+ if (path.extname(normMod).toLowerCase() !== '.sail') continue
28
+ const snap = env.snapshots.get(normMod)
29
+ const scope = env.scopeByPath.get(normMod)
30
+ if (!snap?.ok || !Array.isArray(snap.items) || !scope?.importMap) continue
31
+ for (const item of snap.items) {
32
+ if (item.kind !== 'import') continue
33
+ const spec = item.path
34
+ if (typeof spec !== 'string') continue
35
+ if (!spec.startsWith('./') && !spec.startsWith('../')) continue
36
+ const dep = scope.importMap.get(item.module)
37
+ const p =
38
+ dep != null && typeof dep === 'object' && typeof dep.path === 'string'
39
+ ? dep.path
40
+ : null
41
+ if (p == null) continue
42
+ const abs = path.normalize(p)
43
+ if (path.extname(abs).toLowerCase() !== '.js') continue
44
+ if (!isPathInsideSourceRoot(abs, sourceRoot)) continue
45
+ set.add(abs)
46
+ }
47
+ }
48
+ return set
49
+ }
50
+
51
+ /**
52
+ * @param {object} env — `TypecheckEnv` после успешного `typecheckSail` (`modulePathsOrdered`, …)
53
+ * @param {{ outDir: string, sourceRoot: string, entryPath: string }} layout — {@link resolveCompileLayout}
54
+ * @param {{ mkdir: (p: string, opts?: { recursive?: boolean }) => Promise<void>, writeFile: (p: string, data: string) => Promise<void> }} fsSink
55
+ * @param {(p: string) => string | null | undefined} readFile — для копирования проектных FFI `.js` (RFC-compile §9.1)
56
+ * @returns {Promise<{ ok: true, diagnostics: [], emitted: string[] } | { ok: false, diagnostics: object[], emitted: [] }>}
57
+ */
58
+ export async function emitCodegenStage0Stub (env, layout, fsSink, readFile) {
59
+ /** @type {string[]} */
60
+ const emitted = []
61
+ const sourceRoot = layout.sourceRoot
62
+
63
+ const ffiSet = collectProjectFfiJsPathsForCodegenCopy(env, sourceRoot)
64
+ const ffiSorted = [...ffiSet].sort()
65
+ if (ffiSorted.length > 0 && typeof readFile !== 'function') {
66
+ return {
67
+ ok: false,
68
+ diagnostics: [e5301ReadFileRequired()],
69
+ emitted: []
70
+ }
71
+ }
72
+
73
+ for (const jsPath of ffiSorted) {
74
+ const mapped = projectJsFileToOutputPath(layout, jsPath)
75
+ if (!mapped.ok) {
76
+ return { ok: false, diagnostics: [mapped.diagnostic], emitted: [] }
77
+ }
78
+ const text = readFile(jsPath)
79
+ if (text == null || text === undefined) {
80
+ return {
81
+ ok: false,
82
+ diagnostics: [e5302CannotReadFfi(jsPath)],
83
+ emitted: []
84
+ }
85
+ }
86
+ await fsSink.mkdir(path.dirname(mapped.path), { recursive: true })
87
+ await fsSink.writeFile(mapped.path, text)
88
+ emitted.push(mapped.path)
89
+ }
90
+
91
+ for (const modPath of env.modulePathsOrdered) {
92
+ const norm = path.normalize(modPath)
93
+ if (path.extname(norm).toLowerCase() !== '.sail') continue
94
+ const mapped = sailModuleToOutputJsPath(layout, norm)
95
+ if (!mapped.ok) {
96
+ return { ok: false, diagnostics: [mapped.diagnostic], emitted: [] }
97
+ }
98
+ const gen = emitModuleEsmSource({ env, modulePath: norm, layout })
99
+ if (!gen.ok) {
100
+ return { ok: false, diagnostics: gen.diagnostics, emitted: [] }
101
+ }
102
+ await fsSink.mkdir(path.dirname(mapped.path), { recursive: true })
103
+ await fsSink.writeFile(mapped.path, gen.source)
104
+ emitted.push(mapped.path)
105
+ }
106
+ return { ok: true, diagnostics: [], emitted }
107
+ }
@@ -0,0 +1,177 @@
1
+ /**
2
+ * L5 этап 6: тела автоген-слов ADT (RFC-0.1 §7, RFC-compile §8).
3
+ * Без синтетического IR в buildModuleIr: только строка тела для локальной `function`.
4
+ */
5
+ import { lowerFirst } from '../names/lower-first.js'
6
+
7
+ /**
8
+ * @typedef {import('../typecheck/normalize-sig.js').NormalizedSignature} NormalizedSignature
9
+ */
10
+
11
+ /**
12
+ * @param {string} wordName
13
+ * @param {{ kind: string, ast: object }} adtEntry
14
+ * @returns {{
15
+ * kind: 'sum_elim'|'sum_ctor'|'prod_ctor'|'prod_get'|'prod_with'
16
+ * ast: object
17
+ * tag?: object
18
+ * field?: object
19
+ * } | null}
20
+ */
21
+ export function classifyAutogenWord (wordName, adtEntry) {
22
+ const ast = adtEntry.ast
23
+ if (adtEntry.kind === 'sum') {
24
+ const elim = lowerFirst(ast.name)
25
+ if (wordName === elim) return { kind: 'sum_elim', ast }
26
+ for (const tag of ast.tags ?? []) {
27
+ if (lowerFirst(tag.name) === wordName) {
28
+ return { kind: 'sum_ctor', ast, tag }
29
+ }
30
+ }
31
+ } else if (adtEntry.kind === 'product') {
32
+ if (lowerFirst(ast.name) === wordName) return { kind: 'prod_ctor', ast }
33
+ const withN = lowerFirst(`with${ast.name}`)
34
+ if (wordName === withN) return { kind: 'prod_with', ast }
35
+ for (const f of ast.fields ?? []) {
36
+ if (lowerFirst(f.name + ast.name) === wordName) {
37
+ return { kind: 'prod_get', ast, field: f }
38
+ }
39
+ }
40
+ }
41
+ return null
42
+ }
43
+
44
+ /**
45
+ * @param {import('../typecheck/build-type-env.js').TypecheckEnv} env
46
+ * @param {string} modulePath
47
+ * @param {string} wordName
48
+ * @returns {ReturnType<typeof classifyAutogenWord>}
49
+ */
50
+ export function findAutogenRoleInModule (env, modulePath, wordName) {
51
+ const adtMap = env.adtByPath?.get(modulePath)
52
+ if (!(adtMap instanceof Map)) return null
53
+ for (const entry of adtMap.values()) {
54
+ const role = classifyAutogenWord(wordName, entry)
55
+ if (role) return role
56
+ }
57
+ return null
58
+ }
59
+
60
+ /**
61
+ * @param {object} sumAst
62
+ */
63
+ function emitSumEliminatorBody (sumAst) {
64
+ const tags = sumAst.tags ?? []
65
+ const n = tags.length
66
+ const pNames = tags.map((_, i) => `p${i + 1}`)
67
+ const declP = pNames.length ? `, ${pNames.join(', ')}` : ''
68
+ /** @type {string[]} */
69
+ const lines = [
70
+ `const nB = ${n}`,
71
+ `let s0, d${declP}`,
72
+ 'if (arguments.length === nB + 1) {',
73
+ ' s0 = undefined',
74
+ ' d = arguments[0]'
75
+ ]
76
+ for (let i = 0; i < n; i++) {
77
+ lines.push(` ${pNames[i]} = arguments[${i + 1}]`)
78
+ }
79
+ lines.push(
80
+ '} else if (arguments.length === nB + 2) {',
81
+ ' s0 = arguments[0]',
82
+ ' d = arguments[1]'
83
+ )
84
+ for (let i = 0; i < n; i++) {
85
+ lines.push(` ${pNames[i]} = arguments[${i + 2}]`)
86
+ }
87
+ lines.push(
88
+ '} else {',
89
+ ` throw new Error(${JSON.stringify('ADT eliminator: неверное число аргументов')})`,
90
+ '}'
91
+ )
92
+ for (let i = 0; i < n; i++) {
93
+ const tag = tags[i]
94
+ const tname = JSON.stringify(tag.name)
95
+ if (tag.payload) {
96
+ lines.push(`if (d.tag === ${tname}) return ${pNames[i]}(s0, d.value);`)
97
+ } else {
98
+ lines.push(`if (d.tag === ${tname}) return ${pNames[i]}(s0);`)
99
+ }
100
+ }
101
+ lines.push(
102
+ `throw new Error(${JSON.stringify('ADT eliminator: неизвестный tag')});`
103
+ )
104
+ return lines.join('\n')
105
+ }
106
+
107
+ /**
108
+ * Список формальных параметров для `function name(…)`: eliminator — без параметров (`arguments`).
109
+ *
110
+ * @param {{ kind: string }} role
111
+ * @param {NormalizedSignature | null | undefined} normalizedSig
112
+ */
113
+ export function formatAutogenFormalParameters (role, normalizedSig) {
114
+ if (role.kind === 'sum_elim') return ''
115
+ const left = normalizedSig != null && Array.isArray(normalizedSig.left)
116
+ ? normalizedSig.left
117
+ : []
118
+ if (left.length === 0) return ''
119
+ return Array.from({ length: left.length }, (_, i) => `p${i}`).join(', ')
120
+ }
121
+
122
+ /**
123
+ * @param {{ kind: string, ast: object, tag?: object, field?: object }} role
124
+ * @param {NormalizedSignature | null | undefined} _normalizedSig зарезервировано для будущих проверок
125
+ */
126
+ export function emitAdtAutogenFunctionBody (role, _normalizedSig) {
127
+ switch (role.kind) {
128
+ case 'sum_elim':
129
+ return emitSumEliminatorBody(role.ast)
130
+ case 'sum_ctor': {
131
+ const tag = /** @type {{ name: string, payload?: unknown }} */ (role.tag)
132
+ const tname = JSON.stringify(tag.name)
133
+ if (tag.payload) {
134
+ return `return { tag: ${tname}, value: p0 };`
135
+ }
136
+ return `return { tag: ${tname} };`
137
+ }
138
+ case 'prod_ctor': {
139
+ const ast = role.ast
140
+ const fields = ast.fields ?? []
141
+ /** @type {string[]} */
142
+ const parts = []
143
+ for (let ix = 0; ix < fields.length; ix++) {
144
+ const f = /** @type {{ name: string }} */ (fields[ix])
145
+ parts.push(`${JSON.stringify(f.name)}: p${ix}`)
146
+ }
147
+ return `return { ${parts.join(', ')} };`
148
+ }
149
+ case 'prod_get': {
150
+ const field = /** @type {{ name: string }} */ (role.field)
151
+ const k = JSON.stringify(field.name)
152
+ return `return p0[${k}];`
153
+ }
154
+ case 'prod_with': {
155
+ const ast = role.ast
156
+ const fields = ast.fields ?? []
157
+ const acc = fields.map((/** @type {{ name: string }} */ f) => {
158
+ return `p0[${JSON.stringify(f.name)}]`
159
+ })
160
+ return `return p1(undefined, ${acc.join(', ')});`
161
+ }
162
+ default:
163
+ throw new Error(`emit-adt: неизвестный kind роли ${String(/** @type {{ kind?: string }} */ (role).kind)}`)
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Имя вспомогательной функции для квалифицированного автогена (`~M/w` → не экспортируется из M).
169
+ *
170
+ * @param {string} moduleAlias
171
+ * @param {string} wordName
172
+ */
173
+ export function mangleQualifiedAutogenName (moduleAlias, wordName) {
174
+ const safeA = moduleAlias.replace(/[^A-Za-z0-9_$]/g, '_')
175
+ const safeW = wordName.replace(/[^A-Za-z0-9_$]/g, '_')
176
+ return `__adt_${safeA}__${safeW}`
177
+ }