@elaraai/east 1.0.67 → 1.0.69

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 (79) hide show
  1. package/dist/src/ast.d.ts +2 -0
  2. package/dist/src/ast.d.ts.map +1 -1
  3. package/dist/src/ast_to_ir.d.ts +7 -3
  4. package/dist/src/ast_to_ir.d.ts.map +1 -1
  5. package/dist/src/ast_to_ir.js +62 -15
  6. package/dist/src/ast_to_ir.js.map +1 -1
  7. package/dist/src/codegen/codegen.spec.d.ts +6 -0
  8. package/dist/src/codegen/codegen.spec.d.ts.map +1 -0
  9. package/dist/src/codegen/codegen.spec.js +763 -0
  10. package/dist/src/codegen/codegen.spec.js.map +1 -0
  11. package/dist/src/codegen/doc.d.ts +111 -0
  12. package/dist/src/codegen/doc.d.ts.map +1 -0
  13. package/dist/src/codegen/doc.js +350 -0
  14. package/dist/src/codegen/doc.js.map +1 -0
  15. package/dist/src/codegen/doc.spec.d.ts +6 -0
  16. package/dist/src/codegen/doc.spec.d.ts.map +1 -0
  17. package/dist/src/codegen/doc.spec.js +86 -0
  18. package/dist/src/codegen/doc.spec.js.map +1 -0
  19. package/dist/src/codegen/index.d.ts +15 -0
  20. package/dist/src/codegen/index.d.ts.map +1 -0
  21. package/dist/src/codegen/index.js +15 -0
  22. package/dist/src/codegen/index.js.map +1 -0
  23. package/dist/src/codegen/printer.d.ts +63 -0
  24. package/dist/src/codegen/printer.d.ts.map +1 -0
  25. package/dist/src/codegen/printer.js +1529 -0
  26. package/dist/src/codegen/printer.js.map +1 -0
  27. package/dist/src/codegen/spellings.d.ts +73 -0
  28. package/dist/src/codegen/spellings.d.ts.map +1 -0
  29. package/dist/src/codegen/spellings.js +298 -0
  30. package/dist/src/codegen/spellings.js.map +1 -0
  31. package/dist/src/codegen/spellings.spec.d.ts +6 -0
  32. package/dist/src/codegen/spellings.spec.d.ts.map +1 -0
  33. package/dist/src/codegen/spellings.spec.js +191 -0
  34. package/dist/src/codegen/spellings.spec.js.map +1 -0
  35. package/dist/src/codegen/types.d.ts +67 -0
  36. package/dist/src/codegen/types.d.ts.map +1 -0
  37. package/dist/src/codegen/types.js +173 -0
  38. package/dist/src/codegen/types.js.map +1 -0
  39. package/dist/src/compile.js +10 -10
  40. package/dist/src/compile.js.map +1 -1
  41. package/dist/src/expr/block.d.ts +148 -0
  42. package/dist/src/expr/block.d.ts.map +1 -1
  43. package/dist/src/expr/block.js +168 -4
  44. package/dist/src/expr/block.js.map +1 -1
  45. package/dist/src/expr/index.d.ts +155 -2
  46. package/dist/src/expr/index.d.ts.map +1 -1
  47. package/dist/src/expr/index.js +155 -2
  48. package/dist/src/expr/index.js.map +1 -1
  49. package/dist/src/functions.crossimport.spec.d.ts +6 -0
  50. package/dist/src/functions.crossimport.spec.d.ts.map +1 -0
  51. package/dist/src/functions.crossimport.spec.js +45 -0
  52. package/dist/src/functions.crossimport.spec.js.map +1 -0
  53. package/dist/src/functions.d.ts +4103 -0
  54. package/dist/src/functions.d.ts.map +1 -0
  55. package/dist/src/functions.js +443 -0
  56. package/dist/src/functions.js.map +1 -0
  57. package/dist/src/functions.spec.d.ts +6 -0
  58. package/dist/src/functions.spec.d.ts.map +1 -0
  59. package/dist/src/functions.spec.js +123 -0
  60. package/dist/src/functions.spec.js.map +1 -0
  61. package/dist/src/index.d.ts +2 -0
  62. package/dist/src/index.d.ts.map +1 -1
  63. package/dist/src/index.js +2 -0
  64. package/dist/src/index.js.map +1 -1
  65. package/dist/src/naming.d.ts +50 -0
  66. package/dist/src/naming.d.ts.map +1 -0
  67. package/dist/src/naming.js +194 -0
  68. package/dist/src/naming.js.map +1 -0
  69. package/dist/src/naming.spec.d.ts +6 -0
  70. package/dist/src/naming.spec.d.ts.map +1 -0
  71. package/dist/src/naming.spec.js +154 -0
  72. package/dist/src/naming.spec.js.map +1 -0
  73. package/dist/src/serialization/beast2/index.spec.js +25 -0
  74. package/dist/src/serialization/beast2/index.spec.js.map +1 -1
  75. package/dist/src/serialization/beast2/shared.d.ts +1 -1
  76. package/dist/src/serialization/beast2/shared.d.ts.map +1 -1
  77. package/dist/src/serialization/beast2/shared.js +9 -1
  78. package/dist/src/serialization/beast2/shared.js.map +1 -1
  79. package/package.json +11 -2
@@ -0,0 +1,763 @@
1
+ /**
2
+ * Copyright (c) 2025 Elara AI Pty Ltd
3
+ * Dual-licensed under AGPL-3.0 and commercial license. See LICENSE for details.
4
+ */
5
+ /**
6
+ * The IR → TypeScript → IR round trip (#628): `toSource` prints a module,
7
+ * the module rebuilds through `East.function`, and the rebuilt IR equals
8
+ * the original up to the normalization the cross-runtime contract allows
9
+ * (loc_ids, canonical variable and label names, recursive type ids).
10
+ *
11
+ * Three legs: hand-written functions covering every node kind and spelling
12
+ * family; every exported `*.examples.ts` example (`/tmp/east-examples-ir`,
13
+ * `npm run export:examples`), which also executes and must compute its
14
+ * declared `returns`; every compliance-corpus program (`/tmp/east-test-ir`,
15
+ * `npm run test:export`). The two exported legs skip when the directories
16
+ * are absent — the three-way sweep in east-py runs them under CI's
17
+ * `EAST_CONFORMANCE_REQUIRED=1`.
18
+ */
19
+ import { describe, test, after } from "node:test";
20
+ import assert from "node:assert/strict";
21
+ import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
22
+ import { tmpdir } from "node:os";
23
+ import { dirname, join } from "node:path";
24
+ import { pathToFileURL } from "node:url";
25
+ import { East, Expr, variant, ref, some, none, ArrayType, BlobType, DictType, FloatType, FunctionType, IntegerType, NullType, OptionType, RecursiveType, SetType, StringType, StructType, VariantType, VectorType, IRType, EastTypeType, fromJSONFor, equalFor, isVariant, isPlatformDeclaration, toSource, RAW_ONLY, } from "../index.js";
26
+ // ── the harness ─────────────────────────────────────────────────────────────
27
+ const INDEX_URL = new URL("../index.js", import.meta.url).href;
28
+ const TMP = mkdtempSync(join(tmpdir(), "east-codegen-"));
29
+ /** Set to a directory to keep every printed module (`<label>.mjs`). */
30
+ const SAVE_DIR = process.env["EAST_CONFORMANCE_SAVE"];
31
+ let moduleCounter = 0;
32
+ after(() => rmSync(TMP, { recursive: true, force: true }));
33
+ /** Evaluates a printed module and returns its `main` export. */
34
+ async function rebuild(source) {
35
+ moduleCounter += 1;
36
+ const path = join(TMP, `m${moduleCounter}.mjs`);
37
+ writeFileSync(path, source, "utf-8");
38
+ const mod = await import(pathToFileURL(path).href);
39
+ return mod.main;
40
+ }
41
+ /**
42
+ * The normalized shape of an IR value: loc_ids zeroed, variables and labels
43
+ * renamed in first-occurrence order, recursive type ids renumbered, bigints
44
+ * as strings — what `east-c ir normalize` erases, erased here in TypeScript.
45
+ *
46
+ * Variables are renamed per BINDING, resolved lexically — a parameter, a
47
+ * Let, a loop or match or catch variable each mint a canonical name for the
48
+ * body they scope — not per name: since #639 sibling bodies reuse a name
49
+ * (three callbacks each naming their element `x`), and two programs that
50
+ * bind the same variable under different names (a printed module drops an
51
+ * empty `.catch`, whose default variables the rebuild names afresh) are the
52
+ * same program.
53
+ */
54
+ function canonical(node) {
55
+ const labels = new Map();
56
+ const recursive = new Map();
57
+ let variables = 0;
58
+ const rename = (table, name, prefix) => {
59
+ let hit = table.get(name);
60
+ if (hit === undefined) {
61
+ hit = `${prefix}${table.size}`;
62
+ table.set(name, hit);
63
+ }
64
+ return hit;
65
+ };
66
+ /** Binds `variable` (a Variable node) in `scope`: its canonical name, minted in binding order. */
67
+ const bind = (variable, scope) => {
68
+ const name = `v${variables}`;
69
+ variables += 1;
70
+ scope.set(variable.value.name, name);
71
+ return variableNode(variable, name);
72
+ };
73
+ const variableNode = (v, name) => ["Variable", { type: walk(v.value.type, "type", new Map()), name, mutable: v.value.mutable, captured: v.value.captured }];
74
+ const lookup = (scopes, name) => {
75
+ for (let i = scopes.length - 1; i >= 0; i--) {
76
+ const hit = scopes[i].get(name);
77
+ if (hit !== undefined)
78
+ return hit;
79
+ }
80
+ return `free:${name}`;
81
+ };
82
+ // the scope chain is threaded through the walk: a body opens a scope, a Let extends the current one
83
+ let chain = [new Map()];
84
+ const inScope = (f) => {
85
+ chain = [...chain, new Map()];
86
+ try {
87
+ return f();
88
+ }
89
+ finally {
90
+ chain = chain.slice(0, -1);
91
+ }
92
+ };
93
+ const walk = (v, context, _scope) => {
94
+ if (v === null || v === undefined)
95
+ return null;
96
+ if (typeof v === "bigint")
97
+ return v.toString();
98
+ if (v instanceof Date)
99
+ return v.toISOString();
100
+ if (v instanceof Uint8Array)
101
+ return Array.from(v);
102
+ if (Array.isArray(v))
103
+ return v.map(x => walk(x, context));
104
+ if (isVariant(v)) {
105
+ const tag = v.type;
106
+ const p = v.value;
107
+ if (context === "type") {
108
+ if (tag === "Recursive") {
109
+ const payload = v.value;
110
+ if (payload.type === "ref")
111
+ return ["Recursive", ["ref", rename(recursive, String(payload.value), "r")]];
112
+ return ["Recursive", ["wrapper", { id: rename(recursive, String(payload.value.id), "r"), inner: walk(payload.value.inner, "type") }]];
113
+ }
114
+ return [tag, walk(v.value, context)];
115
+ }
116
+ switch (tag) {
117
+ case "Variable":
118
+ return variableNode(v, lookup(chain, p.name));
119
+ case "Function":
120
+ case "AsyncFunction":
121
+ return inScope(() => [tag, {
122
+ type: walk(p.type, "type"),
123
+ parameters: p.parameters.map(q => bind(q, chain[chain.length - 1])),
124
+ body: walk(p.body, context),
125
+ captures: walk(p.captures, context),
126
+ }]);
127
+ case "Let": {
128
+ const value = walk(p.value, context);
129
+ return [tag, { type: walk(p.type, "type"), value, variable: bind(p.variable, chain[chain.length - 1]) }];
130
+ }
131
+ case "ForArray":
132
+ case "ForDict":
133
+ case "ForSet": {
134
+ const source = tag === "ForArray" ? "array" : tag === "ForDict" ? "dict" : "set";
135
+ const coll = walk(p[source], context);
136
+ return inScope(() => {
137
+ const scope = chain[chain.length - 1];
138
+ const out = { type: walk(p.type, "type"), [source]: coll, label: { name: rename(labels, p.label.name, "L") } };
139
+ if (tag !== "ForSet")
140
+ out["value"] = bind(p.value, scope);
141
+ out["key"] = bind(p.key, scope);
142
+ out["body"] = walk(p.body, context);
143
+ return [tag, out];
144
+ });
145
+ }
146
+ case "Match": {
147
+ const variant = walk(p.variant, context);
148
+ const cases = p.cases.map(c => inScope(() => ({
149
+ case: c.case, variable: bind(c.variable, chain[chain.length - 1]), body: walk(c.body, context),
150
+ })));
151
+ return [tag, { type: walk(p.type, "type"), variant, cases }];
152
+ }
153
+ case "TryCatch": {
154
+ const tryBody = inScope(() => walk(p.try_body, context));
155
+ const caught = inScope(() => {
156
+ const scope = chain[chain.length - 1];
157
+ const message = bind(p.message, scope);
158
+ const stack = bind(p.stack, scope);
159
+ return { message, stack, catch_body: walk(p.catch_body, context) };
160
+ });
161
+ const finallyBody = inScope(() => walk(p.finally_body, context));
162
+ return [tag, { type: walk(p.type, "type"), try_body: tryBody, ...caught, finally_body: finallyBody }];
163
+ }
164
+ case "Block":
165
+ return inScope(() => [tag, { type: walk(p.type, "type"), statements: walk(p.statements, context) }]);
166
+ case "While":
167
+ return [tag, { type: walk(p.type, "type"), predicate: walk(p.predicate, context), label: { name: rename(labels, p.label.name, "L") }, body: inScope(() => walk(p.body, context)) }];
168
+ case "IfElse":
169
+ return [tag, {
170
+ type: walk(p.type, "type"),
171
+ ifs: p.ifs.map(branch => ({ predicate: walk(branch.predicate, context), body: inScope(() => walk(branch.body, context)) })),
172
+ else_body: inScope(() => walk(p.else_body, context)),
173
+ }];
174
+ default:
175
+ return [tag, walk(v.value, context)];
176
+ }
177
+ }
178
+ if (typeof v === "object") {
179
+ const out = {};
180
+ for (const key of Object.keys(v).sort()) {
181
+ if (key === "loc_id") {
182
+ out[key] = "0";
183
+ continue;
184
+ }
185
+ if (key === "label" && typeof v[key] === "object" && "name" in v[key]) {
186
+ out[key] = { name: rename(labels, v[key].name, "L") };
187
+ continue;
188
+ }
189
+ out[key] = walk(v[key], key === "type" || key === "type_parameters" ? "type" : context);
190
+ }
191
+ return out;
192
+ }
193
+ return v;
194
+ };
195
+ return walk(node, null);
196
+ }
197
+ /** The first path at which two canonical forms differ, or `null`. */
198
+ function firstDifference(a, b, path = "ir") {
199
+ if (Object.is(a, b))
200
+ return null;
201
+ if (Array.isArray(a) && Array.isArray(b)) {
202
+ if (a.length !== b.length)
203
+ return `${path}.length: ${a.length} vs ${b.length}`;
204
+ for (let i = 0; i < a.length; i++) {
205
+ const d = firstDifference(a[i], b[i], `${path}[${i}]`);
206
+ if (d !== null)
207
+ return d;
208
+ }
209
+ return null;
210
+ }
211
+ if (a !== null && b !== null && typeof a === "object" && typeof b === "object") {
212
+ for (const k of [...new Set([...Object.keys(a), ...Object.keys(b)])].sort()) {
213
+ const d = firstDifference(a[k], b[k], `${path}.${k}`);
214
+ if (d !== null)
215
+ return d;
216
+ }
217
+ return null;
218
+ }
219
+ return `${path}: ${String(a)} vs ${String(b)}`;
220
+ }
221
+ /** A host value for a message: bigints, Maps and Sets made printable. */
222
+ function show(v) {
223
+ return JSON.stringify(v, (_k, x) => typeof x === "bigint" ? `${x}n` : x instanceof Map ? [...x] : x instanceof Set ? [...x] : x);
224
+ }
225
+ /** Prints, rebuilds, and checks the rebuilt IR against the original. */
226
+ async function roundTrip(fn, label, options = {}) {
227
+ const ir = fn.toIR ? fn.toIR().ir : fn;
228
+ const source = toSource(ir, { importFrom: INDEX_URL, ...options });
229
+ if (SAVE_DIR) {
230
+ const out = join(SAVE_DIR, `${label}.mjs`);
231
+ mkdirSync(dirname(out), { recursive: true });
232
+ writeFileSync(out, source, "utf-8");
233
+ }
234
+ assert.equal(toSource(ir, { importFrom: INDEX_URL, ...options }), source, `${label}: printing is deterministic`);
235
+ let main;
236
+ let rebuilt;
237
+ try {
238
+ main = await rebuild(source);
239
+ rebuilt = canonical(main.toIR().ir);
240
+ }
241
+ catch (e) {
242
+ throw new Error(`${label}: the printed module does not rebuild — ${e.message}\n${source}`);
243
+ }
244
+ const original = canonical(ir);
245
+ const diff = firstDifference(rebuilt, original);
246
+ assert.equal(diff, null, `${label}: rebuilt IR differs (rebuilt vs original) at ${diff}\n${source}`);
247
+ return main;
248
+ }
249
+ /**
250
+ * Runs `check` over every file, collecting failures, and fails once with
251
+ * the list — one line per failure (the sources are kept under
252
+ * `EAST_CONFORMANCE_SAVE`).
253
+ */
254
+ async function sweep(files, labelOf, check) {
255
+ const failures = [];
256
+ for (const file of files) {
257
+ try {
258
+ await check(file);
259
+ }
260
+ catch (e) {
261
+ const line = e.message.split("\n")[0];
262
+ failures.push(line.startsWith(labelOf(file)) ? line : `${labelOf(file)}: ${line}`);
263
+ }
264
+ }
265
+ assert.equal(failures.length, 0, `${failures.length} of ${files.length} failed:\n ${failures.join("\n ")}`);
266
+ }
267
+ // ── the harness's own contract ──────────────────────────────────────────────
268
+ describe("codegen: the canonical form is alpha-equivalence", () => {
269
+ test("programs that bind the same variables under different names are the same program", () => {
270
+ const a = East.function([ArrayType(IntegerType)], IntegerType, ($, xs) => {
271
+ const total = $.let(0n);
272
+ $.for(xs, ($, x) => { $.assign(total, total.add(x)); });
273
+ const doubled = $.const(xs.map(($, x) => x.multiply(2n))); // a sibling reuses `x`
274
+ return total.add(doubled.size());
275
+ });
276
+ const b = East.function([ArrayType(IntegerType)], IntegerType, ($, items) => {
277
+ const acc = $.let(0n);
278
+ $.for(items, ($, item) => { $.assign(acc, acc.add(item)); });
279
+ const twice = $.const(items.map(($, other) => other.multiply(2n))); // distinct names
280
+ return acc.add(twice.size());
281
+ });
282
+ assert.equal(firstDifference(canonical(a.toIR().ir), canonical(b.toIR().ir)), null);
283
+ });
284
+ test("a reference that resolves to another binding is a different program", () => {
285
+ const outer = East.function([IntegerType, IntegerType], IntegerType, ($, x, y) => East.value([x]).map(($, z) => z.add(y)).get(0n)); // adds the parameter y
286
+ const inner = East.function([IntegerType, IntegerType], IntegerType, ($, x, _y) => East.value([x]).map(($, z) => z.add(z)).get(0n)); // adds the element itself
287
+ assert.notEqual(firstDifference(canonical(outer.toIR().ir), canonical(inner.toIR().ir)), null);
288
+ });
289
+ });
290
+ // ── hand-written coverage of every node kind ────────────────────────────────
291
+ describe("codegen: toSource round trips the builder surface", () => {
292
+ test("arithmetic, comparisons and literals", async () => {
293
+ const fn = East.function([IntegerType, FloatType], StringType, ($, n, x) => {
294
+ const twice = $.const(n.multiply(2n).add(1n).subtract(3n));
295
+ const ratio = $.let(x.divide(2.5).add(n.toFloat()).sqrt());
296
+ $.assign(ratio, ratio.negate().abs());
297
+ const flag = $.const(East.less(twice, 10n).bitAnd(East.greaterEqual(ratio, 0.0)).not());
298
+ return East.str `${twice} ${ratio} ${flag} ${East.value("x").length()}`;
299
+ });
300
+ const main = await roundTrip(fn, "arithmetic");
301
+ assert.equal(main.toIR().compile([])(4n, 9.0), fn.toIR().compile([])(4n, 9.0));
302
+ });
303
+ test("statements: if / while / for / match / try / return / break / continue", async () => {
304
+ const Shape = VariantType({ circle: FloatType, square: FloatType, none: NullType });
305
+ const fn = East.function([ArrayType(IntegerType), Shape], IntegerType, ($, xs, shape) => {
306
+ const total = $.let(0n);
307
+ $.for(xs, ($, x, i, label) => {
308
+ $.if(East.equal(x, 0n), $ => { $.continue(label); })
309
+ .elseIf(East.greater(x, 100n), $ => { $.break(label); })
310
+ .else($ => { $.assign(total, total.add(x.multiply(i))); });
311
+ });
312
+ const count = $.let(0n);
313
+ $.while(East.less(count, 3n), ($, label) => {
314
+ $.assign(count, count.add(1n));
315
+ $.if(East.equal(count, 2n), $ => { $.continue(label); });
316
+ });
317
+ $.match(shape, {
318
+ circle: ($, r) => { $.assign(total, total.add(r.toInteger())); },
319
+ square: ($, s) => { $.assign(total, total.subtract(s.toInteger())); },
320
+ });
321
+ $.try($ => {
322
+ $.if(East.less(total, 0n), $ => { $.error("negative"); });
323
+ }).catch(($, message, stack) => {
324
+ $(message.length());
325
+ $.assign(total, stack.size());
326
+ }).finally($ => {
327
+ $.assign(count, 0n);
328
+ });
329
+ $.if(East.equal(total, 7n), $ => { $.return(-1n); });
330
+ $.return(total.add(count));
331
+ });
332
+ const main = await roundTrip(fn, "statements");
333
+ const run = main.toIR().compile([]);
334
+ assert.equal(run([1n, 2n, 0n, 3n], variant("circle", 2.0)), fn.toIR().compile([])([1n, 2n, 0n, 3n], variant("circle", 2.0)));
335
+ });
336
+ test("expressions: ifElse / match / tryCatch / block / nested function / callbacks", async () => {
337
+ const fn = East.function([ArrayType(IntegerType), OptionType(StringType)], StringType, ($, xs, name) => {
338
+ const label = $.const(name.match({ some: ($, s) => s, none: ($) => "anonymous" }));
339
+ const evens = $.const(xs.filter(($, x) => East.equal(x.remainder(2n), 0n)));
340
+ const sum = $.const(evens.reduce(($, acc, x) => acc.add(x), 0n));
341
+ const doubled = $.const(xs.map(($, x) => x.multiply(2n)));
342
+ const big = $.const(East.less(sum, 10n).ifElse($ => "small", $ => Expr.block($ => {
343
+ const s = $.const(East.value("big").concat("!"));
344
+ return s;
345
+ })));
346
+ const safe = $.const(Expr.tryCatch(xs.get(99n), ($, message, stack) => stack.size()));
347
+ const helper = $.const(East.function([IntegerType], IntegerType, ($, y) => y.add(sum)));
348
+ const applied = $.const(helper(safe));
349
+ const grouped = $.const(xs.groupReduce(($, x) => x.remainder(3n), ($, _k) => 0n, ($, acc, x) => acc.add(x)));
350
+ const keys = $.const(grouped.keys().size());
351
+ return East.str `${label} ${big} ${applied} ${doubled.size()} ${keys}`;
352
+ });
353
+ const main = await roundTrip(fn, "expressions");
354
+ const args = [[1n, 2n, 3n, 4n], variant("some", "z")];
355
+ assert.equal(main.toIR().compile([])(...args), fn.toIR().compile([])(...args));
356
+ });
357
+ test("values: struct / variant / array / set / dict / ref / vector / recursive / as", async () => {
358
+ const Point = StructType({ x: FloatType, y: FloatType, "odd-name": IntegerType });
359
+ const ListType = RecursiveType(self => VariantType({ nil: NullType, cons: StructType({ head: IntegerType, tail: self }) }));
360
+ const Wide = VariantType({ a: IntegerType, b: StringType });
361
+ const fn = East.function([IntegerType], IntegerType, ($, n) => {
362
+ const p = $.const(East.value({ x: 1.5, y: 2.5, "odd-name": n }, Point));
363
+ const arr = $.const(East.value([n, n.add(1n)], ArrayType(IntegerType)));
364
+ const set = $.const(East.value(new Set([1n, 2n]), SetType(IntegerType)));
365
+ const dict = $.const(East.value(new Map([["k", n]]), DictType(StringType, IntegerType)));
366
+ const cell = $.const(East.value(ref(n)));
367
+ const vec = $.const(East.value(new Float64Array([1.0, 2.0]), VectorType(FloatType)));
368
+ const nil = $.const(East.wrapRecursive(variant("nil", null), ListType));
369
+ const list = $.const(East.wrapRecursive(variant("cons", { head: n, tail: nil }), ListType));
370
+ const wide = $.const(East.as(East.value(variant("a", n)), Wide));
371
+ const head = $.const(list.unwrap().match({ nil: ($) => 0n, cons: ($, c) => c.head }));
372
+ const unwide = $.const(wide.match({ a: ($, v) => v, b: ($, s) => s.length() }));
373
+ const opt = $.const(some(n), OptionType(IntegerType));
374
+ const nothing = $.const(none, OptionType(IntegerType));
375
+ const picked = $.const(East.value([some(n), none], ArrayType(OptionType(IntegerType))));
376
+ const nested = $.const(new Map([["a", new Set([1n, 2n])], ["b", new Set([3n])]]), DictType(StringType, SetType(IntegerType)));
377
+ const rows = $.const([{ x: 1.5, y: 2.5, "odd-name": n }, { x: 0.0, y: 0.0, "odd-name": 0n }], ArrayType(Point));
378
+ const deep = $.const(some({ x: 1.0, y: 2.0, "odd-name": n }), OptionType(Point));
379
+ return p["odd-name"].add(arr.size()).add(set.size()).add(dict.size()).add(vec.length()).add(head)
380
+ .add(unwide).add(cell.get()).add(opt.unwrap()).add(nothing.match({ some: ($, v) => v, none: ($) => 0n }))
381
+ .add(picked.size()).add(nested.size()).add(rows.size()).add(deep.unwrap()["odd-name"]);
382
+ });
383
+ // The SHAPES, printed on one line each (`width: Infinity`); the layout has its own test.
384
+ const source = toSource(fn, { importFrom: INDEX_URL, width: Infinity });
385
+ // A bound construction is the host literal with the type on the binding; an Option is `some` / `none`.
386
+ assert.doesNotMatch(source, /\$\.(?:let|const)\(East\.value\(/, source);
387
+ // A type whose source fits on a line prints inline wherever it is used; a recursive one is hoisted.
388
+ assert.match(source, /const p = \$\.const\(\{ x: 1\.5, y: 2\.5, "odd-name": n \}, StructType\(\{ x: FloatType, y: FloatType, "odd-name": IntegerType \}\)\);/);
389
+ assert.match(source, /const dict = \$\.const\(new Map\(\[\["k", n\]\]\), DictType\(StringType, IntegerType\)\);/);
390
+ assert.match(source, /const cell = \$\.const\(ref\(n\), RefType\(IntegerType\)\);/);
391
+ assert.match(source, /const wide = \$\.const\(variant\("a", n\), VariantType\(\{ a: IntegerType, b: StringType \}\)\);/); // a literal under East.as is retyped, no As node
392
+ assert.match(source, /const opt = \$\.const\(some\(n\), OptionType\(IntegerType\)\);/);
393
+ assert.match(source, /const nothing = \$\.const\(none, OptionType\(IntegerType\)\);/);
394
+ // The binding's type governs the whole literal: a construction nested anywhere inside prints bare.
395
+ assert.match(source, /const picked = \$\.const\(\[some\(n\), none\], ArrayType\(OptionType\(IntegerType\)\)\);/);
396
+ assert.match(source, /const nested = \$\.const\(new Map\(\[\["a", new Set\(\[1n, 2n\]\)\], \["b", new Set\(\[3n\]\)\]\]\), DictType\(StringType, SetType\(IntegerType\)\)\);/);
397
+ assert.match(source, /const rows = \$\.const\(\[\{ x: 1\.5, y: 2\.5, "odd-name": n \}, \{ x: 0, y: 0, "odd-name": 0n \}\], ArrayType\(StructType\(\{ x: FloatType, y: FloatType, "odd-name": IntegerType \}\)\)\);/);
398
+ assert.match(source, /const deep = \$\.const\(some\(\{ x: 1, y: 2, "odd-name": n \}\), OptionType\(StructType\(\{ x: FloatType, y: FloatType, "odd-name": IntegerType \}\)\)\);/);
399
+ assert.doesNotMatch(source, /\$\.const\([^\n]*East\.value\(/, "no East.value inside a bound literal");
400
+ // The module imports exactly what it uses: the literal helpers and type constructors this program spells, nothing else.
401
+ assert.match(source, /^import \{ East, variant, some, none, ref, NullType, IntegerType, FloatType, StringType, ArrayType, SetType, DictType, StructType, VariantType, OptionType, RefType, VectorType, RecursiveType \} from /m);
402
+ assert.doesNotMatch(source, /^const _t\d+ = (?:OptionType|ArrayType|DictType)\(/m);
403
+ assert.match(source, /^const _t\d+ = RecursiveType\(self => /m); // a recursive type is always hoisted
404
+ assert.match(source, /const nil = \$\.const\(East\.wrapRecursive\(variant\("nil", null\), _t\d+\)\);/); // the wrapper's inner type governs the wrapped literal
405
+ assert.match(source, /const list = \$\.const\(East\.wrapRecursive\(variant\("cons", \{ head: n, tail: nil \}\), _t\d+\)\);/);
406
+ const main = await roundTrip(fn, "values");
407
+ assert.equal(main.toIR().compile([])(5n), fn.toIR().compile([])(5n));
408
+ });
409
+ test("value slots and returns: a construction prints bare where the surface types it, through East.value only where the type would be lost (#645, #646)", async () => {
410
+ const Point = StructType({ x: IntegerType, y: IntegerType });
411
+ const Wide = VariantType({ a: IntegerType, b: StringType });
412
+ const fn = East.function([ArrayType(IntegerType), DictType(StringType, IntegerType)], IntegerType, ($, xs, d) => {
413
+ const more = $.const(xs.concat([7n, 8n])); // a typed slot: bare
414
+ $(d.insert("k", 1n));
415
+ const points = $.const(xs.map(($, x) => East.value({ x, y: x }, Point))); // a callback typing itself: bare, parenthesised
416
+ const opts = $.const(xs.map(($, x) => East.value(some(x), OptionType(IntegerType)))); // `some(x)` alone is a one-case variant: East.value stays
417
+ const nothing = $.const(xs.map(($, _x) => East.value(none, OptionType(IntegerType)))); // `none` needs its type: East.value stays
418
+ const empty = $.const(xs.reduce(($, acc, _x) => acc, East.value([], ArrayType(IntegerType)))); // an inferred slot: an empty array keeps its type
419
+ const total = $.const(xs.reduce(($, acc, x) => acc.add(x), 0n));
420
+ const first = $.const(opts.get(0n).unwrap()); // the match `unwrap` lowers to
421
+ const w = $.const(variant("a", 1n), Wide);
422
+ const ua = $.const(w.unwrap("a"));
423
+ const em = $.const(new Map(), DictType(StringType, IntegerType));
424
+ const es = $.const(new Set([]), SetType(IntegerType));
425
+ $.return(points.size().add(more.size()).add(nothing.size()).add(empty.size()).add(total).add(first).add(ua).add(em.size()).add(es.size()).add(d.get("k")));
426
+ });
427
+ const source = toSource(fn, { importFrom: INDEX_URL, width: Infinity });
428
+ assert.match(source, /xs\.concat\(\[7n, 8n\]\)/, source);
429
+ assert.match(source, /d\.insert\("k", 1n\)/, source);
430
+ assert.match(source, /xs\.map\(\(\$, x, _\d+\) => \(\{ x: x, y: x \}\)\)/, source);
431
+ assert.match(source, /xs\.map\(\(\$, x, _\d+\) => East\.value\(some\(x\), OptionType\(IntegerType\)\)\)/, source);
432
+ assert.match(source, /=> East\.value\(none, OptionType\(IntegerType\)\)\)/, source);
433
+ assert.match(source, /East\.value\(\[\], ArrayType\(IntegerType\)\)\)/, source);
434
+ assert.match(source, /xs\.reduce\(\(\$, acc, x, _\d+\) => acc\.add\(x\), 0n\)/, source);
435
+ assert.match(source, /opts\.get\(0n\)\.unwrap\(\)/, source);
436
+ assert.match(source, /w\.unwrap\("a"\)/, source);
437
+ assert.doesNotMatch(source, /Variant does not have case/, source);
438
+ // an empty map is `new Map()` (`new Map([])` is a `Map<unknown, unknown>` to the compiler); an empty set stays `new Set([])` (a `Set<never>`; `new Set()` is a `Set<unknown>`)
439
+ assert.match(source, /const em = \$\.const\(new Map\(\), DictType\(StringType, IntegerType\)\);/, source);
440
+ assert.match(source, /const es = \$\.const\(new Set\(\[\]\), SetType\(IntegerType\)\);/, source);
441
+ assert.doesNotMatch(source, /new Map\(\[\]\)/, source);
442
+ const main = await roundTrip(fn, "value slots");
443
+ const args = () => [[1n, 2n], new Map([["z", 9n]])]; // the body inserts into the map: fresh per run
444
+ assert.equal(main.toIR().compile([])(...args()), fn.toIR().compile([])(...args()));
445
+ });
446
+ test("layout: a callback returning a struct hugs the `=>` and breaks inside, as prettier prints `=> ({`", async () => {
447
+ const Row = StructType({ alphabetical: IntegerType, betamax: IntegerType, gamma_ray: IntegerType, delta_wing: IntegerType, epsilon_naught: IntegerType, zeta_function: IntegerType });
448
+ const fn = East.function([ArrayType(IntegerType)], IntegerType, ($, xs) => {
449
+ const rows = $.const(xs.map(($, x) => East.value({ alphabetical: x, betamax: x, gamma_ray: x, delta_wing: x, epsilon_naught: x, zeta_function: x }, Row)));
450
+ return rows.size();
451
+ });
452
+ const source = toSource(fn, { importFrom: INDEX_URL });
453
+ assert.ok(source.includes([
454
+ " const rows = $.const(",
455
+ " xs.map(($, x, _2) => ({",
456
+ " alphabetical: x,",
457
+ " betamax: x,",
458
+ " gamma_ray: x,",
459
+ " delta_wing: x,",
460
+ " epsilon_naught: x,",
461
+ " zeta_function: x,",
462
+ " })),",
463
+ " );",
464
+ ].join("\n")), source);
465
+ await roundTrip(fn, "layout struct return");
466
+ });
467
+ test("platform calls, generic platform calls and async functions", async () => {
468
+ const log = East.platform("log", [StringType], NullType);
469
+ const fetchAs = East.asyncGenericPlatform("fetchAs", ["T"], [StringType], "T");
470
+ const sync = East.function([StringType], NullType, ($, s) => {
471
+ $(log(s));
472
+ $(log(East.str `again ${s}`));
473
+ });
474
+ await roundTrip(sync, "platform");
475
+ const fn = East.asyncFunction([StringType], IntegerType, ($, url) => {
476
+ const n = $.const(fetchAs([IntegerType], url));
477
+ return n.add(1n);
478
+ });
479
+ await roundTrip(fn, "async platform");
480
+ });
481
+ test("a declaration handle carries its identity: name, inputs, output, asyncness, type parameters", () => {
482
+ const log = East.platform("my.log", [StringType], NullType);
483
+ const fetchAs = East.asyncGenericPlatform("fetchAs", ["T"], [StringType], "T", { optional: true });
484
+ assert.ok(isPlatformDeclaration(log));
485
+ assert.ok(isPlatformDeclaration(fetchAs));
486
+ assert.ok(!isPlatformDeclaration(() => 1));
487
+ assert.ok(!isPlatformDeclaration({ name: "x", inputs: [], output: NullType }));
488
+ assert.equal(log.name, "my.log");
489
+ assert.deepEqual(log.inputs, [StringType]);
490
+ assert.equal(log.output, NullType);
491
+ assert.equal(log.async, false);
492
+ assert.equal(log.optional, false);
493
+ assert.equal(fetchAs.name, "fetchAs");
494
+ assert.deepEqual(fetchAs.typeParameters, ["T"]);
495
+ assert.equal(fetchAs.async, true);
496
+ assert.equal(fetchAs.optional, true);
497
+ // the identity is the handle's, not the closure's: a declaration still emits its node
498
+ const fn = East.function([StringType], NullType, ($, s) => { $(log(s)); });
499
+ assert.ok(JSON.stringify(fn.toIR().ir, (_k, v) => typeof v === "bigint" ? String(v) : v).includes("my.log"));
500
+ });
501
+ test("a hoisted declaration is named after the platform function; a second signature takes a suffix; a body variable of that name is renamed", async () => {
502
+ const log = East.platform("my.log", [StringType], NullType);
503
+ const logCount = East.platform("my.log", [IntegerType], NullType); // the same name, another signature
504
+ const fn = East.function([StringType], IntegerType, ($, s) => {
505
+ const my_log = $.const(1n); // the platform's identifier: the variable moves, the declaration keeps its name
506
+ $(log(s));
507
+ $(logCount(my_log));
508
+ return my_log;
509
+ });
510
+ const source = toSource(fn, { importFrom: INDEX_URL, width: Infinity });
511
+ assert.match(source, /^const my_log = East\.platform\("my\.log", \[StringType\], NullType\);$/m, source);
512
+ assert.match(source, /^const my_log_2 = East\.platform\("my\.log", \[IntegerType\], NullType\);$/m, source);
513
+ assert.doesNotMatch(source, /_p0/, source);
514
+ assert.match(source, /const v_0 = \$\.const\(1n\);/, source);
515
+ assert.match(source, /\$\(my_log\(s\)\);/, source);
516
+ assert.match(source, /\$\(my_log_2\(v_0\)\);/, source);
517
+ const main = await roundTrip(fn, "named declarations");
518
+ assert.equal(toSource(main.toIR().ir, { importFrom: INDEX_URL, width: Infinity }), source, "print → build → print is the identity");
519
+ });
520
+ test("the widening the builder inserts at a platform argument prints as the expression, and rebuilds", async () => {
521
+ const gz = East.asyncPlatform("gz", [StructType({ level: OptionType(IntegerType) })], IntegerType);
522
+ const fn = East.asyncFunction([], IntegerType, ($) => {
523
+ const options = $.let({ level: variant("some", 6n) }); // a one-case variant: the call widens it to the Option
524
+ const inline = $.const(gz(East.as({ level: some(7n) }, StructType({ level: OptionType(IntegerType) })))); // a widened construction keeps its As
525
+ return gz(options).add(inline);
526
+ });
527
+ const source = toSource(fn, { importFrom: INDEX_URL, width: Infinity });
528
+ assert.match(source, /return gz\(options\)\.add\(inline\);/, source);
529
+ assert.doesNotMatch(source, /East\.as\(options/, source);
530
+ assert.match(source, /gz\(\{ level: some\(7n\) \}\)/, source);
531
+ const main = await roundTrip(fn, "platform widening");
532
+ assert.equal(toSource(main.toIR().ir, { importFrom: INDEX_URL, width: Infinity }), source, "print → build → print is the identity");
533
+ });
534
+ test("a platform call spells as the library's own export when the library's module is given, and the import is added", async () => {
535
+ // a library: the declarations exported flat under their registered names and grouped, as east-node-io exports them
536
+ const libPath = join(TMP, "lib.mjs");
537
+ writeFileSync(libPath, [
538
+ `import { East, StringType, BlobType, ArrayType } from ${JSON.stringify(INDEX_URL)};`,
539
+ `export const tar_create = East.asyncPlatform("tar_create", [ArrayType(StringType)], BlobType);`,
540
+ `export const tar_extract = East.asyncPlatform("tar_extract", [BlobType], StringType);`,
541
+ `export const flat_only = East.platform("flat_only", [StringType], StringType);`,
542
+ `export const Compression = { Tar: { create: tar_create, extract: tar_extract, Implementation: [] } };`,
543
+ "",
544
+ ].join("\n"), "utf-8");
545
+ const specifier = pathToFileURL(libPath).href;
546
+ const lib = await import(specifier);
547
+ const other = East.asyncPlatform("tar_create", [StringType], BlobType); // the same name, a signature the library does not declare
548
+ const optionalTwin = East.platform("flat_only", [StringType], StringType, { optional: true }); // the library's signature, but optional: not the library's handle
549
+ const fn = East.asyncFunction([ArrayType(StringType), StringType], StringType, ($, entries, s) => {
550
+ const blob = $.const(lib.Compression.Tar.create(entries));
551
+ const again = $.const(other(s));
552
+ const text = $.const(lib.tar_extract(blob));
553
+ const twice = $.const(optionalTwin(text)); // `text` is untyped here: the library came through a dynamic import
554
+ return lib.flat_only(text).concat(East.print(again.size())).concat(twice);
555
+ });
556
+ const libraries = { [specifier]: lib };
557
+ const source = toSource(fn, { importFrom: INDEX_URL, width: Infinity, libraries });
558
+ // the grouped path is the spelling; the flat-only export is spelled flat; both come from one import line
559
+ assert.match(source, new RegExp(`^import \\{ Compression, flat_only \\} from ${JSON.stringify(specifier).replace(/[.*+?^$()|[\]\\]/g, "\\$&")};$`, "m"), source);
560
+ assert.match(source, /const blob = \$\.const\(Compression\.Tar\.create\(entries\)\);/, source);
561
+ assert.match(source, /const text = \$\.const\(Compression\.Tar\.extract\(blob\)\);/, source);
562
+ assert.match(source, /return flat_only\(text\)\.concat/, source);
563
+ // a call whose signature no library declares keeps its hoisted declaration — named, and not shadowing the library's name
564
+ assert.match(source, /^const tar_create = East\.asyncPlatform\("tar_create", \[StringType\], BlobType\);$/m, source);
565
+ assert.match(source, /const again = \$\.const\(tar_create\(s\)\);/, source);
566
+ assert.doesNotMatch(source, /East\.asyncPlatform\("tar_extract"/, source);
567
+ // an optional declaration of the library's signature is not the library's handle: its own declaration, named past the import root
568
+ assert.match(source, /^const flat_only_2 = East\.platform\("flat_only", \[StringType\], StringType, \{ optional: true \}\);$/m, source);
569
+ assert.match(source, /const twice = \$\.const\(flat_only_2\(text\)\);/, source);
570
+ // without the library, every call is a hoisted declaration
571
+ const bare = toSource(fn, { importFrom: INDEX_URL, width: Infinity });
572
+ assert.doesNotMatch(bare, /Compression/, bare);
573
+ assert.match(bare, /^const tar_extract = East\.asyncPlatform\("tar_extract"/m, bare);
574
+ const main = await roundTrip(fn, "library spellings", { libraries });
575
+ assert.equal(toSource(main.toIR().ir, { importFrom: INDEX_URL, width: Infinity, libraries }), source, "print → build → print is the identity");
576
+ });
577
+ test("a platform function named after the module's export takes a suffix, so the module still exports its function", async () => {
578
+ const mainFn = East.platform("main", [IntegerType], IntegerType);
579
+ const fn = East.function([IntegerType], IntegerType, ($, n) => mainFn(n).add(1n));
580
+ const source = toSource(fn, { importFrom: INDEX_URL, width: Infinity });
581
+ assert.match(source, /^const main_2 = East\.platform\("main", \[IntegerType\], IntegerType\);$/m, source);
582
+ assert.match(source, /^export const main = East\.function\(\[IntegerType\], IntegerType, \(\$, n\) => main_2\(n\)\.add\(1n\)\);$/m, source);
583
+ const main = await roundTrip(fn, "export-named platform");
584
+ assert.equal(toSource(main.toIR().ir, { importFrom: INDEX_URL, width: Infinity }), source, "print → build → print is the identity");
585
+ });
586
+ test("regex and csv arguments print as the host values the surface takes", async () => {
587
+ const fn = East.function([StringType, ArrayType(StructType({ a: IntegerType }))], StringType, ($, s, rows) => {
588
+ const hit = $.const(s.contains(/a+b/i));
589
+ const idx = $.const(s.indexOf(/x/));
590
+ const rep = $.const(s.replace(/o/, "0"));
591
+ const csv = $.const(rows.encodeCsv({ delimiter: ";", includeHeader: false }));
592
+ const back = $.const(csv.decodeCsv(StructType({ a: IntegerType }), { delimiter: ";", hasHeader: false }));
593
+ return East.str `${hit} ${idx} ${rep} ${back.size()}`;
594
+ });
595
+ const main = await roundTrip(fn, "regex+csv");
596
+ assert.equal(main.toIR().compile([])("aab foo", [{ a: 1n }]), fn.toIR().compile([])("aab foo", [{ a: 1n }]));
597
+ });
598
+ test("another builder's fresh names print as one v_N sequence, and the printed module prints to itself", async () => {
599
+ const fn = East.function([ArrayType(IntegerType)], IntegerType, ($, xs) => xs.map(($, x) => x.multiply(2n)).reduce(($, acc, x) => acc.add(x), 0n));
600
+ // python spells a slot the body did not name `__nN` (TypeScript: `_N`),
601
+ // and an author may have used the printer's own `v_N` spelling.
602
+ const foreign = (v) => Array.isArray(v) ? v.map(foreign)
603
+ : isVariant(v) ? variant(v.type, foreign(v.value))
604
+ : v !== null && typeof v === "object" && !(v instanceof Date) && !(v instanceof Uint8Array)
605
+ ? Object.fromEntries(Object.entries(v).map(([k, x]) => [k, k === "name" && typeof x === "string" ? (/^_\d+$/.test(x) ? `__n${x.slice(1)}` : x === "acc" ? "v_0" : x) : foreign(x)]))
606
+ : v;
607
+ const ir = foreign(fn.toIR().ir);
608
+ const wide = toSource(ir, { importFrom: INDEX_URL, width: Infinity });
609
+ assert.match(wide, /xs\.map\(\(\$, x, v_1\) => x\.multiply\(2n\)\)\.reduce\(\(\$, v_0, x, v_2\) => v_0\.add\(x\), 0n\)/);
610
+ const main = await roundTrip(ir, "foreign names");
611
+ assert.equal(toSource(main.toIR().ir, { importFrom: INDEX_URL, width: Infinity }), wide, "print → build → print is the identity");
612
+ assert.equal(toSource(main.toIR().ir, { importFrom: INDEX_URL }), toSource(ir, { importFrom: INDEX_URL }), "… at the default width too");
613
+ });
614
+ test("layout: a wide literal, its type and an argument list break one entry per line, as prettier lays them out", async () => {
615
+ const Ops = StructType({ add: FunctionType([IntegerType, IntegerType], IntegerType), multiply: FunctionType([IntegerType, IntegerType], IntegerType) });
616
+ const fn = East.function([], IntegerType, ($) => {
617
+ const mathOps = $.const({ add: East.function([IntegerType, IntegerType], IntegerType, ($, a, b) => a.add(b)), multiply: East.function([IntegerType, IntegerType], IntegerType, ($, a, b) => a.multiply(b)) }, Ops);
618
+ return mathOps.add(mathOps.multiply(2n, 3n), 4n);
619
+ });
620
+ const source = toSource(fn, { importFrom: INDEX_URL });
621
+ assert.ok(source.includes([
622
+ "export const main = East.function([], IntegerType, ($) => {",
623
+ " const mathOps = $.const(",
624
+ " {",
625
+ " add: East.function([IntegerType, IntegerType], IntegerType, ($, a, b) => a.add(b)),",
626
+ " multiply: East.function([IntegerType, IntegerType], IntegerType, ($, a, b) => a.multiply(b)),",
627
+ " },",
628
+ " StructType({",
629
+ " add: FunctionType([IntegerType, IntegerType], IntegerType),",
630
+ " multiply: FunctionType([IntegerType, IntegerType], IntegerType),",
631
+ " }),",
632
+ " );",
633
+ " return mathOps.add(mathOps.multiply(2n, 3n), 4n);",
634
+ "});",
635
+ ].join("\n")), source);
636
+ assert.doesNotMatch(source, /^const _t/m, "no type is hoisted for its width");
637
+ const main = await roundTrip(fn, "layout");
638
+ assert.equal(main.toIR().compile([])(), 10n);
639
+ });
640
+ test("layout: a call hugs a trailing block, a chain of three or more calls breaks one call per line, and a block inside a plain argument breaks the call out", async () => {
641
+ const fn = East.function([ArrayType(IntegerType), StringType], StringType, ($, xs, label) => {
642
+ const doubled = $.const(xs.map(($, x) => x.multiply(2n)));
643
+ const big = $.const(East.less(doubled.size(), 10n).ifElse($ => "small", $ => Expr.block($ => {
644
+ const s = $.const(East.value("big").concat("!"));
645
+ return s;
646
+ })));
647
+ $.for(xs, ($, x, i, loop) => {
648
+ $.if(East.equal(x, 0n), $ => { $.continue(loop); });
649
+ });
650
+ return label.concat(" ").concat(big).concat(" ").concat(East.print(doubled.size())).concat(" ").concat(East.print(xs.size()));
651
+ });
652
+ const source = toSource(fn, { importFrom: INDEX_URL });
653
+ const has = (text) => assert.ok(source.includes(text), `expected:\n${text}\nin:\n${source}`);
654
+ has(" const doubled = $.const(xs.map(($, x, _3) => x.multiply(2n)));"); // fits: one line
655
+ has([
656
+ " const big = $.const(", // a block inside a plain argument: the call breaks out …
657
+ " East.less(doubled.size(), 10n).ifElse(", // … and two trailing callbacks hug neither: one arm per line
658
+ " ($) => \"small\",",
659
+ " ($) => {",
660
+ " const s = $.const(East.value(\"big\").concat(\"!\"));",
661
+ " return s;",
662
+ " },",
663
+ " ),",
664
+ " );",
665
+ ].join("\n"));
666
+ has([
667
+ " $.for(xs, ($, x, i, label_1) => {", // a statement hugs its body
668
+ " $.if(East.equal(x, 0n), ($) => {",
669
+ " $.continue(label_1);",
670
+ " });",
671
+ " });",
672
+ ].join("\n"));
673
+ has([
674
+ " return label", // three or more calls that do not fit: one per line
675
+ " .concat(\" \")",
676
+ " .concat(big)",
677
+ " .concat(\" \")",
678
+ " .concat(East.print(doubled.size()))",
679
+ " .concat(\" \")",
680
+ " .concat(East.print(xs.size()));",
681
+ ].join("\n"));
682
+ has([
683
+ "export const main = East.function(", // a head that does not fit: every argument on its own line
684
+ " [ArrayType(IntegerType), StringType],",
685
+ " StringType,",
686
+ " ($, xs, label) => {",
687
+ ].join("\n"));
688
+ assert.ok(source.split("\n").every(l => l.length <= 100 || l.startsWith("import ")), `a line passes 100 columns:\n${source}`);
689
+ const main = await roundTrip(fn, "layout hug+chain");
690
+ assert.equal(main.toIR().compile([])([1n, 2n, 3n], "n"), fn.toIR().compile([])([1n, 2n, 3n], "n"));
691
+ });
692
+ test("a raw builtin prints through East.builtin and rebuilds", async () => {
693
+ const fn = East.function([ArrayType(IntegerType)], ArrayType(IntegerType), ($, xs) => {
694
+ return xs.getKeys([0n, 1n]);
695
+ });
696
+ const source = toSource(fn, { importFrom: INDEX_URL });
697
+ assert.match(source, /East\.builtin\(\s*"ArrayGetKeys"/);
698
+ await roundTrip(fn, "raw builtin");
699
+ });
700
+ });
701
+ // ── the exported corpora ────────────────────────────────────────────────────
702
+ const EXAMPLES_DIR = process.env["EAST_EXAMPLES_IR_DIR"] ?? "/tmp/east-examples-ir";
703
+ const CORPUS_DIR = process.env["EAST_TEST_IR_DIR"] ?? "/tmp/east-test-ir";
704
+ const REQUIRED = process.env["EAST_CONFORMANCE_REQUIRED"] === "1";
705
+ const decodeIR = fromJSONFor(IRType);
706
+ const decodeType = fromJSONFor(EastTypeType);
707
+ function hasPlatform(node) {
708
+ if (node === null || typeof node !== "object")
709
+ return false;
710
+ if (isVariant(node) && node.type === "Platform")
711
+ return true;
712
+ if (isVariant(node))
713
+ return hasPlatform(node.value);
714
+ if (Array.isArray(node))
715
+ return node.some(hasPlatform);
716
+ return Object.keys(node).some(k => k !== "type" && k !== "type_parameters" && hasPlatform(node[k]));
717
+ }
718
+ describe("codegen: every exported example round trips and runs", () => {
719
+ const present = existsSync(EXAMPLES_DIR);
720
+ if (REQUIRED && !present)
721
+ throw new Error(`EAST_CONFORMANCE_REQUIRED=1 but no exported examples in ${EXAMPLES_DIR}`);
722
+ const files = present
723
+ ? readdirSync(EXAMPLES_DIR).flatMap(suite => readdirSync(join(EXAMPLES_DIR, suite)).filter(f => f.endsWith(".json")).map(f => join(EXAMPLES_DIR, suite, f)))
724
+ : [];
725
+ const rawSeen = new Set();
726
+ test(`round trip (${files.length} examples)`, { skip: !present && `no exported examples in ${EXAMPLES_DIR}` }, async () => {
727
+ const labelOf = (file) => `${file.split("/").at(-2)}/${file.split("/").at(-1).slice(0, -5)}`;
728
+ await sweep(files, labelOf, async (file) => {
729
+ const record = JSON.parse(readFileSync(file, "utf-8"));
730
+ const ir = decodeIR(record.ir);
731
+ const label = labelOf(file);
732
+ const source = toSource(ir, { importFrom: INDEX_URL });
733
+ for (const m of source.matchAll(/East\.builtin\("(\w+)"/g))
734
+ rawSeen.add(m[1]);
735
+ const main = await roundTrip(ir, label);
736
+ if (record.async || record.returns === null || record.inputs.some((v) => v === null) || hasPlatform(ir))
737
+ return;
738
+ const inputTypes = record.input_types.map((t) => decodeType(t));
739
+ const outputType = decodeType(record.output_type);
740
+ const inputs = record.inputs.map((v, i) => fromJSONFor(inputTypes[i])(v));
741
+ const expected = fromJSONFor(outputType)(record.returns);
742
+ const got = main.toIR().compile([])(...inputs);
743
+ assert.ok(equalFor(outputType)(got, expected), `${label}: computed ${show(got)}, the example declares ${show(expected)}`);
744
+ });
745
+ // the raw ratchet: the printer may only ever spell MORE
746
+ for (const name of rawSeen)
747
+ assert.ok(RAW_ONLY.has(name), `${name} printed raw but is not in RAW_ONLY`);
748
+ });
749
+ });
750
+ describe("codegen: every compliance-corpus program round trips", () => {
751
+ const present = existsSync(CORPUS_DIR);
752
+ if (REQUIRED && !present)
753
+ throw new Error(`EAST_CONFORMANCE_REQUIRED=1 but no exported IR corpus in ${CORPUS_DIR}`);
754
+ const files = present ? readdirSync(CORPUS_DIR).filter(f => f.endsWith(".json")).sort() : [];
755
+ test(`round trip (${files.length} programs)`, { skip: !present && `no exported IR corpus in ${CORPUS_DIR}` }, async () => {
756
+ const labelOf = (file) => `corpus/${file.slice(0, -5)}`;
757
+ await sweep(files, labelOf, async (file) => {
758
+ const record = JSON.parse(readFileSync(join(CORPUS_DIR, file), "utf-8"));
759
+ await roundTrip(decodeIR(record.ir), labelOf(file));
760
+ });
761
+ });
762
+ });
763
+ //# sourceMappingURL=codegen.spec.js.map