@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,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
+ }