@scriptc/compiler 0.0.6 → 0.0.8

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 (75) hide show
  1. package/dist/backend/emission/emit-exprs.js +47 -1
  2. package/dist/backend/emission/emit-exprs.js.map +1 -1
  3. package/dist/backend/emission/emit-island.js +9 -1
  4. package/dist/backend/emission/emit-island.js.map +1 -1
  5. package/dist/backend/emission/emit-stmts.js +3 -1
  6. package/dist/backend/emission/emit-stmts.js.map +1 -1
  7. package/dist/backend/emission/emit-walkers.js +22 -8
  8. package/dist/backend/emission/emit-walkers.js.map +1 -1
  9. package/dist/backend/emission/emitter.js +12 -1
  10. package/dist/backend/emission/emitter.js.map +1 -1
  11. package/dist/backend/emission/may-throw.js +4 -3
  12. package/dist/backend/emission/may-throw.js.map +1 -1
  13. package/dist/backend/llvm/emitter.js +105 -9
  14. package/dist/backend/llvm/emitter.js.map +1 -1
  15. package/dist/diagnostics/diagnostic.d.ts +49 -1
  16. package/dist/diagnostics/diagnostic.js +119 -5
  17. package/dist/diagnostics/diagnostic.js.map +1 -1
  18. package/dist/frontend/lib-contract.d.ts +114 -0
  19. package/dist/frontend/lib-contract.js +372 -0
  20. package/dist/frontend/lib-contract.js.map +1 -0
  21. package/dist/frontend/lowering/lower-builtins.js +16 -1
  22. package/dist/frontend/lowering/lower-builtins.js.map +1 -1
  23. package/dist/frontend/lowering/lower-calls.d.ts +47 -7
  24. package/dist/frontend/lowering/lower-calls.js +560 -36
  25. package/dist/frontend/lowering/lower-calls.js.map +1 -1
  26. package/dist/frontend/lowering/lower-classes.d.ts +14 -1
  27. package/dist/frontend/lowering/lower-classes.js +85 -15
  28. package/dist/frontend/lowering/lower-classes.js.map +1 -1
  29. package/dist/frontend/lowering/lower-containers.js +43 -1
  30. package/dist/frontend/lowering/lower-containers.js.map +1 -1
  31. package/dist/frontend/lowering/lower-emitter.js +1 -1
  32. package/dist/frontend/lowering/lower-emitter.js.map +1 -1
  33. package/dist/frontend/lowering/lower-exprs.d.ts +26 -3
  34. package/dist/frontend/lowering/lower-exprs.js +474 -44
  35. package/dist/frontend/lowering/lower-exprs.js.map +1 -1
  36. package/dist/frontend/lowering/lower-island.d.ts +7 -0
  37. package/dist/frontend/lowering/lower-island.js +71 -10
  38. package/dist/frontend/lowering/lower-island.js.map +1 -1
  39. package/dist/frontend/lowering/lower-mixins.d.ts +1 -1
  40. package/dist/frontend/lowering/lower-mixins.js +1 -1
  41. package/dist/frontend/lowering/lower-modules.js +95 -4
  42. package/dist/frontend/lowering/lower-modules.js.map +1 -1
  43. package/dist/frontend/lowering/lower-stmts.js +430 -44
  44. package/dist/frontend/lowering/lower-stmts.js.map +1 -1
  45. package/dist/frontend/lowering/lowerer.d.ts +19 -3
  46. package/dist/frontend/lowering/lowerer.js +188 -11
  47. package/dist/frontend/lowering/lowerer.js.map +1 -1
  48. package/dist/frontend/types.d.ts +28 -1
  49. package/dist/frontend/types.js +300 -8
  50. package/dist/frontend/types.js.map +1 -1
  51. package/dist/index.d.ts +11 -2
  52. package/dist/index.js +96 -7
  53. package/dist/index.js.map +1 -1
  54. package/dist/ir/nodes.d.ts +48 -2
  55. package/dist/ir/nodes.js +103 -3
  56. package/dist/ir/nodes.js.map +1 -1
  57. package/dist/ir/validate.js +35 -6
  58. package/dist/ir/validate.js.map +1 -1
  59. package/dist/library/profile.d.ts +53 -3
  60. package/dist/library/profile.js +143 -12
  61. package/dist/library/profile.js.map +1 -1
  62. package/dist/library/sidecar-validate.d.ts +4 -0
  63. package/dist/library/sidecar-validate.js +506 -0
  64. package/dist/library/sidecar-validate.js.map +1 -0
  65. package/dist/library/sidecar.d.ts +191 -0
  66. package/dist/library/sidecar.js +687 -0
  67. package/dist/library/sidecar.js.map +1 -0
  68. package/dist/library/trap-teaching.d.ts +11 -0
  69. package/dist/library/trap-teaching.js +49 -0
  70. package/dist/library/trap-teaching.js.map +1 -0
  71. package/dist/library/wyhash.d.ts +12 -0
  72. package/dist/library/wyhash.js +116 -0
  73. package/dist/library/wyhash.js.map +1 -0
  74. package/package.json +2 -2
  75. package/surface-manifest.json +37 -2
@@ -0,0 +1,687 @@
1
+ /* The ask-2 contract sidecar emitter: projects the entry module's typed
2
+ * contract into the profile-supplied schema (format 1) — a single JSON
3
+ * document beside the archive carrying the version/identity spine, the
4
+ * type table, the model/msg designations, helper signatures, shape flags,
5
+ * channels, the ABI attestation, and the determinism attestations.
6
+ *
7
+ * The sidecar is the CONTRACT, not the program: no source spans, no
8
+ * bodies, no IR, no environment or machine data, no absolute paths, no
9
+ * timestamps. Emission is deterministic — top-level keys in the schema's
10
+ * §1 order, every array order semantic (declaration order where the
11
+ * schema says declaration, profile canonical order for abi.exports), so
12
+ * re-running the identical invocation reproduces a byte-identical file.
13
+ *
14
+ * Declaration order is read from the SYNTAX TREE (frontend/lib-contract.ts)
15
+ * — never from checker property enumeration, which hands back internal or
16
+ * sorted order (the ratified record-field-order ruling: the IR's sorted
17
+ * canonicalization is storage; AST order is the contract).
18
+ *
19
+ * Order must be DERIVABLE FROM ONE DECLARATION SITE or the build refuses
20
+ * (ask 3's define-or-refuse rule): declaration merging into a tabled type
21
+ * refuses with every site named (SC4010), conditional/mapped types
22
+ * producing a tabled type refuse (SC4011), and a union COMPOSED of other
23
+ * kind-tagged unions (`type Msg = A | B` — the reducer-composition
24
+ * pattern) is allowed with a pinned order: depth-first source order of
25
+ * the constituent declarations (A's arms in A's own declaration order,
26
+ * then B's), an arm name appearing in several constituents keeping its
27
+ * FIRST occurrence. A type alias of a named type is transparent: the
28
+ * table derives from the aliased declaration, the alias adds no entry
29
+ * and no reordering.
30
+ *
31
+ * Not-yet facts emit the schema's stated absent forms, never invented
32
+ * values: `integer_slots` is `[]` with every numeric slot spelled `f64`
33
+ * (the pre-ask-4 sequencing the schema names valid — the empty list is
34
+ * itself an attestation), and `deterministic` is computed from the module
35
+ * graph (ir/nodes.ts's conservative ambient-surface scan), never
36
+ * defaulted. */
37
+ import { Buffer } from "node:buffer";
38
+ import { readFileSync } from "node:fs";
39
+ import { dirname, isAbsolute, join, relative, sep } from "node:path";
40
+ import { fileURLToPath } from "node:url";
41
+ import { libSidecarComputedDiag, libSidecarDiag, libSidecarMergedDiag } from "../diagnostics/diagnostic.js";
42
+ import { BUILD_ID_SEED, hex16, lengthPrefixedStream, SOURCE_HASH_SEED, wyhash64 } from "./wyhash.js";
43
+ /** The sidecar schema's format this emitter writes. */
44
+ export const SIDECAR_FORMAT = 1;
45
+ /* ── identity hashing (schema §2 + the "module-graph" source contract) ─── */
46
+ let releaseVersion = null;
47
+ /** The compiler's exact release identifier — the published package
48
+ * version, read once from the compiler package's own package.json (this
49
+ * module lives two levels below the package root in src/ and dist/
50
+ * alike). build_id input 1 and the sidecar's `compiler_version`. */
51
+ export function compilerReleaseVersion() {
52
+ if (releaseVersion === null) {
53
+ const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "package.json");
54
+ releaseVersion = JSON.parse(readFileSync(pkgPath, "utf8")).version;
55
+ }
56
+ return releaseVersion;
57
+ }
58
+ /** One path under the sidecar's canonical rules: compilation-root-relative
59
+ * POSIX, or `profile:`-namespaced when the file sits outside the root
60
+ * (never absolute, no `.`/`..` segments). */
61
+ export function canonicalPath(rootDir, file) {
62
+ const rel = relative(rootDir, file);
63
+ if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) {
64
+ return `profile:${file.split(/[\\/]/).pop()}`;
65
+ }
66
+ return sep === "/" ? rel : rel.split(sep).join("/");
67
+ }
68
+ /** Canonicalize + sort the module graph for hashing: root-relative POSIX
69
+ * paths, ascending by plain bytewise comparison of the path strings. A
70
+ * module outside the compilation root (the profile file's directory)
71
+ * cannot spell a canonical relative path, so it rides the `profile:`
72
+ * namespace under its basename — deterministic and absolute-path-free. */
73
+ export function canonicalModuleGraph(rootDir, sources) {
74
+ const enc = new TextEncoder();
75
+ const entries = [];
76
+ for (const [file, text] of sources) {
77
+ entries.push({ canonical: canonicalPath(rootDir, file), bytes: enc.encode(text) });
78
+ }
79
+ entries.sort((a, b) => {
80
+ const c = Buffer.compare(Buffer.from(a.canonical, "utf8"), Buffer.from(b.canonical, "utf8"));
81
+ return c !== 0 ? c : Buffer.compare(a.bytes, b.bytes);
82
+ });
83
+ return entries;
84
+ }
85
+ /** build_id (schema §2: compiler version, profile bytes, sorted module
86
+ * graph — every input length-prefixed) and source_hash (the
87
+ * "module-graph" contract: the sorted module graph alone, so the value is
88
+ * stable across compiler releases). Both 16 lowercase hex digits. */
89
+ export function libraryIdentityHashes(compilerVersion, profileBytes, modules) {
90
+ const enc = new TextEncoder();
91
+ const graphChunks = [];
92
+ for (const m of modules) {
93
+ graphChunks.push(enc.encode(m.canonical), m.bytes);
94
+ }
95
+ const buildChunks = [enc.encode(compilerVersion), profileBytes, ...graphChunks];
96
+ return {
97
+ buildId: hex16(wyhash64(lengthPrefixedStream(buildChunks), BUILD_ID_SEED)),
98
+ sourceHash: hex16(wyhash64(lengthPrefixedStream(graphChunks), SOURCE_HASH_SEED)),
99
+ };
100
+ }
101
+ /** The profile's canonical `abi.exports` suffix order: the identity
102
+ * getters first (abi_version, then build_id), the mode-provided entries
103
+ * (sink registration, init, collect, reset — declared ones only), then
104
+ * the export map in profile order. Suffix = symbol minus prefix. */
105
+ export function abiExportSuffixes(profile) {
106
+ const strip = (sym) => sym.slice(profile.prefix.length);
107
+ const out = [];
108
+ if (profile.sidecar !== null) {
109
+ out.push(strip(profile.sidecar.abiVersionSymbol), strip(profile.sidecar.buildIdSymbol));
110
+ }
111
+ out.push(strip(profile.sinkRegisterSymbol), strip(profile.initSymbol));
112
+ if (profile.collectSymbol !== null)
113
+ out.push(strip(profile.collectSymbol));
114
+ if (profile.resultResetSymbol !== null)
115
+ out.push(strip(profile.resultResetSymbol));
116
+ for (const e of profile.exports)
117
+ out.push(strip(e.symbol));
118
+ return out;
119
+ }
120
+ function classify(decl, index) {
121
+ const s = decl.shape;
122
+ if (s.k === "unsupported") {
123
+ return s.computed === undefined
124
+ ? { c: "unsupported", why: s.text, decl, index }
125
+ : { c: "unsupported", why: s.text, computed: s.computed, decl, index };
126
+ }
127
+ if (s.k === "object") {
128
+ return { c: "struct", storage: decl.form === "interface" ? "node" : "value", fields: s.fields, decl, index };
129
+ }
130
+ if (decl.form === "alias" && s.k === "ref")
131
+ return { c: "alias", target: s.name, decl, index };
132
+ if (decl.form === "alias" && s.k === "stringLit")
133
+ return { c: "enum", members: [s.text], decl, index };
134
+ if (decl.form === "alias" && s.k === "union") {
135
+ if (s.parts.every((p) => p.k === "stringLit")) {
136
+ return { c: "enum", members: s.parts.map((p) => p.text), decl, index };
137
+ }
138
+ const parts = [];
139
+ for (const p of s.parts) {
140
+ if (p.k === "ref") {
141
+ parts.push({ p: "ref", name: p.name, loc: decl.loc });
142
+ continue;
143
+ }
144
+ if (p.k !== "object") {
145
+ return { c: "unsupported", why: "a union mixing non-object constituents (a tagged union's arms are object literals with a string-literal 'kind', or references to other kind-tagged unions)", decl, index };
146
+ }
147
+ const kindField = p.fields.find((f) => f.name === "kind");
148
+ if (kindField === undefined || kindField.shape.k !== "stringLit" || kindField.optional) {
149
+ return { c: "unsupported", why: "a union constituent without a non-optional string-literal 'kind' discriminant", decl, index };
150
+ }
151
+ parts.push({ p: "arm", name: kindField.shape.text, fields: p.fields.filter((f) => f.name !== "kind"), loc: kindField.loc });
152
+ }
153
+ return { c: "tagged", parts, decl, index };
154
+ }
155
+ return { c: "unsupported", why: `a shape outside the sidecar's vocabulary (${s.k})`, decl, index };
156
+ }
157
+ class SidecarError extends Error {
158
+ detail;
159
+ loc;
160
+ constructor(detail, loc) {
161
+ super(detail);
162
+ this.detail = detail;
163
+ this.loc = loc;
164
+ }
165
+ }
166
+ /** A refusal that carries its own minted diagnostic (SC4010/SC4011 —
167
+ * the SC4009 default rides SidecarError). */
168
+ class SidecarRefusal extends Error {
169
+ diag;
170
+ constructor(diag) {
171
+ super(diag.message);
172
+ this.diag = diag;
173
+ }
174
+ }
175
+ class Projector {
176
+ facts;
177
+ config;
178
+ entryLoc;
179
+ byName = new Map();
180
+ multiSite = new Map();
181
+ table = new Map();
182
+ inProgress = new Set();
183
+ flatArms = new Map();
184
+ flattening = new Set();
185
+ synthCounter = 0;
186
+ constructor(facts, config, entryLoc) {
187
+ this.facts = facts;
188
+ this.config = config;
189
+ this.entryLoc = entryLoc;
190
+ // facts.types carries one entry per name (a repeated exported name is
191
+ // a multi-site fact, refused below when the name is actually used).
192
+ facts.types.forEach((decl, index) => this.byName.set(decl.name, classify(decl, index)));
193
+ for (const m of facts.multiSiteTypes)
194
+ this.multiSite.set(m.name, m.sites);
195
+ }
196
+ lookup(name, loc) {
197
+ const c = this.byName.get(name);
198
+ if (c === undefined) {
199
+ throw new SidecarError(`'${name}' is not an exported type declaration of the program's modules`, loc);
200
+ }
201
+ // Define-or-refuse: a name whose members gather from several
202
+ // declaration sites has no single-source order — refuse the moment
203
+ // the contract touches it, naming every site (SC4010).
204
+ const sites = this.multiSite.get(name);
205
+ if (sites !== undefined) {
206
+ throw new SidecarRefusal(libSidecarMergedDiag(name, sites, c.decl.loc));
207
+ }
208
+ if (c.c === "unsupported") {
209
+ if (c.computed !== undefined) {
210
+ throw new SidecarRefusal(libSidecarComputedDiag(name, c.computed, c.decl.loc));
211
+ }
212
+ throw new SidecarError(`'${name}' cannot join the type table: it is ${c.why}`, c.decl.loc);
213
+ }
214
+ return c;
215
+ }
216
+ /** Follow `type A = B` alias chains to the aliased declaration: the
217
+ * table derives from the target's declaration site, and the alias
218
+ * introduces no entry and no reordering. */
219
+ resolve(name, loc) {
220
+ const seen = new Set();
221
+ let cur = name;
222
+ for (;;) {
223
+ if (seen.has(cur)) {
224
+ throw new SidecarError(`the type alias chain through '${name}' is cyclic`, loc);
225
+ }
226
+ seen.add(cur);
227
+ const c = this.lookup(cur, loc);
228
+ if (c.c !== "alias")
229
+ return { name: cur, c };
230
+ cur = c.target;
231
+ }
232
+ }
233
+ /** A kind-tagged union's arms, its reference constituents flattened
234
+ * DEPTH-FIRST IN SOURCE ORDER (the ask-3 composition rule, pinned by
235
+ * conformance): an inline arm lands where it is spelled; a constituent
236
+ * union contributes its own (recursively flattened) arms, in its own
237
+ * declaration order, at the position of the reference. An arm name
238
+ * arriving from several constituents keeps its FIRST occurrence; a
239
+ * repeat among one declaration's own inline arms stays a refusal. */
240
+ unionArms(unionName, loc) {
241
+ const memo = this.flatArms.get(unionName);
242
+ if (memo !== undefined)
243
+ return memo;
244
+ if (this.flattening.has(unionName)) {
245
+ throw new SidecarError(`union composition is cyclic through '${unionName}' — a union cannot spread itself`, loc);
246
+ }
247
+ const c = this.lookup(unionName, loc);
248
+ if (c.c !== "tagged") {
249
+ throw new SidecarError(`'${unionName}' is not a kind-tagged union of object literals`, c.decl.loc);
250
+ }
251
+ this.flattening.add(unionName);
252
+ try {
253
+ const out = [];
254
+ const originOf = new Map();
255
+ for (const part of c.parts) {
256
+ if (part.p === "arm") {
257
+ const origin = originOf.get(part.name);
258
+ if (origin === unionName) {
259
+ throw new SidecarError(`union '${unionName}' repeats arm '${part.name}'`, part.loc);
260
+ }
261
+ if (origin !== undefined)
262
+ continue; // an earlier constituent's arm — first occurrence wins
263
+ originOf.set(part.name, unionName);
264
+ out.push({ name: part.name, fields: part.fields, loc: part.loc });
265
+ continue;
266
+ }
267
+ const r = this.resolve(part.name, part.loc);
268
+ if (r.c.c !== "tagged") {
269
+ throw new SidecarError(`constituent '${part.name}' of union '${unionName}' is not a kind-tagged union — only kind-tagged unions compose by reference`, part.loc);
270
+ }
271
+ for (const arm of this.unionArms(r.name, part.loc)) {
272
+ if (originOf.has(arm.name))
273
+ continue; // first occurrence wins under depth-first order
274
+ originOf.set(arm.name, r.name);
275
+ out.push(arm);
276
+ }
277
+ }
278
+ this.flatArms.set(unionName, out);
279
+ return out;
280
+ }
281
+ finally {
282
+ this.flattening.delete(unionName);
283
+ }
284
+ }
285
+ /** Project a syntactic field to a TypeRef, tabling every named type it
286
+ * references. `container`/`member` seed synthesized names. */
287
+ fieldRef(field, container) {
288
+ const inner = this.shapeRef(field.shape, container, field.name, field.loc);
289
+ if (field.optional) {
290
+ if (inner.kind === "optional")
291
+ return inner;
292
+ return { kind: "optional", inner };
293
+ }
294
+ return inner;
295
+ }
296
+ shapeRef(shape, container, member, loc) {
297
+ switch (shape.k) {
298
+ case "bool":
299
+ return { kind: "bool" };
300
+ case "number":
301
+ // Every numeric slot spells f64 until ask-4 integer inference
302
+ // lands (the schema's stated pre-inference sequencing).
303
+ return { kind: "f64" };
304
+ case "text":
305
+ case "bytes":
306
+ return { kind: "bytes" };
307
+ case "array":
308
+ return { kind: "slice", elem: this.shapeRef(shape.elem, container, member, loc) };
309
+ case "union": {
310
+ const present = shape.parts.filter((p) => p.k !== "absent");
311
+ const absents = shape.parts.length - present.length;
312
+ if (absents > 0 && present.length === 1) {
313
+ const inner = this.shapeRef(present[0], container, member, loc);
314
+ return inner.kind === "optional" ? inner : { kind: "optional", inner };
315
+ }
316
+ throw new SidecarError(`'${container}.${member}' is an inline union — declare it as a named kind-tagged union (or a string-literal-union enum) and reference it by name`, loc);
317
+ }
318
+ case "ref": {
319
+ // Aliases are transparent: the table entry (and its order) is the
320
+ // ALIASED declaration's, under the aliased declaration's name.
321
+ const r = this.resolve(shape.name, loc);
322
+ if (r.c.c === "struct") {
323
+ this.tableNamed(r.name, loc);
324
+ return { kind: r.c.storage, name: r.name };
325
+ }
326
+ if (r.c.c === "enum") {
327
+ this.tableNamed(r.name, loc);
328
+ return { kind: "enum", name: r.name };
329
+ }
330
+ if (r.name === this.config.msg) {
331
+ throw new SidecarError(`'${container}.${member}' references the designated msg union '${r.name}' — the msg union is the dispatch surface, not a table type`, loc);
332
+ }
333
+ this.tableNamed(r.name, loc);
334
+ return { kind: "union", name: r.name };
335
+ }
336
+ case "object":
337
+ return { kind: "value", name: this.tableSynthesized(container, member, shape.fields, loc) };
338
+ case "stringLit":
339
+ throw new SidecarError(`'${container}.${member}' is a bare string-literal type — declare a named string-literal union and reference it as an enum`, loc);
340
+ case "void":
341
+ throw new SidecarError(`'${container}.${member}' is void — void exists only as a bare union arm's payload`, loc);
342
+ case "absent":
343
+ throw new SidecarError(`'${container}.${member}' is null/undefined alone — pair it with a value type for an optional slot`, loc);
344
+ case "tuple":
345
+ throw new SidecarError(`'${container}.${member}' is a tuple — the sidecar vocabulary has slices and named records, not positional tuples`, loc);
346
+ case "unsupported":
347
+ throw new SidecarError(`'${container}.${member}' has no sidecar projection: ${shape.text}`, loc);
348
+ }
349
+ }
350
+ /** Ensure a declared type's table entry exists (recursively projecting
351
+ * what it references). Cycles refuse: recursive contract types cannot
352
+ * encode (schema rule V5). */
353
+ tableNamed(name, loc) {
354
+ if (this.table.has(name))
355
+ return;
356
+ if (name === this.config.msg) {
357
+ throw new SidecarError(`the designated msg union '${name}' cannot join the type table`, loc);
358
+ }
359
+ if (this.inProgress.has(name)) {
360
+ throw new SidecarError(`the contract type graph is cyclic through '${name}' — recursive contract types cannot encode`, loc);
361
+ }
362
+ const c = this.lookup(name, loc);
363
+ if (c.c === "alias") {
364
+ // Callers table resolved names; a designation reaching here through
365
+ // an alias still tables the aliased declaration, nothing else.
366
+ this.tableNamed(this.resolve(name, loc).name, loc);
367
+ return;
368
+ }
369
+ this.inProgress.add(name);
370
+ try {
371
+ if (c.c === "enum") {
372
+ const seen = new Set();
373
+ for (const m of c.members) {
374
+ if (seen.has(m))
375
+ throw new SidecarError(`enum '${name}' repeats member '${m}'`, c.decl.loc);
376
+ seen.add(m);
377
+ }
378
+ this.table.set(name, { kind: "enum", entry: { name, members: c.members }, anchor: c.index, sub: -1 });
379
+ return;
380
+ }
381
+ if (c.c === "struct") {
382
+ const entry = { name, fields: [] };
383
+ // Insert before projecting fields? No: cycle detection rides
384
+ // inProgress; the entry lands complete.
385
+ const seen = new Set();
386
+ for (const f of c.fields) {
387
+ if (seen.has(f.name))
388
+ throw new SidecarError(`record '${name}' repeats field '${f.name}'`, f.loc);
389
+ seen.add(f.name);
390
+ entry.fields.push({ name: f.name, type: this.fieldRef(f, name) });
391
+ }
392
+ this.table.set(name, { kind: "struct", entry, anchor: c.index, sub: -1 });
393
+ return;
394
+ }
395
+ // A named tagged union (never the msg union — fenced above), its
396
+ // arms flattened depth-first (composition + the repeat refusal live
397
+ // in unionArms).
398
+ const entry = { name, arms: [] };
399
+ for (const arm of this.unionArms(name, loc)) {
400
+ entry.arms.push({ name: arm.name, payload: this.armPayloadRef(name, arm) });
401
+ }
402
+ this.table.set(name, { kind: "union", entry, anchor: c.index, sub: -1 });
403
+ }
404
+ finally {
405
+ this.inProgress.delete(name);
406
+ }
407
+ }
408
+ /** A named union arm's payload TypeRef: void for bare arms, the single
409
+ * payload field's type, or a synthesized by-value record for
410
+ * multi-field inline payloads. */
411
+ armPayloadRef(unionName, arm) {
412
+ if (arm.fields.length === 0)
413
+ return { kind: "void" };
414
+ if (arm.fields.length === 1)
415
+ return this.fieldRef(arm.fields[0], unionName);
416
+ return { kind: "value", name: this.tableSynthesized(unionName, arm.name, arm.fields, arm.loc) };
417
+ }
418
+ /** Table an anonymous inline record under the schema's synthesized-name
419
+ * contract: `<Container>_<member>`, deterministic and stable across
420
+ * identical re-compiles, unique in the one namespace. */
421
+ tableSynthesized(container, member, fields, loc) {
422
+ const name = `${container}_${member}`;
423
+ const existing = this.table.get(name);
424
+ if (existing !== undefined)
425
+ return name; // the same inline decl, revisited
426
+ if (this.byName.has(name)) {
427
+ throw new SidecarError(`the inline record at '${container}.${member}' needs the synthesized name '${name}', which a declared type already uses — rename one`, loc);
428
+ }
429
+ const entry = { name, synthesized: true, fields: [] };
430
+ const anchor = this.byName.get(container)?.index ?? this.facts.types.length;
431
+ const sub = this.synthCounter++;
432
+ this.table.set(name, { kind: "struct", entry, anchor, sub });
433
+ const seen = new Set();
434
+ for (const f of fields) {
435
+ if (seen.has(f.name))
436
+ throw new SidecarError(`the inline record at '${container}.${member}' repeats field '${f.name}'`, f.loc);
437
+ seen.add(f.name);
438
+ entry.fields.push({ name: f.name, type: this.fieldRef(f, name) });
439
+ }
440
+ return name;
441
+ }
442
+ /** A msg arm's payload descriptor (§5's five families). */
443
+ msgDescriptor(msgName, arm) {
444
+ const fields = arm.fields;
445
+ if (fields.length === 0)
446
+ return { kind: "void" };
447
+ if (fields.length === 2) {
448
+ const [first, second] = [fields[0], fields[1]];
449
+ // Family 4 covers exactly the number-first two-field record; a
450
+ // bytes-first spelling takes the record family instead
451
+ // (declaration order is semantic everywhere — the clarified rule).
452
+ if (!first.optional &&
453
+ !second.optional &&
454
+ first.shape.k === "number" &&
455
+ (second.shape.k === "text" || second.shape.k === "bytes")) {
456
+ return { kind: "number_bytes", number_field: first.name, number_class: "f64", bytes_field: second.name };
457
+ }
458
+ }
459
+ if (fields.length === 1 && !fields[0].optional) {
460
+ const ref = this.fieldRef(fields[0], msgName);
461
+ switch (ref.kind) {
462
+ case "bytes":
463
+ return { kind: "bytes" };
464
+ case "f64":
465
+ case "i64":
466
+ return { kind: "number", class: ref.kind };
467
+ case "node":
468
+ case "value":
469
+ return { kind: "record", name: ref.name };
470
+ case "union":
471
+ return { kind: "union", name: ref.name };
472
+ case "enum":
473
+ return { kind: "enum", name: ref.name };
474
+ case "void":
475
+ return { kind: "void" };
476
+ default:
477
+ return { kind: "scalar", type: ref };
478
+ }
479
+ }
480
+ // Everything else — bytes-first pairs, three-plus fields, optional
481
+ // payload fields — tables a synthesized record (family 5).
482
+ return { kind: "record", name: this.tableSynthesized(msgName, arm.name, fields, arm.loc) };
483
+ }
484
+ /** The finished type table, each array in declaration order (synthesized
485
+ * entries anchor at their containing declaration). */
486
+ finishedTable() {
487
+ const ordered = [...this.table.values()].sort((a, b) => a.anchor - b.anchor || a.sub - b.sub);
488
+ return {
489
+ structs: ordered.filter((e) => e.kind === "struct").map((e) => e.entry),
490
+ enums: ordered.filter((e) => e.kind === "enum").map((e) => e.entry),
491
+ unions: ordered.filter((e) => e.kind === "union").map((e) => e.entry),
492
+ };
493
+ }
494
+ }
495
+ /** Whether a helper's result rides the transient result arena (anything
496
+ * materialized: buffers, slices, records, optionals) rather than a plain
497
+ * scalar return. */
498
+ function helperArena(returns) {
499
+ return returns.kind !== "bool" && returns.kind !== "f64" && returns.kind !== "i64" && returns.kind !== "enum";
500
+ }
501
+ export function buildSidecar(input) {
502
+ const { profile, facts } = input;
503
+ const config = profile.sidecar;
504
+ const entryLoc = { file: profile.entry, start: 0, end: 0 };
505
+ const diagnostics = [];
506
+ for (const m of facts.malformedConsts) {
507
+ diagnostics.push(libSidecarDiag(`exported const '${m.name}' ${m.detail}`, m.loc));
508
+ }
509
+ if (diagnostics.length > 0)
510
+ return { ok: false, diagnostics };
511
+ try {
512
+ const projector = new Projector(facts, config, entryLoc);
513
+ // The model: the designated root state type, a record in the table.
514
+ const modelClass = projector.lookup(config.model, entryLoc);
515
+ if (modelClass.c !== "struct") {
516
+ throw new SidecarError(`the profile designates model '${config.model}', which is not a record type`, modelClass.decl.loc);
517
+ }
518
+ projector.tableNamed(config.model, entryLoc);
519
+ const modelFieldNames = new Set(modelClass.fields.map((f) => f.name));
520
+ // The msg union: declaration-order arms (composed constituents
521
+ // flattened depth-first in source order, first occurrence winning a
522
+ // duplicated arm name — the reducer-composition rule), positional
523
+ // wire tags, at most 256 arms (tags ride a u8).
524
+ const msgClass = projector.lookup(config.msg, entryLoc);
525
+ if (msgClass.c !== "tagged") {
526
+ throw new SidecarError(`the profile designates msg '${config.msg}', which is not a kind-tagged union of object literals`, msgClass.decl.loc);
527
+ }
528
+ const flatArms = projector.unionArms(config.msg, entryLoc);
529
+ if (flatArms.length > 256) {
530
+ throw new SidecarError(`msg union '${config.msg}' declares ${flatArms.length} arms — wire tags ride a u8, so at most 256 are permitted`, msgClass.decl.loc);
531
+ }
532
+ const msgArms = flatArms.map((arm) => ({
533
+ name: arm.name,
534
+ payload: projector.msgDescriptor(config.msg, arm),
535
+ }));
536
+ const armByName = new Map(msgArms.map((a) => [a.name, a.payload]));
537
+ // Helpers: exported functions taking the model first, in declaration
538
+ // order (the array index is the ABI call index), minus the designated
539
+ // init/update/subscriptions entries.
540
+ const designated = new Set([config.initExport, config.updateExport, config.subscriptionsExport]);
541
+ const helpers = [];
542
+ for (const fn of facts.functions) {
543
+ if (designated.has(fn.name))
544
+ continue;
545
+ const first = fn.params[0];
546
+ if (first === undefined || first.shape === null || first.shape.k !== "ref" || first.shape.name !== config.model)
547
+ continue;
548
+ if (fn.generic) {
549
+ throw new SidecarError(`helper '${fn.name}' is generic — a contract helper needs one concrete signature`, fn.loc);
550
+ }
551
+ const params = [];
552
+ fn.params.slice(1).forEach((p, i) => {
553
+ if (p.shape === null) {
554
+ throw new SidecarError(`helper '${fn.name}' parameter ${i + 2} ('${p.name}') has no type annotation`, fn.loc);
555
+ }
556
+ params.push(projector.shapeRef(p.shape, `helpers_${fn.name}`, p.name, fn.loc));
557
+ });
558
+ if (fn.returns === null) {
559
+ throw new SidecarError(`helper '${fn.name}' has no return type annotation`, fn.loc);
560
+ }
561
+ if (fn.returns.k === "void") {
562
+ throw new SidecarError(`helper '${fn.name}' returns void — a contract helper returns a value the host can read`, fn.loc);
563
+ }
564
+ // Helper-return synthesized names are two-part like everything else:
565
+ // container 'helpers', member the helper's name — `helpers_<name>`,
566
+ // never a '_return' suffix (the ratified spelling).
567
+ const returns = projector.shapeRef(fn.returns, "helpers", fn.name, fn.loc);
568
+ if (helpers.some((h) => h.name === fn.name)) {
569
+ throw new SidecarError(`helper '${fn.name}' is declared twice`, fn.loc);
570
+ }
571
+ helpers.push({ name: fn.name, params, returns, arena: helperArena(returns) });
572
+ }
573
+ const helperNames = new Set(helpers.map((h) => h.name));
574
+ // Shape flags from the designated entries' declared signatures.
575
+ const returnsCmd = (which, exportName) => {
576
+ const fn = facts.functions.find((f) => f.name === exportName);
577
+ if (fn === undefined) {
578
+ throw new SidecarError(`the profile designates ${which} export '${exportName}', but the entry module exports no function by that name`, entryLoc);
579
+ }
580
+ const r = fn.returns;
581
+ if (r !== null && r.k === "ref" && r.name === config.model)
582
+ return false;
583
+ if (r !== null && r.k === "tuple" && r.elems.length === 2 && r.elems[0].k === "ref" && r.elems[0].name === config.model) {
584
+ return true;
585
+ }
586
+ throw new SidecarError(`${which} export '${exportName}' must declare its return as '${config.model}' (bare state) or a two-element tuple '[${config.model}, ...]' (state plus an effect value)`, fn.loc);
587
+ };
588
+ const initReturnsCmd = returnsCmd("init", config.initExport);
589
+ const updateReturnsCmd = returnsCmd("update", config.updateExport);
590
+ const hasSubscriptions = facts.functions.some((f) => f.name === config.subscriptionsExport);
591
+ // Unbound lists: model fields or helper entries (helpers are bindable
592
+ // surface — the clarified rule) on the model side, arm names on the
593
+ // msg side. Absent consts mean "the program declares none".
594
+ const modelUnbound = facts.modelUnbound?.value ?? [];
595
+ for (const name of modelUnbound) {
596
+ if (!modelFieldNames.has(name) && !helperNames.has(name)) {
597
+ throw new SidecarError(`modelUnbound names '${name}', which is neither a field of '${config.model}' nor a helper entry`, facts.modelUnbound.loc);
598
+ }
599
+ }
600
+ const msgUnbound = facts.msgUnbound?.value ?? [];
601
+ for (const name of msgUnbound) {
602
+ if (!armByName.has(name)) {
603
+ throw new SidecarError(`msgUnbound names '${name}', which is not an arm of '${config.msg}'`, facts.msgUnbound.loc);
604
+ }
605
+ }
606
+ // Channels: the four function channels answer export presence by
607
+ // suffix; the two host-constructed channels and the env map ride the
608
+ // exported-const conventions and must target conforming arms.
609
+ const exports = abiExportSuffixes(profile);
610
+ const exportSet = new Set(exports);
611
+ const namedChannel = (constName) => {
612
+ const c = facts[constName];
613
+ if (c === null)
614
+ return null;
615
+ const payload = armByName.get(c.value);
616
+ if (payload === undefined) {
617
+ throw new SidecarError(`${constName} names '${c.value}', which is not an arm of '${config.msg}'`, c.loc);
618
+ }
619
+ if (payload.kind !== "record" && payload.kind !== "union" && payload.kind !== "enum" && payload.kind !== "scalar") {
620
+ throw new SidecarError(`${constName} names arm '${c.value}', whose payload descriptor is '${payload.kind}' — a host-constructed channel arm needs a named-type-family payload`, c.loc);
621
+ }
622
+ return c.value;
623
+ };
624
+ const envMsgs = facts.envMsgs?.value ?? [];
625
+ const seenEnv = new Set();
626
+ for (const e of envMsgs) {
627
+ if (seenEnv.has(e.env)) {
628
+ throw new SidecarError(`envMsgs repeats environment variable '${e.env}'`, facts.envMsgs.loc);
629
+ }
630
+ seenEnv.add(e.env);
631
+ const payload = armByName.get(e.msg);
632
+ if (payload === undefined) {
633
+ throw new SidecarError(`envMsgs targets '${e.msg}', which is not an arm of '${config.msg}'`, facts.envMsgs.loc);
634
+ }
635
+ if (payload.kind !== "bytes") {
636
+ throw new SidecarError(`envMsgs targets arm '${e.msg}', whose payload descriptor is '${payload.kind}' — the host delivers an environment value as bytes`, facts.envMsgs.loc);
637
+ }
638
+ }
639
+ const doc = {
640
+ format: SIDECAR_FORMAT,
641
+ wire_version: config.wireVersion,
642
+ abi_version: config.abiVersion,
643
+ compiler_version: input.compilerVersion,
644
+ entry: input.entry,
645
+ source_hash: input.sourceHash,
646
+ build_id: input.buildId,
647
+ types: projector.finishedTable(),
648
+ model: config.model,
649
+ model_helpers: helpers,
650
+ model_unbound: modelUnbound,
651
+ msg: { name: config.msg, arms: msgArms, unbound: msgUnbound },
652
+ init_returns_cmd: initReturnsCmd,
653
+ update_returns_cmd: updateReturnsCmd,
654
+ has_subscriptions: hasSubscriptions,
655
+ channels: {
656
+ command_msg: exportSet.has("command_msg"),
657
+ frame_msg: exportSet.has("frame_msg"),
658
+ key_msg: exportSet.has("key_msg"),
659
+ pinch_msg: exportSet.has("pinch_msg"),
660
+ appearance_msg: namedChannel("appearanceMsg"),
661
+ chrome_msg: namedChannel("chromeMsg"),
662
+ env_msgs: envMsgs,
663
+ },
664
+ abi: { prefix: profile.prefix, exports, snapshot_format: config.snapshotFormat },
665
+ // The pre-ask-4 absent form the schema names valid: every numeric
666
+ // slot above spelled f64, and the empty list is itself the
667
+ // attestation that no slot was integer-classed.
668
+ integer_slots: [],
669
+ deterministic: input.deterministic,
670
+ // Structural in library mode: the SC4005 gate refused any graph
671
+ // reaching async/timer/event-loop surface before emission, so a
672
+ // sidecar exists only for async_free graphs.
673
+ async_free: true,
674
+ };
675
+ return { ok: true, doc, json: JSON.stringify(doc, null, 2) + "\n" };
676
+ }
677
+ catch (e) {
678
+ if (e instanceof SidecarRefusal) {
679
+ return { ok: false, diagnostics: [e.diag] };
680
+ }
681
+ if (e instanceof SidecarError) {
682
+ return { ok: false, diagnostics: [libSidecarDiag(e.detail, e.loc)] };
683
+ }
684
+ throw e;
685
+ }
686
+ }
687
+ //# sourceMappingURL=sidecar.js.map