@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.
- package/bin/sail.mjs +4 -0
- package/cli/run-cli.js +176 -0
- package/index.js +11 -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 +33 -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,498 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L5 этап 3: один модуль `.sail` → один ESM (RFC-compile-0.1 §9, RFC-IR §7).
|
|
3
|
+
*/
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { buildModuleIr } from '../ir/index.js'
|
|
6
|
+
import {
|
|
7
|
+
calleeAsyncPropagatesToCaller,
|
|
8
|
+
declaredEffectFlags
|
|
9
|
+
} from '../typecheck/effect-decl.js'
|
|
10
|
+
import { buildAutogenSignaturesForModule } from '../typecheck/adt-autogen-sigs.js'
|
|
11
|
+
import {
|
|
12
|
+
emitAdtAutogenFunctionBody,
|
|
13
|
+
findAutogenRoleInModule,
|
|
14
|
+
formatAutogenFormalParameters,
|
|
15
|
+
mangleQualifiedAutogenName
|
|
16
|
+
} from './emit-adt.js'
|
|
17
|
+
import {
|
|
18
|
+
emitWordBodyIr,
|
|
19
|
+
formatFormalParametersForWireSig,
|
|
20
|
+
irStepsCallsPropagatingAsyncCallee
|
|
21
|
+
} from './emit-body.js'
|
|
22
|
+
import { relativeSpecifierFromOutputs } from './esm-imports.js'
|
|
23
|
+
import {
|
|
24
|
+
e5201BadImportAlias,
|
|
25
|
+
e5202ImportEmptySpecifier,
|
|
26
|
+
e5203BadBracketWord,
|
|
27
|
+
e5204BadJsWordName,
|
|
28
|
+
e5205EmitAdtAutogen,
|
|
29
|
+
e5205EmitWord,
|
|
30
|
+
e5206JSDocModuleHeader,
|
|
31
|
+
e5206JSDocWord
|
|
32
|
+
} from './codegen-diagnostics.js'
|
|
33
|
+
import {
|
|
34
|
+
formatModuleSailHeaderBlock,
|
|
35
|
+
formatWordSailBlock,
|
|
36
|
+
findWordItemInSnapshot
|
|
37
|
+
} from './emit-jsdoc-sail.js'
|
|
38
|
+
import { projectJsFileToOutputPath, sailModuleToOutputJsPath } from './out-layout.js'
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {string} name
|
|
42
|
+
* @returns {boolean}
|
|
43
|
+
*/
|
|
44
|
+
function isValidJsIdentifier (name) {
|
|
45
|
+
return /^[A-Za-z_$][\w$]*$/.test(name)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @param {unknown} sig
|
|
50
|
+
* @returns {boolean}
|
|
51
|
+
*/
|
|
52
|
+
function sigPropagatesAsyncToCaller (sig) {
|
|
53
|
+
if (sig == null || typeof sig !== 'object') return false
|
|
54
|
+
return calleeAsyncPropagatesToCaller(declaredEffectFlags(sig))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Резолвер сигнатур для `await` при варианте B (IR без `calleeAsync` под `-Async`).
|
|
59
|
+
*
|
|
60
|
+
* @param {object} env
|
|
61
|
+
* @param {string} normModulePath
|
|
62
|
+
* @param {{ imports?: object[], words?: object[] }} payload
|
|
63
|
+
*/
|
|
64
|
+
export function createCalleeAsyncResolverForModule (env, normModulePath, payload) {
|
|
65
|
+
const sigByPath = env.sigIrByPath
|
|
66
|
+
const localMap = sigByPath?.get(normModulePath)
|
|
67
|
+
/** @type {Map<string, string>} */
|
|
68
|
+
const aliasToResolved = new Map()
|
|
69
|
+
for (const imp of payload.imports ?? []) {
|
|
70
|
+
if (
|
|
71
|
+
typeof imp.module === 'string' &&
|
|
72
|
+
typeof imp.resolvedPath === 'string'
|
|
73
|
+
) {
|
|
74
|
+
aliasToResolved.set(imp.module, path.normalize(imp.resolvedPath))
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
localPropagatesAsync (name) {
|
|
79
|
+
const s = localMap?.get(name)
|
|
80
|
+
return sigPropagatesAsyncToCaller(s)
|
|
81
|
+
},
|
|
82
|
+
qualifiedPropagatesAsync (moduleAlias, wordName) {
|
|
83
|
+
const rp = aliasToResolved.get(moduleAlias)
|
|
84
|
+
if (rp == null) return false
|
|
85
|
+
const m = sigByPath?.get(rp)
|
|
86
|
+
const s = m?.get(wordName)
|
|
87
|
+
return sigPropagatesAsyncToCaller(s)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @param {string} body
|
|
94
|
+
* @param {number} indentWidth
|
|
95
|
+
* @returns {string}
|
|
96
|
+
*/
|
|
97
|
+
function indentBody (body, indentWidth = 2) {
|
|
98
|
+
const pad = ' '.repeat(indentWidth)
|
|
99
|
+
return body
|
|
100
|
+
.split('\n')
|
|
101
|
+
.map((line) => (line.length === 0 ? line : pad + line))
|
|
102
|
+
.join('\n')
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* @param {unknown} steps
|
|
107
|
+
* @param {(w: { kind: 'local', name: string } | { kind: 'qualified', module: string, word: string }) => void} visit
|
|
108
|
+
*/
|
|
109
|
+
function walkIrStepsForWordRefs (steps, visit) {
|
|
110
|
+
if (!Array.isArray(steps)) return
|
|
111
|
+
for (const raw of steps) {
|
|
112
|
+
if (raw == null || typeof raw !== 'object') continue
|
|
113
|
+
const n = /** @type {Record<string, unknown>} */ (raw)
|
|
114
|
+
const k = String(n.kind ?? '')
|
|
115
|
+
if (k === 'Word') {
|
|
116
|
+
const ref = n.ref
|
|
117
|
+
if (ref === 'local' && typeof n.name === 'string') {
|
|
118
|
+
visit({ kind: 'local', name: n.name })
|
|
119
|
+
} else if (
|
|
120
|
+
ref === 'qualified' &&
|
|
121
|
+
typeof n.module === 'string' &&
|
|
122
|
+
typeof (n.word ?? n.name) === 'string'
|
|
123
|
+
) {
|
|
124
|
+
visit({
|
|
125
|
+
kind: 'qualified',
|
|
126
|
+
module: n.module,
|
|
127
|
+
word: String(n.word ?? n.name)
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (k === 'Builtin' && n.name === 'call') {
|
|
132
|
+
const meta = n.callInlineMeta
|
|
133
|
+
if (meta != null && typeof meta === 'object') {
|
|
134
|
+
const m = /** @type {{ kind?: string, innerSteps?: unknown }} */ (meta)
|
|
135
|
+
if (m.kind === 'inline' || m.kind == null) {
|
|
136
|
+
walkIrStepsForWordRefs(m.innerSteps, visit)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (k === 'Quotation') {
|
|
141
|
+
const qm = n.quoteEmitMeta
|
|
142
|
+
if (qm != null && typeof qm === 'object') {
|
|
143
|
+
walkIrStepsForWordRefs(
|
|
144
|
+
/** @type {{ innerSteps?: unknown }} */ (qm).innerSteps,
|
|
145
|
+
visit
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* @param {import('../typecheck/build-type-env.js').TypecheckEnv} env
|
|
154
|
+
* @param {string} normPath
|
|
155
|
+
* @param {Map<string, unknown> | undefined} wordsMap
|
|
156
|
+
* @param {{ imports?: object[], words?: object[] }} payload
|
|
157
|
+
* @param {Map<string, Set<string>>} bracketWordsByAlias
|
|
158
|
+
*/
|
|
159
|
+
function collectAdtAutogenToEmit (
|
|
160
|
+
env,
|
|
161
|
+
normPath,
|
|
162
|
+
wordsMap,
|
|
163
|
+
payload,
|
|
164
|
+
bracketWordsByAlias
|
|
165
|
+
) {
|
|
166
|
+
/** @type {Map<string, { role: object, sig: object }>} */
|
|
167
|
+
const localFns = new Map()
|
|
168
|
+
/** @type {Map<string, { role: object, sig: object, mangled: string }>} */
|
|
169
|
+
const qualFns = new Map()
|
|
170
|
+
/** @type {Map<string, string>} */
|
|
171
|
+
const qualCallee = new Map()
|
|
172
|
+
|
|
173
|
+
const userNames =
|
|
174
|
+
wordsMap instanceof Map ? new Set(wordsMap.keys()) : new Set()
|
|
175
|
+
|
|
176
|
+
const autoHere = buildAutogenSignaturesForModule(env, normPath)
|
|
177
|
+
|
|
178
|
+
/** @type {Map<string, string>} */
|
|
179
|
+
const aliasToPath = new Map()
|
|
180
|
+
for (const imp of payload.imports ?? []) {
|
|
181
|
+
if (
|
|
182
|
+
typeof imp.module === 'string' &&
|
|
183
|
+
typeof imp.resolvedPath === 'string'
|
|
184
|
+
) {
|
|
185
|
+
aliasToPath.set(imp.module, path.normalize(imp.resolvedPath))
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const considerLocal = (name) => {
|
|
190
|
+
if (userNames.has(name)) return
|
|
191
|
+
if (!autoHere.has(name)) return
|
|
192
|
+
const role = findAutogenRoleInModule(env, normPath, name)
|
|
193
|
+
if (!role) return
|
|
194
|
+
localFns.set(name, { role, sig: autoHere.get(name) })
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const considerQual = (alias, w) => {
|
|
198
|
+
const bracket = bracketWordsByAlias.get(alias)
|
|
199
|
+
if (bracket?.has(w)) {
|
|
200
|
+
qualCallee.set(`${alias}\0${w}`, w)
|
|
201
|
+
return
|
|
202
|
+
}
|
|
203
|
+
const rp = aliasToPath.get(alias)
|
|
204
|
+
if (rp == null) return
|
|
205
|
+
const autoThere = buildAutogenSignaturesForModule(env, rp)
|
|
206
|
+
if (!autoThere.has(w)) return
|
|
207
|
+
const role = findAutogenRoleInModule(env, rp, w)
|
|
208
|
+
if (!role) return
|
|
209
|
+
const mangled = mangleQualifiedAutogenName(alias, w)
|
|
210
|
+
qualFns.set(`${alias}\0${w}`, {
|
|
211
|
+
role,
|
|
212
|
+
sig: autoThere.get(w),
|
|
213
|
+
mangled
|
|
214
|
+
})
|
|
215
|
+
qualCallee.set(`${alias}\0${w}`, mangled)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
for (const word of payload.words ?? []) {
|
|
219
|
+
walkIrStepsForWordRefs(word.irSteps, (ref) => {
|
|
220
|
+
if (ref.kind === 'local') considerLocal(ref.name)
|
|
221
|
+
else considerQual(ref.module, ref.word)
|
|
222
|
+
})
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return { localFns, qualFns, qualCallee }
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* @param {Map<string, { role: object, sig: object }>} localFns
|
|
230
|
+
* @param {Map<string, { role: object, sig: object, mangled: string }>} qualFns
|
|
231
|
+
*/
|
|
232
|
+
function emitAutogenBlock (localFns, qualFns) {
|
|
233
|
+
/** @type {string[]} */
|
|
234
|
+
const lines = []
|
|
235
|
+
const localNames = [...localFns.keys()].sort()
|
|
236
|
+
for (const nm of localNames) {
|
|
237
|
+
const { role, sig } = localFns.get(nm)
|
|
238
|
+
const body = emitAdtAutogenFunctionBody(role, sig)
|
|
239
|
+
const params = formatAutogenFormalParameters(role, sig)
|
|
240
|
+
const indented = indentBody(body)
|
|
241
|
+
lines.push(`function ${nm}(${params}) {\n${indented}\n}`)
|
|
242
|
+
}
|
|
243
|
+
const qualKeys = [...qualFns.keys()].sort()
|
|
244
|
+
for (const k of qualKeys) {
|
|
245
|
+
const { role, sig, mangled } = qualFns.get(k)
|
|
246
|
+
const body = emitAdtAutogenFunctionBody(role, sig)
|
|
247
|
+
const params = formatAutogenFormalParameters(role, sig)
|
|
248
|
+
const indented = indentBody(body)
|
|
249
|
+
lines.push(`function ${mangled}(${params}) {\n${indented}\n}`)
|
|
250
|
+
}
|
|
251
|
+
return lines.join('\n\n')
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* @param {{
|
|
256
|
+
* env: object
|
|
257
|
+
* modulePath: string
|
|
258
|
+
* layout: { outDir: string, sourceRoot: string, entryPath?: string }
|
|
259
|
+
* }} opts
|
|
260
|
+
* @returns {{ ok: true, source: string } | { ok: false, diagnostics: object[] }}
|
|
261
|
+
*/
|
|
262
|
+
export function emitModuleEsmSource ({ env, modulePath, layout }) {
|
|
263
|
+
const normPath = path.normalize(modulePath)
|
|
264
|
+
const oa = sailModuleToOutputJsPath(layout, normPath)
|
|
265
|
+
if (!oa.ok) {
|
|
266
|
+
return { ok: false, diagnostics: [oa.diagnostic] }
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const payload = buildModuleIr(env, normPath, {
|
|
270
|
+
moduleStatus: 'ok',
|
|
271
|
+
valueBindings: true
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
/** @type {Map<string, Set<string>>} */
|
|
275
|
+
const bracketWordsByAlias = new Map()
|
|
276
|
+
for (const imp of payload.imports ?? []) {
|
|
277
|
+
const words = imp.bracket?.words
|
|
278
|
+
if (Array.isArray(words) && words.length > 0) {
|
|
279
|
+
bracketWordsByAlias.set(imp.module, new Set(words))
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const wordsMap = env.wordDeclByPath?.get(normPath)
|
|
284
|
+
const { localFns: autogenLocalFns, qualFns: autogenQualFns, qualCallee } =
|
|
285
|
+
collectAdtAutogenToEmit(
|
|
286
|
+
env,
|
|
287
|
+
normPath,
|
|
288
|
+
wordsMap,
|
|
289
|
+
payload,
|
|
290
|
+
bracketWordsByAlias
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
/** @type {Set<string>} */
|
|
294
|
+
const sumEliminatorNames = new Set()
|
|
295
|
+
for (const [name, { sig }] of autogenLocalFns) {
|
|
296
|
+
if (
|
|
297
|
+
sig != null &&
|
|
298
|
+
typeof sig === 'object' &&
|
|
299
|
+
/** @type {{ adtEliminator?: string }} */ (sig).adtEliminator === 'sum'
|
|
300
|
+
) {
|
|
301
|
+
sumEliminatorNames.add(name)
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** @type {(moduleAlias: string, wordName: string) => string} */
|
|
306
|
+
const resolveQualifiedCallee = (moduleAlias, wordName) => {
|
|
307
|
+
const qk = `${moduleAlias}\0${wordName}`
|
|
308
|
+
if (qualCallee.has(qk)) return qualCallee.get(qk)
|
|
309
|
+
const set = bracketWordsByAlias.get(moduleAlias)
|
|
310
|
+
if (set?.has(wordName)) {
|
|
311
|
+
return wordName
|
|
312
|
+
}
|
|
313
|
+
return `${moduleAlias}.${wordName}`
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** @type {string[]} */
|
|
317
|
+
const importLines = []
|
|
318
|
+
for (const imp of payload.imports ?? []) {
|
|
319
|
+
const alias = imp.module
|
|
320
|
+
if (!isValidJsIdentifier(alias)) {
|
|
321
|
+
return {
|
|
322
|
+
ok: false,
|
|
323
|
+
diagnostics: [e5201BadImportAlias(String(alias))]
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
const rp = imp.resolvedPath
|
|
327
|
+
/** @type {string} */
|
|
328
|
+
let spec
|
|
329
|
+
if (typeof rp === 'string' && rp.toLowerCase().endsWith('.sail')) {
|
|
330
|
+
const ob = sailModuleToOutputJsPath(layout, rp)
|
|
331
|
+
if (!ob.ok) {
|
|
332
|
+
return { ok: false, diagnostics: [ob.diagnostic] }
|
|
333
|
+
}
|
|
334
|
+
spec = relativeSpecifierFromOutputs(oa.path, ob.path)
|
|
335
|
+
} else if (typeof rp === 'string' && rp.toLowerCase().endsWith('.js')) {
|
|
336
|
+
const declPath = typeof imp.path === 'string' ? imp.path : ''
|
|
337
|
+
const isRelativeProjectFfi =
|
|
338
|
+
declPath.startsWith('./') || declPath.startsWith('../')
|
|
339
|
+
if (!isRelativeProjectFfi && declPath !== '') {
|
|
340
|
+
spec = declPath
|
|
341
|
+
} else {
|
|
342
|
+
const ob = projectJsFileToOutputPath(layout, rp)
|
|
343
|
+
if (!ob.ok) {
|
|
344
|
+
return { ok: false, diagnostics: [ob.diagnostic] }
|
|
345
|
+
}
|
|
346
|
+
spec = relativeSpecifierFromOutputs(oa.path, ob.path)
|
|
347
|
+
}
|
|
348
|
+
} else {
|
|
349
|
+
spec = typeof imp.path === 'string' ? imp.path : ''
|
|
350
|
+
if (spec === '') {
|
|
351
|
+
return {
|
|
352
|
+
ok: false,
|
|
353
|
+
diagnostics: [e5202ImportEmptySpecifier(String(alias))]
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
const quotedSpec = JSON.stringify(spec)
|
|
358
|
+
const bw = imp.bracket?.words
|
|
359
|
+
// bracket.types: runtime import откладывается до ADT/codegen этапа 6
|
|
360
|
+
if (Array.isArray(bw) && bw.length > 0) {
|
|
361
|
+
for (const w of bw) {
|
|
362
|
+
if (!isValidJsIdentifier(w)) {
|
|
363
|
+
return {
|
|
364
|
+
ok: false,
|
|
365
|
+
diagnostics: [e5203BadBracketWord(String(w))]
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
importLines.push(`import { ${bw.join(', ')} } from ${quotedSpec};`)
|
|
370
|
+
} else {
|
|
371
|
+
importLines.push(`import * as ${alias} from ${quotedSpec};`)
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** @type {string[]} */
|
|
376
|
+
const funcParts = []
|
|
377
|
+
const words = Array.isArray(payload.words) ? [...payload.words] : []
|
|
378
|
+
words.sort((a, b) => String(a.name).localeCompare(String(b.name)))
|
|
379
|
+
|
|
380
|
+
const snap = env.snapshots?.get(normPath)
|
|
381
|
+
|
|
382
|
+
const headerRes = formatModuleSailHeaderBlock(normPath, env)
|
|
383
|
+
if (!headerRes.ok) {
|
|
384
|
+
return {
|
|
385
|
+
ok: false,
|
|
386
|
+
diagnostics: [e5206JSDocModuleHeader(headerRes.message)]
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
/** @type {string} */
|
|
390
|
+
const moduleJSDocBlock = headerRes.block
|
|
391
|
+
|
|
392
|
+
const calleeAsyncResolver = createCalleeAsyncResolverForModule(
|
|
393
|
+
env,
|
|
394
|
+
normPath,
|
|
395
|
+
payload
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
/** @type {string} */
|
|
399
|
+
let autogenBlock = ''
|
|
400
|
+
if (autogenLocalFns.size > 0 || autogenQualFns.size > 0) {
|
|
401
|
+
try {
|
|
402
|
+
autogenBlock = emitAutogenBlock(autogenLocalFns, autogenQualFns)
|
|
403
|
+
} catch (e) {
|
|
404
|
+
const msg = e instanceof Error ? e.message : String(e)
|
|
405
|
+
return {
|
|
406
|
+
ok: false,
|
|
407
|
+
diagnostics: [e5205EmitAdtAutogen(msg)]
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
for (const word of words) {
|
|
413
|
+
if (!word.sigAvailable || word.normalizedSig == null) continue
|
|
414
|
+
if (!Array.isArray(word.irSteps)) continue
|
|
415
|
+
const wname = String(word.name)
|
|
416
|
+
if (!isValidJsIdentifier(wname)) {
|
|
417
|
+
return {
|
|
418
|
+
ok: false,
|
|
419
|
+
diagnostics: [e5204BadJsWordName(wname)]
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const needsAsyncKeyword =
|
|
424
|
+
word.asyncDefinition === true ||
|
|
425
|
+
irStepsCallsPropagatingAsyncCallee(word.irSteps, calleeAsyncResolver)
|
|
426
|
+
|
|
427
|
+
let bodyResult
|
|
428
|
+
try {
|
|
429
|
+
bodyResult = emitWordBodyIr(word.irSteps, {
|
|
430
|
+
strict: true,
|
|
431
|
+
wordName: wname,
|
|
432
|
+
normalizedSig: word.normalizedSig,
|
|
433
|
+
entryStackIds: word.entryStackIds ?? null,
|
|
434
|
+
resolveQualifiedCallee,
|
|
435
|
+
callerAsync: needsAsyncKeyword,
|
|
436
|
+
calleeAsyncResolver,
|
|
437
|
+
sumEliminatorNames
|
|
438
|
+
})
|
|
439
|
+
} catch (e) {
|
|
440
|
+
const msg = e instanceof Error ? e.message : String(e)
|
|
441
|
+
return {
|
|
442
|
+
ok: false,
|
|
443
|
+
diagnostics: [e5205EmitWord(wname, msg)]
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const params = formatFormalParametersForWireSig(
|
|
448
|
+
word.normalizedSig,
|
|
449
|
+
word.entryStackIds ?? null
|
|
450
|
+
)
|
|
451
|
+
const asyncKw = needsAsyncKeyword ? 'async ' : ''
|
|
452
|
+
const exportKw = word.exported === true ? 'export ' : ''
|
|
453
|
+
const indented = indentBody(bodyResult.source)
|
|
454
|
+
|
|
455
|
+
let exportJSDoc = ''
|
|
456
|
+
if (word.exported === true) {
|
|
457
|
+
const wordAst = findWordItemInSnapshot(snap, wname)
|
|
458
|
+
const blockRes = formatWordSailBlock({
|
|
459
|
+
wordName: wname,
|
|
460
|
+
wordAst,
|
|
461
|
+
wireSig: word.normalizedSig,
|
|
462
|
+
exportAsync: needsAsyncKeyword,
|
|
463
|
+
asyncDefinition: word.asyncDefinition === true,
|
|
464
|
+
mayFail: word.mayFail === true
|
|
465
|
+
})
|
|
466
|
+
if (!blockRes.ok) {
|
|
467
|
+
return {
|
|
468
|
+
ok: false,
|
|
469
|
+
diagnostics: [e5206JSDocWord(wname, blockRes.message)]
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
exportJSDoc = `${blockRes.block}\n`
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
funcParts.push(
|
|
476
|
+
`${exportJSDoc}${exportKw}${asyncKw}function ${wname}(${params}) {\n${indented}\n}`
|
|
477
|
+
)
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const pieces = []
|
|
481
|
+
if (importLines.length > 0) {
|
|
482
|
+
pieces.push(importLines.join('\n'))
|
|
483
|
+
}
|
|
484
|
+
if (moduleJSDocBlock.length > 0) {
|
|
485
|
+
if (pieces.length > 0) pieces.push('')
|
|
486
|
+
pieces.push(moduleJSDocBlock)
|
|
487
|
+
}
|
|
488
|
+
if (autogenBlock.length > 0) {
|
|
489
|
+
if (pieces.length > 0) pieces.push('')
|
|
490
|
+
pieces.push(autogenBlock)
|
|
491
|
+
}
|
|
492
|
+
if (funcParts.length > 0) {
|
|
493
|
+
if (pieces.length > 0) pieces.push('')
|
|
494
|
+
pieces.push(funcParts.join('\n\n'))
|
|
495
|
+
}
|
|
496
|
+
const source = pieces.length > 0 ? `${pieces.join('\n')}\n` : '\n'
|
|
497
|
+
return { ok: true, source }
|
|
498
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Статические спецификаторы ESM между артефактами под out-dir (RFC-compile-0.1 §9.2).
|
|
3
|
+
*/
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Относительный спецификатор от `dirname(O_A)` к `O_B` с префиксом `./` или `../`
|
|
8
|
+
* и путём в стиле URL (прямые слэши).
|
|
9
|
+
*
|
|
10
|
+
* @param {string} fromOutJsPath — абсолютный путь к выходному `.js` текущего модуля
|
|
11
|
+
* @param {string} toOutJsPath — абсолютный путь к выходному `.js` зависимости
|
|
12
|
+
* @returns {string}
|
|
13
|
+
*/
|
|
14
|
+
export function relativeSpecifierFromOutputs (fromOutJsPath, toOutJsPath) {
|
|
15
|
+
const fromDir = path.dirname(path.resolve(fromOutJsPath))
|
|
16
|
+
const toAbs = path.resolve(toOutJsPath)
|
|
17
|
+
let rel = path.relative(fromDir, toAbs)
|
|
18
|
+
if (!rel || rel === '') {
|
|
19
|
+
rel = path.basename(toAbs)
|
|
20
|
+
}
|
|
21
|
+
rel = rel.split(path.sep).join('/')
|
|
22
|
+
if (!rel.startsWith('.')) {
|
|
23
|
+
rel = `./${rel}`
|
|
24
|
+
}
|
|
25
|
+
return rel
|
|
26
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L5 codegen: вход после L3 (RFC-compile-0.1 §4, RFC-IR-0.1 §1.1).
|
|
3
|
+
*/
|
|
4
|
+
import fs from 'node:fs/promises'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import { typecheckSail } from '../typecheck/index.js'
|
|
7
|
+
import { emitCodegenStage0Stub } from './compile-graph.js'
|
|
8
|
+
import { resolveCompileLayout, validateEntryInsideSourceRoot } from './out-layout.js'
|
|
9
|
+
|
|
10
|
+
export { emitCodegenStage0Stub } from './compile-graph.js'
|
|
11
|
+
export { emitWordBodyIr, formatFormalParametersForWireSig } from './emit-body.js'
|
|
12
|
+
export { emitModuleEsmSource } from './emit-module.js'
|
|
13
|
+
export { relativeSpecifierFromOutputs } from './esm-imports.js'
|
|
14
|
+
export {
|
|
15
|
+
isPathInsideSourceRoot,
|
|
16
|
+
projectJsFileToOutputPath,
|
|
17
|
+
resolveCompileLayout,
|
|
18
|
+
sailModuleToOutputJsPath,
|
|
19
|
+
validateEntryInsideSourceRoot
|
|
20
|
+
} from './out-layout.js'
|
|
21
|
+
|
|
22
|
+
function defaultFsSink () {
|
|
23
|
+
return {
|
|
24
|
+
mkdir: (p, opts) => fs.mkdir(p, opts),
|
|
25
|
+
writeFile: (p, data) => fs.writeFile(p, data, 'utf8')
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Компиляция точки входа: typecheck; при `ok` — эмиссия ESM по модулям замыкания (этап 3).
|
|
31
|
+
*
|
|
32
|
+
* @param {{
|
|
33
|
+
* entryPath: string,
|
|
34
|
+
* outDir: string,
|
|
35
|
+
* sourceRoot?: string,
|
|
36
|
+
* readFile: (p: string) => string | null | undefined,
|
|
37
|
+
* resolvePackage?: (spec: string, fromPath: string) => string | null,
|
|
38
|
+
* fs?: {
|
|
39
|
+
* mkdir: (p: string, opts?: { recursive?: boolean }) => Promise<void>,
|
|
40
|
+
* writeFile: (p: string, data: string) => Promise<void>
|
|
41
|
+
* }
|
|
42
|
+
* }} opts
|
|
43
|
+
* @returns {Promise<
|
|
44
|
+
* | { ok: false, diagnostics: object[] }
|
|
45
|
+
* | { ok: true, diagnostics: [], emitted: string[] }
|
|
46
|
+
* >}
|
|
47
|
+
*/
|
|
48
|
+
export async function compileSailToOutDir (opts) {
|
|
49
|
+
const entryPath = path.resolve(opts.entryPath)
|
|
50
|
+
const tc = typecheckSail({
|
|
51
|
+
entryPath,
|
|
52
|
+
readFile: opts.readFile,
|
|
53
|
+
resolvePackage: opts.resolvePackage
|
|
54
|
+
})
|
|
55
|
+
if (!tc.ok) {
|
|
56
|
+
return { ok: false, diagnostics: tc.diagnostics }
|
|
57
|
+
}
|
|
58
|
+
const layout = resolveCompileLayout({
|
|
59
|
+
entryPath,
|
|
60
|
+
outDir: opts.outDir,
|
|
61
|
+
sourceRoot: opts.sourceRoot
|
|
62
|
+
})
|
|
63
|
+
const entryLayoutOk = validateEntryInsideSourceRoot(layout)
|
|
64
|
+
if (!entryLayoutOk.ok) {
|
|
65
|
+
return { ok: false, diagnostics: entryLayoutOk.diagnostics }
|
|
66
|
+
}
|
|
67
|
+
const fsSink = opts.fs ?? defaultFsSink()
|
|
68
|
+
return emitCodegenStage0Stub(tc.env, layout, fsSink, opts.readFile)
|
|
69
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Раскладка выходных путей (RFC-compile-0.1 §9.2): source-root, O(S) для `.sail` → `.js`.
|
|
3
|
+
*/
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import {
|
|
6
|
+
e5101SailInvalidRelative,
|
|
7
|
+
e5101SailOutsideSourceRoot,
|
|
8
|
+
e5102SailExpectedExtension,
|
|
9
|
+
e5103FfiInvalidRelative,
|
|
10
|
+
e5103FfiOutsideSourceRoot,
|
|
11
|
+
e5104FfiExpectedExtension,
|
|
12
|
+
e5105EntryOutsideSourceRoot
|
|
13
|
+
} from './codegen-diagnostics.js'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {{ entryPath: string, outDir: string, sourceRoot?: string }} opts
|
|
17
|
+
* @returns {{ entryPath: string, outDir: string, sourceRoot: string }}
|
|
18
|
+
*/
|
|
19
|
+
export function resolveCompileLayout (opts) {
|
|
20
|
+
const entryPath = path.resolve(opts.entryPath)
|
|
21
|
+
const outDir = path.resolve(opts.outDir)
|
|
22
|
+
const sourceRoot =
|
|
23
|
+
opts.sourceRoot != null ? path.resolve(opts.sourceRoot) : path.dirname(entryPath)
|
|
24
|
+
return { entryPath, outDir, sourceRoot }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {string} filePath
|
|
29
|
+
* @param {string} sourceRoot
|
|
30
|
+
* @returns {boolean}
|
|
31
|
+
*/
|
|
32
|
+
export function isPathInsideSourceRoot (filePath, sourceRoot) {
|
|
33
|
+
const f = path.resolve(filePath)
|
|
34
|
+
const r = path.resolve(sourceRoot)
|
|
35
|
+
const prefix = r.endsWith(path.sep) ? r : r + path.sep
|
|
36
|
+
return f === r || f.startsWith(prefix)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {{ entryPath: string, sourceRoot: string } & object} layout
|
|
41
|
+
* @returns {{ ok: true } | { ok: false, diagnostics: object[] }}
|
|
42
|
+
*/
|
|
43
|
+
export function validateEntryInsideSourceRoot (layout) {
|
|
44
|
+
if (!isPathInsideSourceRoot(layout.entryPath, layout.sourceRoot)) {
|
|
45
|
+
return {
|
|
46
|
+
ok: false,
|
|
47
|
+
diagnostics: [
|
|
48
|
+
e5105EntryOutsideSourceRoot({
|
|
49
|
+
entryPath: layout.entryPath,
|
|
50
|
+
sourceRoot: layout.sourceRoot
|
|
51
|
+
})
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return { ok: true }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @param {{ outDir: string, sourceRoot: string }} layout — из {@link resolveCompileLayout}
|
|
60
|
+
* @param {string} sailPath — абсолютный или нормализуемый путь к `.sail`
|
|
61
|
+
* @returns {{ ok: true, path: string } | { ok: false, diagnostic: { code: string, message: string } }}
|
|
62
|
+
*/
|
|
63
|
+
export function sailModuleToOutputJsPath (layout, sailPath) {
|
|
64
|
+
const S = path.normalize(path.resolve(sailPath))
|
|
65
|
+
if (!isPathInsideSourceRoot(S, layout.sourceRoot)) {
|
|
66
|
+
return { ok: false, diagnostic: e5101SailOutsideSourceRoot(S) }
|
|
67
|
+
}
|
|
68
|
+
const rel = path.relative(layout.sourceRoot, S)
|
|
69
|
+
if (rel === '' || rel.split(path.sep).includes('..')) {
|
|
70
|
+
return { ok: false, diagnostic: e5101SailInvalidRelative(S) }
|
|
71
|
+
}
|
|
72
|
+
const { dir, name, ext } = path.parse(rel)
|
|
73
|
+
if (ext.toLowerCase() !== '.sail') {
|
|
74
|
+
return { ok: false, diagnostic: e5102SailExpectedExtension(S) }
|
|
75
|
+
}
|
|
76
|
+
const jsRel = path.join(dir, `${name}.js`)
|
|
77
|
+
return { ok: true, path: path.join(layout.outDir, jsRel) }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Выходной путь для проектного FFI `.js` (RFC-compile §9.1–9.2): зеркало относительного пути
|
|
82
|
+
* от `source-root` без смены расширения.
|
|
83
|
+
*
|
|
84
|
+
* @param {{ outDir: string, sourceRoot: string }} layout
|
|
85
|
+
* @param {string} jsPath — абсолютный или нормализуемый путь к `.js` под проектом
|
|
86
|
+
* @returns {{ ok: true, path: string } | { ok: false, diagnostic: { code: string, message: string } }}
|
|
87
|
+
*/
|
|
88
|
+
export function projectJsFileToOutputPath (layout, jsPath) {
|
|
89
|
+
const S = path.normalize(path.resolve(jsPath))
|
|
90
|
+
if (!isPathInsideSourceRoot(S, layout.sourceRoot)) {
|
|
91
|
+
return { ok: false, diagnostic: e5103FfiOutsideSourceRoot(S) }
|
|
92
|
+
}
|
|
93
|
+
const rel = path.relative(layout.sourceRoot, S)
|
|
94
|
+
if (rel === '' || rel.split(path.sep).includes('..')) {
|
|
95
|
+
return { ok: false, diagnostic: e5103FfiInvalidRelative(S) }
|
|
96
|
+
}
|
|
97
|
+
const ext = path.extname(rel)
|
|
98
|
+
if (ext.toLowerCase() !== '.js') {
|
|
99
|
+
return { ok: false, diagnostic: e5104FfiExpectedExtension(S) }
|
|
100
|
+
}
|
|
101
|
+
return { ok: true, path: path.join(layout.outDir, rel) }
|
|
102
|
+
}
|