@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.
- package/dist/src/ast.d.ts +2 -0
- package/dist/src/ast.d.ts.map +1 -1
- package/dist/src/ast_to_ir.d.ts +7 -3
- package/dist/src/ast_to_ir.d.ts.map +1 -1
- package/dist/src/ast_to_ir.js +62 -15
- package/dist/src/ast_to_ir.js.map +1 -1
- package/dist/src/codegen/codegen.spec.d.ts +6 -0
- package/dist/src/codegen/codegen.spec.d.ts.map +1 -0
- package/dist/src/codegen/codegen.spec.js +763 -0
- package/dist/src/codegen/codegen.spec.js.map +1 -0
- package/dist/src/codegen/doc.d.ts +111 -0
- package/dist/src/codegen/doc.d.ts.map +1 -0
- package/dist/src/codegen/doc.js +350 -0
- package/dist/src/codegen/doc.js.map +1 -0
- package/dist/src/codegen/doc.spec.d.ts +6 -0
- package/dist/src/codegen/doc.spec.d.ts.map +1 -0
- package/dist/src/codegen/doc.spec.js +86 -0
- package/dist/src/codegen/doc.spec.js.map +1 -0
- package/dist/src/codegen/index.d.ts +15 -0
- package/dist/src/codegen/index.d.ts.map +1 -0
- package/dist/src/codegen/index.js +15 -0
- package/dist/src/codegen/index.js.map +1 -0
- package/dist/src/codegen/printer.d.ts +63 -0
- package/dist/src/codegen/printer.d.ts.map +1 -0
- package/dist/src/codegen/printer.js +1529 -0
- package/dist/src/codegen/printer.js.map +1 -0
- package/dist/src/codegen/spellings.d.ts +73 -0
- package/dist/src/codegen/spellings.d.ts.map +1 -0
- package/dist/src/codegen/spellings.js +298 -0
- package/dist/src/codegen/spellings.js.map +1 -0
- package/dist/src/codegen/spellings.spec.d.ts +6 -0
- package/dist/src/codegen/spellings.spec.d.ts.map +1 -0
- package/dist/src/codegen/spellings.spec.js +191 -0
- package/dist/src/codegen/spellings.spec.js.map +1 -0
- package/dist/src/codegen/types.d.ts +67 -0
- package/dist/src/codegen/types.d.ts.map +1 -0
- package/dist/src/codegen/types.js +173 -0
- package/dist/src/codegen/types.js.map +1 -0
- package/dist/src/compile.js +10 -10
- package/dist/src/compile.js.map +1 -1
- package/dist/src/expr/block.d.ts +148 -0
- package/dist/src/expr/block.d.ts.map +1 -1
- package/dist/src/expr/block.js +168 -4
- package/dist/src/expr/block.js.map +1 -1
- package/dist/src/expr/index.d.ts +155 -2
- package/dist/src/expr/index.d.ts.map +1 -1
- package/dist/src/expr/index.js +155 -2
- package/dist/src/expr/index.js.map +1 -1
- package/dist/src/functions.crossimport.spec.d.ts +6 -0
- package/dist/src/functions.crossimport.spec.d.ts.map +1 -0
- package/dist/src/functions.crossimport.spec.js +45 -0
- package/dist/src/functions.crossimport.spec.js.map +1 -0
- package/dist/src/functions.d.ts +4103 -0
- package/dist/src/functions.d.ts.map +1 -0
- package/dist/src/functions.js +443 -0
- package/dist/src/functions.js.map +1 -0
- package/dist/src/functions.spec.d.ts +6 -0
- package/dist/src/functions.spec.d.ts.map +1 -0
- package/dist/src/functions.spec.js +123 -0
- package/dist/src/functions.spec.js.map +1 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/naming.d.ts +50 -0
- package/dist/src/naming.d.ts.map +1 -0
- package/dist/src/naming.js +194 -0
- package/dist/src/naming.js.map +1 -0
- package/dist/src/naming.spec.d.ts +6 -0
- package/dist/src/naming.spec.d.ts.map +1 -0
- package/dist/src/naming.spec.js +154 -0
- package/dist/src/naming.spec.js.map +1 -0
- package/dist/src/serialization/beast2/index.spec.js +25 -0
- package/dist/src/serialization/beast2/index.spec.js.map +1 -1
- package/dist/src/serialization/beast2/shared.d.ts +1 -1
- package/dist/src/serialization/beast2/shared.d.ts.map +1 -1
- package/dist/src/serialization/beast2/shared.js +9 -1
- package/dist/src/serialization/beast2/shared.js.map +1 -1
- package/package.json +11 -2
|
@@ -0,0 +1,1529 @@
|
|
|
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 printer (#628): `toSource`.
|
|
7
|
+
*
|
|
8
|
+
* The printer walks an IR value and writes a module whose `East.function(...)`
|
|
9
|
+
* rebuilds the same IR. Its spellings are the builder surface's — the `$`
|
|
10
|
+
* a body receives and the expression methods — and the builtin table's
|
|
11
|
+
* ({@link SPELLINGS}):
|
|
12
|
+
*
|
|
13
|
+
* - a Function / AsyncFunction is `East.function([types], out, ($, params)
|
|
14
|
+
* => { ... })`; nested functions are expressions of the enclosing body;
|
|
15
|
+
* - a Block is that body's statements; the block's last node is `return
|
|
16
|
+
* <expr>;` when it is an expression, or the statement it is — except in
|
|
17
|
+
* a body whose type the builder infers from what the arrow returns (a
|
|
18
|
+
* callback, an `Expr.block`, an expression arm), where a last statement
|
|
19
|
+
* is `return $.assign(...)`: the `$.` forms evaluate to the statement
|
|
20
|
+
* they pushed, which the builder then does not push twice;
|
|
21
|
+
* - Let / Assign / Return / Break / Continue / Error, and a Null-typed
|
|
22
|
+
* IfElse / Match / While / For / TryCatch, print as `$.const` / `$.let` /
|
|
23
|
+
* `$.assign` / `$.return` / `$.break` / `$.continue` / `$.error` /
|
|
24
|
+
* `$.if(...).elseIf(...).else(...)` / `$.match` / `$.while` / `$.for` /
|
|
25
|
+
* `$.try(...).catch(...).finally(...)`, whose bodies are `($, ...) => {
|
|
26
|
+
* ... }` arrows (every body takes the block first);
|
|
27
|
+
* - every other node prints as an expression: literals as TypeScript
|
|
28
|
+
* literals (`1n`, `1.5`, `"s"`, `new Date(...)`, `new Uint8Array([...])`),
|
|
29
|
+
* Struct / Variant / NewArray / NewSet / NewDict / NewRef / NewVector /
|
|
30
|
+
* NewMatrix as the host literal — printed by `literalFor(T)`, a factory
|
|
31
|
+
* over the type (as `compareFor` is) under which a construction nested
|
|
32
|
+
* anywhere prints bare, an Option case as `some(v)` / `none` — bare
|
|
33
|
+
* wherever the surface types the position (a binding, `$.let(new
|
|
34
|
+
* Map([...]), T)`; a method's value slot, `xs.concat([1n, 2n])`; a call
|
|
35
|
+
* argument; a declared return) or the literal types itself (a callback
|
|
36
|
+
* returning `{ a: x }`), and through `East.value(..., T)` only where the
|
|
37
|
+
* type would otherwise be lost (a callback returning `none`, an empty
|
|
38
|
+
* collection or a general `variant`) — an expression IfElse / Match /
|
|
39
|
+
* TryCatch / Block through `.ifElse(...)` / `.match({...})` /
|
|
40
|
+
* `Expr.tryCatch(...)` / `Expr.block(...)`, the match `unwrap` lowers to
|
|
41
|
+
* as `.unwrap()` / `.unwrap("case")`, a Builtin through its spelling
|
|
42
|
+
* row (callbacks as `($, ...) => ...` arrows) or the raw
|
|
43
|
+
* `East.builtin(name, [T...], [args], out)`, an As through `East.as`, a
|
|
44
|
+
* WrapRecursive through `East.wrapRecursive`, an unresolved cross-language
|
|
45
|
+
* import (the `east.importFunction` Platform node) through
|
|
46
|
+
* `East.importFunction(pkg, name, T)`.
|
|
47
|
+
*
|
|
48
|
+
* Variables keep their IR names when they are JavaScript identifiers — the
|
|
49
|
+
* authoring names both builders carry (#639) and TypeScript's `_N` for a
|
|
50
|
+
* slot the body did not name; python's `__nN` and a name the scope already
|
|
51
|
+
* uses are renamed `v_N`, numbered once per module (above any `v_N` the IR
|
|
52
|
+
* holds) so a printed module rebuilds to itself; `$` is reserved for the
|
|
53
|
+
* block. Every type prints inline where it is used (`$.let(new Map([...]),
|
|
54
|
+
* DictType(IntegerType, StringType))`, as an author writes it); a recursive
|
|
55
|
+
* type is hoisted to a module constant `_tN` (deduplicated structurally).
|
|
56
|
+
* A platform call spells as the library that declares it spells it when the
|
|
57
|
+
* library's module is given (`libraries`: `Compression.Tar.create(entries)`,
|
|
58
|
+
* imported from the library) — found by walking the module's exports for
|
|
59
|
+
* the declaration handle of that name — and otherwise hoists to a
|
|
60
|
+
* module-level declaration named after the platform function
|
|
61
|
+
* (`const tar_create = East.asyncPlatform("tar_create", …)`; a second
|
|
62
|
+
* signature under one name takes a `_2` suffix), one per distinct
|
|
63
|
+
* signature. A closure-free function called where it stands — an `East.function`
|
|
64
|
+
* artifact the compiler inlined at its call, `(East.function(...))(x)` — is
|
|
65
|
+
* hoisted to a module constant `_fN` (one per distinct function) and called
|
|
66
|
+
* by name, as the source called it. Deep expression nesting is broken with
|
|
67
|
+
* `const _eN = <expr>` temporaries, so any IR width or depth prints to
|
|
68
|
+
* parseable source.
|
|
69
|
+
*
|
|
70
|
+
* The source is written as a layout document ({@link render}) and laid out
|
|
71
|
+
* as prettier lays TypeScript out: a literal, an argument list or a type
|
|
72
|
+
* breaks one entry per line when the line it sits on would pass
|
|
73
|
+
* {@link LINE_WIDTH}, a call hugs a trailing callback (`xs.map(($, x) => {`
|
|
74
|
+
* stays on its line, the body breaks inside), and a chain of three or more
|
|
75
|
+
* calls that does not fit prints one call per line.
|
|
76
|
+
*
|
|
77
|
+
* The contract, shared with the python printer (`east/codegen/printer.py`):
|
|
78
|
+
* `build(print(IR)) ≡ IR` after normalization (`east-c ir normalize`) —
|
|
79
|
+
* total or loud (every node kind prints or {@link Unprintable} names it),
|
|
80
|
+
* idiomatic, deterministic.
|
|
81
|
+
*/
|
|
82
|
+
import { Expr } from "../expr/expr.js";
|
|
83
|
+
import { isPlatformDeclaration } from "../expr/block.js";
|
|
84
|
+
import { toEastTypeValue } from "../type_of_type.js";
|
|
85
|
+
import { IMPORT_PLATFORM } from "../functions.js";
|
|
86
|
+
import { spellingFor } from "./spellings.js";
|
|
87
|
+
import { TYPE_IMPORTS, isOptionValue, objectKey, typeConstructors, typeDoc, typeKey } from "./types.js";
|
|
88
|
+
import { LINE_WIDTH, bracket, callArgs, choice, fn, group, hardline, hug, ifBreak, indent, isHuggable, join, line, render, softline, willBreak, } from "./doc.js";
|
|
89
|
+
/**
|
|
90
|
+
* Thrown for an IR shape the TypeScript surface cannot spell.
|
|
91
|
+
*
|
|
92
|
+
* The message names the node kind (and builtin) and where it sits — a
|
|
93
|
+
* statement in expression position, a jump to no loop, a `NewVector` of
|
|
94
|
+
* expressions, a `finally` on an expression `TryCatch`, an unknown node
|
|
95
|
+
* kind. Builtins without a spelling row print raw and are never unprintable.
|
|
96
|
+
*/
|
|
97
|
+
export class Unprintable extends Error {
|
|
98
|
+
constructor(message) {
|
|
99
|
+
super(message);
|
|
100
|
+
this.name = "Unprintable";
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const STATEMENT_KINDS = new Set([
|
|
104
|
+
"Let", "Assign", "Return", "Break", "Continue", "While", "ForArray", "ForSet", "ForDict",
|
|
105
|
+
]);
|
|
106
|
+
/** The node kinds a host literal spells. */
|
|
107
|
+
const CONSTRUCTIONS = new Set(["Struct", "Variant", "NewArray", "NewSet", "NewDict", "NewRef", "NewVector", "NewMatrix"]);
|
|
108
|
+
/** The node kinds whose static type the builder reads back from the expression itself (not from the slot). */
|
|
109
|
+
const EXPRESSIONS = new Set(["Variable", "GetField", "Call", "CallAsync", "Builtin", "Platform", "UnwrapRecursive"]);
|
|
110
|
+
const MAX_DEPTH = 24;
|
|
111
|
+
/** The block parameter every statement-bearing body declares first. */
|
|
112
|
+
const BLOCK = "$";
|
|
113
|
+
const RESERVED = new Set([
|
|
114
|
+
"break", "case", "catch", "class", "const", "continue", "debugger", "default", "delete",
|
|
115
|
+
"do", "else", "enum", "export", "extends", "false", "finally", "for", "function", "if",
|
|
116
|
+
"import", "in", "instanceof", "new", "null", "return", "super", "switch", "this", "throw",
|
|
117
|
+
"true", "try", "typeof", "var", "void", "while", "with", "yield", "let", "static",
|
|
118
|
+
"implements", "interface", "package", "private", "protected", "public", "await", "async",
|
|
119
|
+
"arguments", "eval", "East", "Expr", "variant", "ref", "matrix", "undefined", "NaN", "Infinity",
|
|
120
|
+
]);
|
|
121
|
+
const IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
122
|
+
/** A template slot: an argument, a type parameter, the RegExp or the CSV options. */
|
|
123
|
+
const SLOT = /\{(R|C|T\d+|\d+)\}/g;
|
|
124
|
+
/** A template head that is a method on a slot: `{0}.map`. */
|
|
125
|
+
const METHOD_HEAD = /^\{(\d+)\}\.([A-Za-z_]\w*)$/;
|
|
126
|
+
function isIdent(name) {
|
|
127
|
+
return IDENT.test(name) && !RESERVED.has(name) && !name.startsWith("__");
|
|
128
|
+
}
|
|
129
|
+
function isNullValue(node) {
|
|
130
|
+
return node.type === "Value" && node.value.type.type === "Null";
|
|
131
|
+
}
|
|
132
|
+
function isNullType(t) {
|
|
133
|
+
return t.type === "Null" || t.type === "Never";
|
|
134
|
+
}
|
|
135
|
+
/** A TypeScript literal for an East scalar value (an IR `Value` payload). */
|
|
136
|
+
function literal(lit) {
|
|
137
|
+
switch (lit.type) {
|
|
138
|
+
case "Null": return "null";
|
|
139
|
+
case "Boolean": return lit.value ? "true" : "false";
|
|
140
|
+
case "Integer": return `${lit.value}n`;
|
|
141
|
+
case "Float": {
|
|
142
|
+
const v = lit.value;
|
|
143
|
+
if (Number.isNaN(v))
|
|
144
|
+
return "NaN";
|
|
145
|
+
if (v === Infinity)
|
|
146
|
+
return "Infinity";
|
|
147
|
+
if (v === -Infinity)
|
|
148
|
+
return "-Infinity";
|
|
149
|
+
if (Object.is(v, -0))
|
|
150
|
+
return "-0";
|
|
151
|
+
return String(v);
|
|
152
|
+
}
|
|
153
|
+
case "String": return JSON.stringify(lit.value);
|
|
154
|
+
case "DateTime": return `new Date(${JSON.stringify(lit.value.toISOString())})`;
|
|
155
|
+
case "Blob": return `new Uint8Array([${Array.from(lit.value).join(", ")}])`;
|
|
156
|
+
default:
|
|
157
|
+
throw new Unprintable(`literal of kind ${lit.type}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/** Names in one TypeScript function: IR variable name → identifier. */
|
|
161
|
+
class Scope {
|
|
162
|
+
parent;
|
|
163
|
+
names = new Map();
|
|
164
|
+
used;
|
|
165
|
+
constructor(parent) {
|
|
166
|
+
this.parent = parent;
|
|
167
|
+
this.used = parent ? new Set(parent.used) : new Set([BLOCK]);
|
|
168
|
+
}
|
|
169
|
+
lookup(irName) {
|
|
170
|
+
return this.names.get(irName) ?? this.parent?.lookup(irName);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/** Whether a node prints as a statement (`$.xxx(...)` or a Let): a
|
|
174
|
+
* statement kind, an Error, or a Null-typed branch / match / try. */
|
|
175
|
+
function isStatement(node) {
|
|
176
|
+
return STATEMENT_KINDS.has(node.type) || node.type === "Error" || ((node.type === "IfElse" || node.type === "Match" || node.type === "TryCatch")
|
|
177
|
+
&& isNullType(node.value.type));
|
|
178
|
+
}
|
|
179
|
+
/** `($, x) =>` — an arrow's head. */
|
|
180
|
+
function arrowHead(names) {
|
|
181
|
+
return `(${names.join(", ")}) =>`;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* A concise arrow, `($, x) => body`: a group, so that when the arrow is
|
|
185
|
+
* hugged onto a line the body does not fit, it breaks after the `=>` with
|
|
186
|
+
* the body indented on the next line — and, as the expanded last argument
|
|
187
|
+
* of a call, ends with a trailing comma and the call's close on its own
|
|
188
|
+
* line — as prettier lays it out. A body that is a host literal hugs the
|
|
189
|
+
* `=>` instead (`($, x) => ({` / `($, x) => [`) and breaks inside.
|
|
190
|
+
*/
|
|
191
|
+
function arrow(names, body) {
|
|
192
|
+
const head = arrowHead(names);
|
|
193
|
+
if (isHuggable(body))
|
|
194
|
+
return fn(group([head, " ", body]), group([head, " ", body]));
|
|
195
|
+
return fn(group([head, indent([line, body])]), group([head, indent([line, body]), ifBreak(","), softline]));
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* The arrow of a block body: `($, x) => { ...; }` (`($) => {}` when empty).
|
|
199
|
+
* A group, so that its statements lay out in their own right when the
|
|
200
|
+
* arrow is hugged onto a line that fits.
|
|
201
|
+
*/
|
|
202
|
+
function arrowBlock(names, stmts) {
|
|
203
|
+
if (stmts.length === 0)
|
|
204
|
+
return fn([arrowHead(names), " {}"]);
|
|
205
|
+
return fn(group([arrowHead(names), " {", indent([hardline, join(hardline, stmts)]), hardline, "}"]));
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* A spelling template split into its call shape — the head before the
|
|
209
|
+
* argument list and the argument templates — or `null` when the template
|
|
210
|
+
* is not `head(args)`.
|
|
211
|
+
*/
|
|
212
|
+
function parseCallTemplate(template) {
|
|
213
|
+
if (!template.endsWith(")"))
|
|
214
|
+
return null;
|
|
215
|
+
let depth = 0;
|
|
216
|
+
for (let i = template.length - 1; i >= 0; i--) {
|
|
217
|
+
const ch = template[i];
|
|
218
|
+
if (ch === ")")
|
|
219
|
+
depth += 1;
|
|
220
|
+
else if (ch === "(") {
|
|
221
|
+
depth -= 1;
|
|
222
|
+
if (depth === 0) {
|
|
223
|
+
const head = template.slice(0, i);
|
|
224
|
+
return head === "" ? null : { head, args: splitArgs(template.slice(i + 1, -1)) };
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
/** The top-level comma-separated pieces of an argument-list template. */
|
|
231
|
+
function splitArgs(inner) {
|
|
232
|
+
if (inner.trim() === "")
|
|
233
|
+
return [];
|
|
234
|
+
const args = [];
|
|
235
|
+
let depth = 0;
|
|
236
|
+
let start = 0;
|
|
237
|
+
for (let i = 0; i < inner.length; i++) {
|
|
238
|
+
const ch = inner[i];
|
|
239
|
+
if (ch === "(" || ch === "[" || ch === "{")
|
|
240
|
+
depth += 1;
|
|
241
|
+
else if (ch === ")" || ch === "]" || ch === "}")
|
|
242
|
+
depth -= 1;
|
|
243
|
+
else if (ch === "," && depth === 0) {
|
|
244
|
+
args.push(inner.slice(start, i).trim());
|
|
245
|
+
start = i + 1;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
args.push(inner.slice(start).trim());
|
|
249
|
+
return args;
|
|
250
|
+
}
|
|
251
|
+
/** `text` with every slot replaced by `slot(name)`; a lone slot is the slot's document itself. */
|
|
252
|
+
function fill(text, slot) {
|
|
253
|
+
const parts = [];
|
|
254
|
+
let last = 0;
|
|
255
|
+
for (const m of text.matchAll(SLOT)) {
|
|
256
|
+
const at = m.index ?? 0;
|
|
257
|
+
if (at > last)
|
|
258
|
+
parts.push(text.slice(last, at));
|
|
259
|
+
parts.push(slot(m[1]));
|
|
260
|
+
last = at + m[0].length;
|
|
261
|
+
}
|
|
262
|
+
if (last < text.length)
|
|
263
|
+
parts.push(text.slice(last));
|
|
264
|
+
return parts.length === 1 ? parts[0] : parts;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* The variables a Function node refers to but does not bind: none, and the
|
|
268
|
+
* function is closure-free — it can stand at module level. Names are unique
|
|
269
|
+
* along a scope chain (both builders and the normalizer keep them so), so
|
|
270
|
+
* one set of the names bound anywhere inside suffices.
|
|
271
|
+
*/
|
|
272
|
+
function freeVariables(fn) {
|
|
273
|
+
const referenced = new Set();
|
|
274
|
+
const bound = new Set();
|
|
275
|
+
const nameOf = (v) => v.value.name;
|
|
276
|
+
const walk = (v) => {
|
|
277
|
+
if (Array.isArray(v)) {
|
|
278
|
+
for (const x of v)
|
|
279
|
+
walk(x);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
if (v === null || typeof v !== "object" || v instanceof Date || v instanceof Uint8Array)
|
|
283
|
+
return;
|
|
284
|
+
const node = v;
|
|
285
|
+
if (typeof node.type === "string" && node.value !== null && typeof node.value === "object") {
|
|
286
|
+
const p = node.value;
|
|
287
|
+
switch (node.type) {
|
|
288
|
+
case "Variable":
|
|
289
|
+
referenced.add(p.name);
|
|
290
|
+
break;
|
|
291
|
+
case "Function":
|
|
292
|
+
case "AsyncFunction":
|
|
293
|
+
for (const q of p.parameters)
|
|
294
|
+
bound.add(nameOf(q));
|
|
295
|
+
break;
|
|
296
|
+
case "Let":
|
|
297
|
+
bound.add(nameOf(p.variable));
|
|
298
|
+
break;
|
|
299
|
+
case "ForArray":
|
|
300
|
+
case "ForDict":
|
|
301
|
+
bound.add(nameOf(p.value));
|
|
302
|
+
bound.add(nameOf(p.key));
|
|
303
|
+
break;
|
|
304
|
+
case "ForSet":
|
|
305
|
+
bound.add(nameOf(p.key));
|
|
306
|
+
break;
|
|
307
|
+
case "Match":
|
|
308
|
+
for (const c of p.cases)
|
|
309
|
+
bound.add(nameOf(c.variable));
|
|
310
|
+
break;
|
|
311
|
+
case "TryCatch":
|
|
312
|
+
bound.add(nameOf(p.message));
|
|
313
|
+
bound.add(nameOf(p.stack));
|
|
314
|
+
break;
|
|
315
|
+
default: break;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
// a type never holds a variable — and a type value has a `type` field of its own
|
|
319
|
+
for (const [k, x] of Object.entries(v))
|
|
320
|
+
if (k !== "type" && k !== "type_parameters")
|
|
321
|
+
walk(x);
|
|
322
|
+
};
|
|
323
|
+
walk(fn);
|
|
324
|
+
return new Set([...referenced].filter(n => !bound.has(n)));
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* The case an `unwrap` lowers to — a Match whose one arm returns its own
|
|
328
|
+
* variable and whose every other arm errors `Variant does not have case
|
|
329
|
+
* <that case>`; both surfaces lower `v.unwrap(name)` to exactly this — or
|
|
330
|
+
* `null` for any other match.
|
|
331
|
+
*/
|
|
332
|
+
function unwrapCase(p) {
|
|
333
|
+
const cases = p.cases;
|
|
334
|
+
const returned = cases.filter(c => c.body.type === "Variable" && c.body.value.name === c.variable.value.name);
|
|
335
|
+
if (returned.length !== 1)
|
|
336
|
+
return null;
|
|
337
|
+
const name = returned[0].case;
|
|
338
|
+
for (const c of cases) {
|
|
339
|
+
if (c === returned[0])
|
|
340
|
+
continue;
|
|
341
|
+
if (c.body.type !== "Error")
|
|
342
|
+
return null;
|
|
343
|
+
const message = c.body.value.message;
|
|
344
|
+
if (message.type !== "Value" || message.value.value.type !== "String")
|
|
345
|
+
return null;
|
|
346
|
+
if (message.value.value.value !== `Variant does not have case ${name}`)
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
return name;
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Whether a construction determines its own East type when it stands bare
|
|
353
|
+
* in a position whose type the builder infers (a callback's return): a
|
|
354
|
+
* scalar or an expression, a struct of such, a non-empty array / set / map
|
|
355
|
+
* of such. An empty collection, a variant (`some(x)` alone builds a
|
|
356
|
+
* one-case variant, not an Option; `none` has no payload type), a ref, a
|
|
357
|
+
* vector or a matrix need the type — those print through `East.value(v,
|
|
358
|
+
* T)` there.
|
|
359
|
+
*/
|
|
360
|
+
function selfTyping(node) {
|
|
361
|
+
const p = node.value;
|
|
362
|
+
switch (node.type) {
|
|
363
|
+
case "Struct": return p.fields.every(f => selfTyping(f.value));
|
|
364
|
+
case "NewArray":
|
|
365
|
+
case "NewSet": {
|
|
366
|
+
const values = p.values;
|
|
367
|
+
return values.length > 0 && values.every(selfTyping);
|
|
368
|
+
}
|
|
369
|
+
case "NewDict": {
|
|
370
|
+
const entries = p.values;
|
|
371
|
+
return entries.length > 0 && entries.every(e => selfTyping(e.key) && selfTyping(e.value));
|
|
372
|
+
}
|
|
373
|
+
case "Variant":
|
|
374
|
+
case "NewRef":
|
|
375
|
+
case "NewVector":
|
|
376
|
+
case "NewMatrix":
|
|
377
|
+
case "Function":
|
|
378
|
+
case "AsyncFunction":
|
|
379
|
+
case "Error": return false;
|
|
380
|
+
default: return true; // a literal or an expression carries its type
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* A platform call's argument as the surface writes it: the builder widens
|
|
385
|
+
* an expression whose type is a strict subtype of the declared input by
|
|
386
|
+
* itself (an `As` to the input), so an `As` around an expression prints as
|
|
387
|
+
* the expression alone — `gz(options)`, not `gz(East.as(options, T))` —
|
|
388
|
+
* and rebuilds the same node. A widened construction keeps its `As`: bare,
|
|
389
|
+
* the builder would lift it as the input type directly and the node would
|
|
390
|
+
* be gone.
|
|
391
|
+
*/
|
|
392
|
+
function platformArgument(node) {
|
|
393
|
+
if (node.type !== "As")
|
|
394
|
+
return node;
|
|
395
|
+
const inner = node.value.value;
|
|
396
|
+
return EXPRESSIONS.has(inner.type) ? inner : node;
|
|
397
|
+
}
|
|
398
|
+
/** A structural key for a Function node: two inlined copies of one artifact print once. */
|
|
399
|
+
function functionKey(fn) {
|
|
400
|
+
return JSON.stringify(fn, (_k, v) => typeof v === "bigint" ? `${v}n` : v instanceof Date ? v.toISOString() : v instanceof Uint8Array ? Array.from(v) : v);
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* A member chain's document: the receiver and its segments on one line
|
|
404
|
+
* when they fit, else — for three or more calls not rooted at the block —
|
|
405
|
+
* one call per line, indented under the receiver; a chain holding a block
|
|
406
|
+
* body always expands.
|
|
407
|
+
*/
|
|
408
|
+
function chainDoc({ receiver, segments }) {
|
|
409
|
+
if (segments.length < 3 || receiver === BLOCK)
|
|
410
|
+
return [receiver, ...segments];
|
|
411
|
+
const oneLine = [receiver, ...segments];
|
|
412
|
+
const expanded = group([receiver, indent(segments.map(s => [hardline, s]))]);
|
|
413
|
+
return segments.some(willBreak) ? expanded : choice(oneLine, expanded);
|
|
414
|
+
}
|
|
415
|
+
/** How deep into a module's exported objects a declaration is looked for. */
|
|
416
|
+
const LIBRARY_DEPTH = 4;
|
|
417
|
+
function isPlainObject(v) {
|
|
418
|
+
if (v === null || typeof v !== "object")
|
|
419
|
+
return false;
|
|
420
|
+
const proto = Object.getPrototypeOf(v);
|
|
421
|
+
return proto === Object.prototype || proto === null;
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Every platform declaration the library modules export, by platform name,
|
|
425
|
+
* each with the path the module exports it under. A declaration exported
|
|
426
|
+
* under several names keeps the grouped one (`Compression.Tar.create` over
|
|
427
|
+
* the flat `tar_create` — the documented surface), the shortest of those,
|
|
428
|
+
* the first found among equals.
|
|
429
|
+
*/
|
|
430
|
+
function librarySpellings(libraries) {
|
|
431
|
+
const found = new Map();
|
|
432
|
+
const better = (a, b) => {
|
|
433
|
+
const groupedA = a.path[a.path.length - 1] !== a.declaration.name;
|
|
434
|
+
const groupedB = b.path[b.path.length - 1] !== b.declaration.name;
|
|
435
|
+
if (groupedA !== groupedB)
|
|
436
|
+
return groupedA;
|
|
437
|
+
return a.path.length < b.path.length;
|
|
438
|
+
};
|
|
439
|
+
for (const [specifier, mod] of Object.entries(libraries)) {
|
|
440
|
+
const seen = new Set();
|
|
441
|
+
const stack = [[mod, []]];
|
|
442
|
+
while (stack.length > 0) {
|
|
443
|
+
const [obj, path] = stack.pop();
|
|
444
|
+
if (seen.has(obj))
|
|
445
|
+
continue;
|
|
446
|
+
seen.add(obj);
|
|
447
|
+
for (const key of Object.keys(obj).sort()) {
|
|
448
|
+
if (!isIdent(key))
|
|
449
|
+
continue;
|
|
450
|
+
const value = obj[key];
|
|
451
|
+
const at = [...path, key];
|
|
452
|
+
if (isPlatformDeclaration(value)) {
|
|
453
|
+
const candidate = { specifier, path: at, declaration: value };
|
|
454
|
+
const prior = found.get(value.name);
|
|
455
|
+
if (prior === undefined || better(candidate, prior))
|
|
456
|
+
found.set(value.name, candidate);
|
|
457
|
+
}
|
|
458
|
+
else if (at.length < LIBRARY_DEPTH && isPlainObject(value)) {
|
|
459
|
+
stack.push([value, at]);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
return found;
|
|
465
|
+
}
|
|
466
|
+
const NEVER_A_DECLARATION_NAME = new Set(["some", "none", ...TYPE_IMPORTS]);
|
|
467
|
+
class Printer {
|
|
468
|
+
rootName;
|
|
469
|
+
width;
|
|
470
|
+
spellings;
|
|
471
|
+
types = new Map(); // type key -> [const name, source]
|
|
472
|
+
/** Type value -> its host-literal printer. Keyed by the type object, not its source: a recursive wrapper's inner type has refs that only print inside the wrapper. */
|
|
473
|
+
literals = new WeakMap();
|
|
474
|
+
/** The names the printed module imports from `@elaraai/east` — exactly the ones it uses. */
|
|
475
|
+
used = new Set(["East"]);
|
|
476
|
+
platforms = new Map(); // signature -> const name
|
|
477
|
+
platformDecls = [];
|
|
478
|
+
/** The declaration names taken, and the module-level names a body variable must not shadow (declarations, library import roots). */
|
|
479
|
+
platformNames = new Set();
|
|
480
|
+
reserved = new Set();
|
|
481
|
+
/** Library specifier -> the import roots the module uses from it. */
|
|
482
|
+
libraryImports = new Map();
|
|
483
|
+
/** Closure-free functions called where they stand: structural key -> `_fN`, and their declarations. */
|
|
484
|
+
hoisted = new Map();
|
|
485
|
+
hoistedDecls = [];
|
|
486
|
+
rawBuiltins = new Set();
|
|
487
|
+
/** Method-call documents -> the chain they end. */
|
|
488
|
+
chains = new WeakMap();
|
|
489
|
+
helperCounter = 0;
|
|
490
|
+
tempCounter = 0;
|
|
491
|
+
varCounter = 0;
|
|
492
|
+
/** The import roots the given libraries would bring in — names no hoisted declaration may take. */
|
|
493
|
+
libraryRoots;
|
|
494
|
+
constructor(rootName, width, spellings) {
|
|
495
|
+
this.rootName = rootName;
|
|
496
|
+
this.width = width;
|
|
497
|
+
this.spellings = spellings;
|
|
498
|
+
this.libraryRoots = new Set([...spellings.values()].map(s => s.path[0]));
|
|
499
|
+
}
|
|
500
|
+
// ── module-level pieces ──────────────────────────────────────────────
|
|
501
|
+
/** A type as source, inline; a recursive type hoisted to a `_tN` constant. */
|
|
502
|
+
typeRef(t) {
|
|
503
|
+
typeConstructors(t, this.used);
|
|
504
|
+
const key = typeKey(t);
|
|
505
|
+
if (!key.includes("RecursiveType("))
|
|
506
|
+
return typeDoc(t);
|
|
507
|
+
const hit = this.types.get(key);
|
|
508
|
+
if (hit === undefined) {
|
|
509
|
+
const name = `_t${this.types.size}`;
|
|
510
|
+
this.types.set(key, [name, typeDoc(t)]);
|
|
511
|
+
return name;
|
|
512
|
+
}
|
|
513
|
+
return hit[0];
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* The library's spelling of a platform call, or `null` when no given
|
|
517
|
+
* library exports a declaration of that name with the call's signature
|
|
518
|
+
* (asyncness, optionality, arity and types, or type parameter count when
|
|
519
|
+
* generic) — a call the library's handle would not build stays a hoisted
|
|
520
|
+
* declaration of its own.
|
|
521
|
+
*/
|
|
522
|
+
platformSpelling(p) {
|
|
523
|
+
const hit = this.spellings.get(p.name);
|
|
524
|
+
if (hit === undefined)
|
|
525
|
+
return null;
|
|
526
|
+
const decl = hit.declaration;
|
|
527
|
+
if (decl.async !== !!p.async || decl.optional !== !!p.optional)
|
|
528
|
+
return null;
|
|
529
|
+
const tps = p.type_parameters;
|
|
530
|
+
if (decl.typeParameters !== undefined)
|
|
531
|
+
return decl.typeParameters.length === tps.length ? hit : null;
|
|
532
|
+
if (tps.length > 0)
|
|
533
|
+
return null;
|
|
534
|
+
const args = p.arguments;
|
|
535
|
+
if (decl.inputs.length !== args.length)
|
|
536
|
+
return null;
|
|
537
|
+
const same = (t, key) => typeof t !== "string" && typeKey(toEastTypeValue(t)) === key;
|
|
538
|
+
if (!decl.inputs.every((t, i) => same(t, typeKey(args[i].value.type))))
|
|
539
|
+
return null;
|
|
540
|
+
return same(decl.output, typeKey(p.type)) ? hit : null;
|
|
541
|
+
}
|
|
542
|
+
/** A library spelling in use: its import root is recorded, and the member path returned. */
|
|
543
|
+
useSpelling(spelling) {
|
|
544
|
+
let roots = this.libraryImports.get(spelling.specifier);
|
|
545
|
+
if (roots === undefined) {
|
|
546
|
+
roots = new Set();
|
|
547
|
+
this.libraryImports.set(spelling.specifier, roots);
|
|
548
|
+
}
|
|
549
|
+
roots.add(spelling.path[0]);
|
|
550
|
+
return spelling.path.join(".");
|
|
551
|
+
}
|
|
552
|
+
/** The signature a hoisted declaration is deduplicated by. */
|
|
553
|
+
platformSignature(p) {
|
|
554
|
+
const inputs = p.arguments.map(a => typeKey(a.value.type));
|
|
555
|
+
const tps = p.type_parameters.map(t => typeKey(t));
|
|
556
|
+
return JSON.stringify([p.name, inputs, typeKey(p.type), !!p.async, !!p.optional, tps]);
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* The module-level name of a hoisted declaration: the platform function's
|
|
560
|
+
* own name as an identifier (`tar_create`; `my.log` is `my_log`), a `_2`,
|
|
561
|
+
* `_3`… suffix when another signature, the module's own export or a
|
|
562
|
+
* library's import root already took it, `_pN` when the name cannot be an
|
|
563
|
+
* identifier.
|
|
564
|
+
*/
|
|
565
|
+
platformName(irName) {
|
|
566
|
+
let base = irName.replace(/[^A-Za-z0-9_$]/g, "_");
|
|
567
|
+
if (!/^[A-Za-z_$]/.test(base))
|
|
568
|
+
base = `_${base}`;
|
|
569
|
+
if (!isIdent(base) || NEVER_A_DECLARATION_NAME.has(base))
|
|
570
|
+
base = `_p${this.platformNames.size}`;
|
|
571
|
+
const taken = (name) => this.platformNames.has(name) || name === this.rootName || this.libraryRoots.has(name);
|
|
572
|
+
let name = base;
|
|
573
|
+
for (let n = 2; taken(name); n++)
|
|
574
|
+
name = `${base}_${n}`;
|
|
575
|
+
this.platformNames.add(name);
|
|
576
|
+
return name;
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Fixes the module-level names before any body prints, so a variable a
|
|
580
|
+
* body binds never shadows them: every hoisted declaration's name (by
|
|
581
|
+
* signature, in IR order) and the import root of every library spelling.
|
|
582
|
+
*/
|
|
583
|
+
prepare(ir) {
|
|
584
|
+
const walk = (v) => {
|
|
585
|
+
if (Array.isArray(v)) {
|
|
586
|
+
for (const x of v)
|
|
587
|
+
walk(x);
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
if (v === null || typeof v !== "object" || v instanceof Date || v instanceof Uint8Array)
|
|
591
|
+
return;
|
|
592
|
+
const node = v;
|
|
593
|
+
if (node.type === "Platform" && node.value !== null && typeof node.value === "object") {
|
|
594
|
+
const p = node.value;
|
|
595
|
+
const args = p.arguments;
|
|
596
|
+
const isImport = p.name === IMPORT_PLATFORM && args.length === 2 && args.every(a => a.type === "Value");
|
|
597
|
+
if (!isImport) {
|
|
598
|
+
const spelled = this.platformSpelling(p);
|
|
599
|
+
if (spelled !== null) {
|
|
600
|
+
this.reserved.add(spelled.path[0]);
|
|
601
|
+
}
|
|
602
|
+
else {
|
|
603
|
+
const sig = this.platformSignature(p);
|
|
604
|
+
if (!this.platforms.has(sig)) {
|
|
605
|
+
const name = this.platformName(p.name);
|
|
606
|
+
this.platforms.set(sig, name);
|
|
607
|
+
this.reserved.add(name);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
for (const [k, x] of Object.entries(v))
|
|
613
|
+
if (k !== "type" && k !== "type_parameters")
|
|
614
|
+
walk(x);
|
|
615
|
+
};
|
|
616
|
+
walk(ir);
|
|
617
|
+
}
|
|
618
|
+
platformRef(p) {
|
|
619
|
+
const sig = this.platformSignature(p);
|
|
620
|
+
const name = this.platforms.get(sig) ?? this.platformName(p.name);
|
|
621
|
+
this.platforms.set(sig, name);
|
|
622
|
+
if (this.platformDecls.some(d => Array.isArray(d) && d[1] === name))
|
|
623
|
+
return name;
|
|
624
|
+
const tps = p.type_parameters;
|
|
625
|
+
const args = bracket("[", p.arguments.map(a => this.typeRef(a.value.type)), "]");
|
|
626
|
+
const opt = p.optional ? ["{ optional: true }"] : [];
|
|
627
|
+
if (tps.length > 0) {
|
|
628
|
+
// A generic call: the concrete type arguments are all the node
|
|
629
|
+
// records, so the declaration is spelled with placeholders for
|
|
630
|
+
// them in order and the inputs/output as the call has them.
|
|
631
|
+
const params = bracket("[", tps.map((_t, i) => JSON.stringify(`T${i}`)), "]");
|
|
632
|
+
const decl = p.async ? "East.asyncGenericPlatform" : "East.genericPlatform";
|
|
633
|
+
this.platformDecls.push([
|
|
634
|
+
"const ", name, " = ", decl, callArgs([JSON.stringify(p.name), params, args, this.typeRef(p.type), ...opt]), ";",
|
|
635
|
+
]);
|
|
636
|
+
}
|
|
637
|
+
else {
|
|
638
|
+
const decl = p.async ? "East.asyncPlatform" : "East.platform";
|
|
639
|
+
this.platformDecls.push([
|
|
640
|
+
"const ", name, " = ", decl, callArgs([JSON.stringify(p.name), args, this.typeRef(p.type), ...opt]), ";",
|
|
641
|
+
]);
|
|
642
|
+
}
|
|
643
|
+
return name;
|
|
644
|
+
}
|
|
645
|
+
freshHelper(prefix) {
|
|
646
|
+
this.helperCounter += 1;
|
|
647
|
+
return `_${prefix}${this.helperCounter}`;
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* A closure-free Function node as a module constant `_fN`: its
|
|
651
|
+
* declaration is emitted after any it hoists itself (a constant is built
|
|
652
|
+
* where it is declared), once per distinct function.
|
|
653
|
+
*/
|
|
654
|
+
hoistFunction(node) {
|
|
655
|
+
const key = functionKey(node);
|
|
656
|
+
const hit = this.hoisted.get(key);
|
|
657
|
+
if (hit !== undefined)
|
|
658
|
+
return hit;
|
|
659
|
+
const name = this.freshHelper("f");
|
|
660
|
+
this.hoisted.set(key, name);
|
|
661
|
+
const doc = this.functionExpr(node, new Scope(null));
|
|
662
|
+
this.hoistedDecls.push(["const ", name, " = ", doc, ";"]);
|
|
663
|
+
return name;
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* `receiver.name(args)`: a segment of the member chain `receiver` ends
|
|
667
|
+
* (when it is one) or starts, laid out by {@link chainDoc}.
|
|
668
|
+
*/
|
|
669
|
+
methodCall(receiver, name, args) {
|
|
670
|
+
const segment = [".", name, callArgs(args)];
|
|
671
|
+
const prior = typeof receiver === "string" ? undefined : this.chains.get(receiver);
|
|
672
|
+
const chain = prior
|
|
673
|
+
? { receiver: prior.receiver, segments: [...prior.segments, segment] }
|
|
674
|
+
: { receiver, segments: [segment] };
|
|
675
|
+
const doc = chainDoc(chain);
|
|
676
|
+
this.chains.set(doc, chain);
|
|
677
|
+
return doc;
|
|
678
|
+
}
|
|
679
|
+
// ── functions ────────────────────────────────────────────────────────
|
|
680
|
+
/** Binds an IR Variable node in `scope` and returns its identifier. */
|
|
681
|
+
bind(scope, variable) {
|
|
682
|
+
const irName = variable.value.name;
|
|
683
|
+
let name = isIdent(irName) ? irName : null;
|
|
684
|
+
if (name === null || scope.used.has(name) || this.reserved.has(name)) {
|
|
685
|
+
// The other builder's own spelling, or a name this scope already
|
|
686
|
+
// uses: `v_N` from one module-wide counter, so the rebuilt module
|
|
687
|
+
// names the slot the same way and prints to itself.
|
|
688
|
+
name = `v_${this.varCounter}`;
|
|
689
|
+
this.varCounter += 1;
|
|
690
|
+
}
|
|
691
|
+
scope.names.set(irName, name);
|
|
692
|
+
scope.used.add(name);
|
|
693
|
+
return name;
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* The `East.function([types], out, ($, params) => { ... })` document of a
|
|
697
|
+
* Function / AsyncFunction node; `consts` are hoisted Lets printed first
|
|
698
|
+
* as the body's own bindings (a python artifact's captured constants).
|
|
699
|
+
*/
|
|
700
|
+
functionExpr(node, scope, consts = []) {
|
|
701
|
+
const p = node.value;
|
|
702
|
+
const fnType = p.type;
|
|
703
|
+
const inner = new Scope(scope);
|
|
704
|
+
const params = p.parameters.map(v => this.bind(inner, v));
|
|
705
|
+
const ctor = node.type === "AsyncFunction" ? "East.asyncFunction" : "East.function";
|
|
706
|
+
const f = fnType.value;
|
|
707
|
+
const inputs = bracket("[", f.inputs.map(t => this.typeRef(t)), "]");
|
|
708
|
+
const out = this.typeRef(f.output);
|
|
709
|
+
const names = [BLOCK, ...params];
|
|
710
|
+
// A body that is one expression is a concise arrow, as a callback is
|
|
711
|
+
// and as the source writes it: `($, x) => x.multiply(2n)`; the declared
|
|
712
|
+
// output types what it returns, so a construction prints bare.
|
|
713
|
+
if (consts.length === 0 && p.body.type !== "Block" && !isStatement(p.body)) {
|
|
714
|
+
const sub = [];
|
|
715
|
+
const text = this.conciseBody(p.body, inner, sub, true);
|
|
716
|
+
if (sub.length === 0)
|
|
717
|
+
return [ctor, callArgs([inputs, out, arrow(names, text)])];
|
|
718
|
+
}
|
|
719
|
+
const stmts = [];
|
|
720
|
+
for (const let_ of consts) {
|
|
721
|
+
if (let_.type !== "Let")
|
|
722
|
+
throw new Unprintable(`${let_.type} before the root function`);
|
|
723
|
+
stmts.push(...this.statementDocs(let_, inner));
|
|
724
|
+
}
|
|
725
|
+
stmts.push(...this.bodyDocs(p.body, inner, "function"));
|
|
726
|
+
return [ctor, callArgs([inputs, out, arrowBlock(names, stmts)])];
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* The statements of a body (a Block, or one node), one document each.
|
|
730
|
+
*
|
|
731
|
+
* `mode` says how the last node prints. `"function"` is an
|
|
732
|
+
* `East.function` body with a declared output: a last expression is
|
|
733
|
+
* `return`ed, a last statement stands. `"callback"` is a body whose
|
|
734
|
+
* output the builder infers from what the arrow returns — a callback, an
|
|
735
|
+
* `Expr.block`, an expression arm — so the last node is always returned:
|
|
736
|
+
* an expression as itself, a statement as its `$.` form (which evaluates
|
|
737
|
+
* to the statement already pushed, so the builder does not push it
|
|
738
|
+
* twice). `"null"` is a branch, loop, case, try, catch or finally body:
|
|
739
|
+
* the builder pads one whose last statement is not Null-typed with a
|
|
740
|
+
* `Value null`, so that null is not printed — rebuilding restores it; a
|
|
741
|
+
* `Value null` after a Null-typed statement is the body's own `return
|
|
742
|
+
* null;` and prints as one; a body that is only a `Value null` prints
|
|
743
|
+
* empty.
|
|
744
|
+
*/
|
|
745
|
+
bodyDocs(body, scope, mode) {
|
|
746
|
+
const nodes = body.type === "Block" ? [...body.value.statements] : [body];
|
|
747
|
+
if (mode === "null" && nodes.length > 1 && isNullValue(nodes[nodes.length - 1])
|
|
748
|
+
&& !isNullType(nodes[nodes.length - 2].value.type)) {
|
|
749
|
+
nodes.pop();
|
|
750
|
+
}
|
|
751
|
+
if (mode === "null" && nodes.length === 1 && isNullValue(nodes[0])) {
|
|
752
|
+
return [];
|
|
753
|
+
}
|
|
754
|
+
const docs = [];
|
|
755
|
+
nodes.forEach((node, i) => {
|
|
756
|
+
if (i < nodes.length - 1) {
|
|
757
|
+
docs.push(...this.statementDocs(node, scope));
|
|
758
|
+
}
|
|
759
|
+
else if (mode === "null" && isNullValue(node)) {
|
|
760
|
+
docs.push("return null;");
|
|
761
|
+
}
|
|
762
|
+
else if (mode === "callback" || (mode === "function" && !isStatement(node))) {
|
|
763
|
+
docs.push(...this.returnDocs(node, scope, mode === "function"));
|
|
764
|
+
}
|
|
765
|
+
else {
|
|
766
|
+
docs.push(...this.statementDocs(node, scope));
|
|
767
|
+
}
|
|
768
|
+
});
|
|
769
|
+
return docs;
|
|
770
|
+
}
|
|
771
|
+
/**
|
|
772
|
+
* `return <expr>;` for an expression, `return $.xxx(...);` for a
|
|
773
|
+
* statement. A `$.try(...).catch(...).finally(...)` chain returns nothing,
|
|
774
|
+
* so a TryCatch with a finally body is bound first and the binding
|
|
775
|
+
* returned; a Let has no value to return. `typed` says the body's output
|
|
776
|
+
* is declared (an `East.function`), so a returned construction prints
|
|
777
|
+
* bare; a callback's is inferred from what it returns.
|
|
778
|
+
*/
|
|
779
|
+
returnDocs(node, scope, typed) {
|
|
780
|
+
const pre = [];
|
|
781
|
+
if (node.type === "Let")
|
|
782
|
+
throw new Unprintable("a Let as the last statement of a body whose value is returned");
|
|
783
|
+
if (node.type === "TryCatch" && isNullType(node.value.type) && !isNullValue(node.value.finally_body)) {
|
|
784
|
+
const name = this.freshHelper("s");
|
|
785
|
+
const tried = this.tryStatement(node, scope, pre, false);
|
|
786
|
+
pre.push(["const ", name, " = ", tried, ";"]);
|
|
787
|
+
pre.push([name, ".finally", callArgs([this.bodyArg(node.value.finally_body, scope, [])]), ";"]);
|
|
788
|
+
return [...pre, `return ${name};`];
|
|
789
|
+
}
|
|
790
|
+
const text = this.statementExpr(node, scope, pre) ?? this.valueDoc(node, scope, pre, 0, typed);
|
|
791
|
+
return [...pre, ["return ", text, ";"]];
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* A branch/loop/handler body as the argument of its statement:
|
|
795
|
+
* `($, ...) => { ... }`, the block first; `label` binds the loop label
|
|
796
|
+
* parameter.
|
|
797
|
+
*/
|
|
798
|
+
bodyArg(body, scope, params, label = null) {
|
|
799
|
+
const inner = new Scope(scope);
|
|
800
|
+
const names = [BLOCK, ...params.map(v => this.bind(inner, v))];
|
|
801
|
+
if (label !== null)
|
|
802
|
+
names.push(this.freshLabel(inner, label));
|
|
803
|
+
return arrowBlock(names, this.bodyDocs(body, inner, "null"));
|
|
804
|
+
}
|
|
805
|
+
freshLabel(scope, irLabel) {
|
|
806
|
+
let name = "label";
|
|
807
|
+
let n = 0;
|
|
808
|
+
while (scope.used.has(name)) {
|
|
809
|
+
n += 1;
|
|
810
|
+
name = `label_${n}`;
|
|
811
|
+
}
|
|
812
|
+
scope.names.set(`label:${irLabel}`, name);
|
|
813
|
+
scope.used.add(name);
|
|
814
|
+
return name;
|
|
815
|
+
}
|
|
816
|
+
labelRef(scope, irLabel) {
|
|
817
|
+
const hit = scope.lookup(`label:${irLabel}`);
|
|
818
|
+
if (hit === undefined)
|
|
819
|
+
throw new Unprintable(`jump to a label (${JSON.stringify(irLabel)}) no enclosing loop binds`);
|
|
820
|
+
return hit;
|
|
821
|
+
}
|
|
822
|
+
// ── statements ───────────────────────────────────────────────────────
|
|
823
|
+
/** A node in statement position: a Let binding, a `$.` form, or `$(expr)` — its helpers first. */
|
|
824
|
+
statementDocs(node, scope) {
|
|
825
|
+
const p = node.value;
|
|
826
|
+
const pre = [];
|
|
827
|
+
if (node.type === "Let") {
|
|
828
|
+
const varT = p.variable.value.type;
|
|
829
|
+
let valueNode = p.value;
|
|
830
|
+
// A widening at the binding is the binding's declared type: `$.const(v, T)`.
|
|
831
|
+
let typed = typeKey(varT) === typeKey(valueNode.value.type) ? null : this.typeRef(varT);
|
|
832
|
+
if (valueNode.type === "As" && typeKey(valueNode.value.type) === typeKey(varT)) {
|
|
833
|
+
valueNode = valueNode.value.value;
|
|
834
|
+
typed = this.typeRef(varT);
|
|
835
|
+
}
|
|
836
|
+
// A bound construction is the host literal with the type on the binding
|
|
837
|
+
// (`$.let(new Map([...]), T)`): an `East.value(..., T)` wrapper there is
|
|
838
|
+
// the redundancy the surface's own diagnostics flag. A scalar literal
|
|
839
|
+
// needs no type (`$.let(0n)`), so it stays on the expression path.
|
|
840
|
+
const literal = typed === null && valueNode.type !== "Value" ? this.literalFor(varT)(valueNode, scope, pre, 0) : null;
|
|
841
|
+
if (literal !== null)
|
|
842
|
+
typed = this.typeRef(varT);
|
|
843
|
+
const value = literal ?? this.expr(valueNode, scope, pre);
|
|
844
|
+
const name = this.bind(scope, p.variable);
|
|
845
|
+
const ctor = p.variable.value.mutable ? `${BLOCK}.let` : `${BLOCK}.const`;
|
|
846
|
+
return [...pre, ["const ", name, " = ", ctor, callArgs(typed === null ? [value] : [value, typed]), ";"]];
|
|
847
|
+
}
|
|
848
|
+
const text = this.statementExpr(node, scope, pre);
|
|
849
|
+
if (text !== null)
|
|
850
|
+
return [...pre, [text, ";"]];
|
|
851
|
+
return [...pre, [BLOCK, callArgs([this.expr(node, scope, pre)]), ";"]];
|
|
852
|
+
}
|
|
853
|
+
/**
|
|
854
|
+
* The `$.` form of a statement node (`$.assign(a, b)`, `$.if(...)...`),
|
|
855
|
+
* or `null` for a node with none (an expression, a Let).
|
|
856
|
+
*/
|
|
857
|
+
statementExpr(node, scope, pre) {
|
|
858
|
+
const kind = node.type;
|
|
859
|
+
const p = node.value;
|
|
860
|
+
if (kind === "Assign") {
|
|
861
|
+
// the variable types the value, as the block's declared output types a `$.return`
|
|
862
|
+
const value = this.valueDoc(p.value, scope, pre, 0, true);
|
|
863
|
+
return this.methodCall(BLOCK, "assign", [this.varRef(p.variable, scope), value]);
|
|
864
|
+
}
|
|
865
|
+
if (kind === "Return") {
|
|
866
|
+
return this.methodCall(BLOCK, "return", [this.valueDoc(p.value, scope, pre, 0, true)]);
|
|
867
|
+
}
|
|
868
|
+
if (kind === "Break" || kind === "Continue") {
|
|
869
|
+
return this.methodCall(BLOCK, kind === "Break" ? "break" : "continue", [this.labelRef(scope, p.label.name)]);
|
|
870
|
+
}
|
|
871
|
+
if (kind === "Error") {
|
|
872
|
+
return this.methodCall(BLOCK, "error", [this.expr(p.message, scope, pre)]);
|
|
873
|
+
}
|
|
874
|
+
if (kind === "While") {
|
|
875
|
+
const pred = this.expr(p.predicate, scope, pre);
|
|
876
|
+
const body = this.bodyArg(p.body, scope, [], p.label.name);
|
|
877
|
+
return this.methodCall(BLOCK, "while", [pred, body]);
|
|
878
|
+
}
|
|
879
|
+
if (kind === "ForArray" || kind === "ForSet" || kind === "ForDict") {
|
|
880
|
+
const src = kind === "ForArray" ? "array" : kind === "ForSet" ? "set" : "dict";
|
|
881
|
+
const coll = this.expr(p[src], scope, pre);
|
|
882
|
+
const params = kind === "ForSet" ? [p.key] : [p.value, p.key];
|
|
883
|
+
const body = this.bodyArg(p.body, scope, params, p.label.name);
|
|
884
|
+
return this.methodCall(BLOCK, "for", [coll, body]);
|
|
885
|
+
}
|
|
886
|
+
if (kind === "IfElse" && isNullType(p.type))
|
|
887
|
+
return this.ifStatement(node, scope, pre);
|
|
888
|
+
if (kind === "Match" && isNullType(p.type))
|
|
889
|
+
return this.matchStatement(node, scope, pre);
|
|
890
|
+
if (kind === "TryCatch" && isNullType(p.type))
|
|
891
|
+
return this.tryStatement(node, scope, pre);
|
|
892
|
+
return null;
|
|
893
|
+
}
|
|
894
|
+
ifStatement(node, scope, pre) {
|
|
895
|
+
const p = node.value;
|
|
896
|
+
let doc = BLOCK;
|
|
897
|
+
p.ifs.forEach((branch, i) => {
|
|
898
|
+
const pred = this.expr(branch.predicate, scope, pre);
|
|
899
|
+
const body = this.bodyArg(branch.body, scope, []);
|
|
900
|
+
doc = this.methodCall(doc, i === 0 ? "if" : "elseIf", [pred, body]);
|
|
901
|
+
});
|
|
902
|
+
if (!isNullValue(p.else_body)) {
|
|
903
|
+
doc = this.methodCall(doc, "else", [this.bodyArg(p.else_body, scope, [])]);
|
|
904
|
+
}
|
|
905
|
+
return doc;
|
|
906
|
+
}
|
|
907
|
+
matchStatement(node, scope, pre) {
|
|
908
|
+
const p = node.value;
|
|
909
|
+
const subject = this.expr(p.variant, scope, pre);
|
|
910
|
+
const arms = [];
|
|
911
|
+
for (const c of p.cases) {
|
|
912
|
+
if (isNullValue(c.body))
|
|
913
|
+
continue;
|
|
914
|
+
arms.push([objectKey(c.case), ": ", this.bodyArg(c.body, scope, [c.variable])]);
|
|
915
|
+
}
|
|
916
|
+
return this.methodCall(BLOCK, "match", [subject, hug(bracket("{", arms, "}", " "))]);
|
|
917
|
+
}
|
|
918
|
+
/** `$.try(...).catch(...).finally(...)`; `withFinally: false` leaves the finally off. */
|
|
919
|
+
tryStatement(node, scope, pre, withFinally = true) {
|
|
920
|
+
void pre;
|
|
921
|
+
const p = node.value;
|
|
922
|
+
let doc = this.methodCall(BLOCK, "try", [this.bodyArg(p.try_body, scope, [])]);
|
|
923
|
+
if (!isNullValue(p.catch_body)) {
|
|
924
|
+
doc = this.methodCall(doc, "catch", [this.bodyArg(p.catch_body, scope, [p.message, p.stack])]);
|
|
925
|
+
}
|
|
926
|
+
if (withFinally && !isNullValue(p.finally_body)) {
|
|
927
|
+
doc = this.methodCall(doc, "finally", [this.bodyArg(p.finally_body, scope, [])]);
|
|
928
|
+
}
|
|
929
|
+
return doc;
|
|
930
|
+
}
|
|
931
|
+
// ── expressions ──────────────────────────────────────────────────────
|
|
932
|
+
varRef(node, scope) {
|
|
933
|
+
const hit = scope.lookup(node.value.name);
|
|
934
|
+
if (hit === undefined)
|
|
935
|
+
throw new Unprintable(`variable ${JSON.stringify(node.value.name)} is not bound`);
|
|
936
|
+
return hit;
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* The document of an expression node; helper declarations and
|
|
940
|
+
* temporaries go to `pre` (statements to emit before the user).
|
|
941
|
+
*/
|
|
942
|
+
expr(node, scope, pre, depth = 0) {
|
|
943
|
+
if (depth > MAX_DEPTH) {
|
|
944
|
+
const text = this.expr(node, scope, pre, 0);
|
|
945
|
+
this.tempCounter += 1;
|
|
946
|
+
const name = `_e${this.tempCounter}`;
|
|
947
|
+
pre.push(["const ", name, " = ", text, ";"]);
|
|
948
|
+
return name;
|
|
949
|
+
}
|
|
950
|
+
const d = depth + 1;
|
|
951
|
+
const kind = node.type;
|
|
952
|
+
const p = node.value;
|
|
953
|
+
switch (kind) {
|
|
954
|
+
case "Value":
|
|
955
|
+
return literal(p.value);
|
|
956
|
+
case "Variable":
|
|
957
|
+
return this.varRef(node, scope);
|
|
958
|
+
case "Builtin":
|
|
959
|
+
return this.builtinExpr(node, scope, pre, d);
|
|
960
|
+
case "Platform": {
|
|
961
|
+
const argNodes = p.arguments;
|
|
962
|
+
if (p.name === IMPORT_PLATFORM && argNodes.length === 2 && argNodes.every(a => a.type === "Value")) {
|
|
963
|
+
// an unresolved cross-language import: its own spelling, not a platform declaration
|
|
964
|
+
const [pkg, name] = argNodes.map(a => literal(a.value.value));
|
|
965
|
+
return ["East.importFunction", callArgs([pkg, name, this.typeRef(p.type)])];
|
|
966
|
+
}
|
|
967
|
+
const args = argNodes.map(a => this.valueDoc(platformArgument(a), scope, pre, d, true));
|
|
968
|
+
const spelled = this.platformSpelling(p);
|
|
969
|
+
const ref = spelled !== null ? this.useSpelling(spelled) : this.platformRef(p);
|
|
970
|
+
if (p.type_parameters.length > 0) {
|
|
971
|
+
const tps = bracket("[", p.type_parameters.map(t => this.typeRef(t)), "]");
|
|
972
|
+
return [ref, callArgs([tps, ...args])];
|
|
973
|
+
}
|
|
974
|
+
return [ref, callArgs(args)];
|
|
975
|
+
}
|
|
976
|
+
case "Function":
|
|
977
|
+
case "AsyncFunction":
|
|
978
|
+
return this.functionExpr(node, scope);
|
|
979
|
+
case "Call":
|
|
980
|
+
case "CallAsync": {
|
|
981
|
+
const head = p.function;
|
|
982
|
+
// the callee's declared inputs type its arguments
|
|
983
|
+
const args = p.arguments.map(a => this.valueDoc(a, scope, pre, d, true));
|
|
984
|
+
if ((head.type === "Function" || head.type === "AsyncFunction") && freeVariables(head).size === 0) {
|
|
985
|
+
// an artifact inlined at its call: hoisted, and called by name as the source called it
|
|
986
|
+
return [this.hoistFunction(head), callArgs(args)];
|
|
987
|
+
}
|
|
988
|
+
let callee = this.expr(head, scope, pre, d);
|
|
989
|
+
// a callee that already prints as a call, a member or a name needs no parentheses
|
|
990
|
+
const fk = p.function.type;
|
|
991
|
+
if (!["Variable", "GetField", "Call", "CallAsync", "Builtin", "Platform"].includes(fk))
|
|
992
|
+
callee = ["(", callee, ")"];
|
|
993
|
+
return [callee, callArgs(args)];
|
|
994
|
+
}
|
|
995
|
+
case "GetField": {
|
|
996
|
+
const base = this.expr(p.struct, scope, pre, d);
|
|
997
|
+
const name = p.field;
|
|
998
|
+
return IDENT.test(name) && !name.startsWith("_") ? [base, ".", name] : [base, "[", JSON.stringify(name), "]"];
|
|
999
|
+
}
|
|
1000
|
+
case "Struct":
|
|
1001
|
+
case "Variant":
|
|
1002
|
+
case "NewArray":
|
|
1003
|
+
case "NewSet":
|
|
1004
|
+
case "NewDict":
|
|
1005
|
+
case "NewRef":
|
|
1006
|
+
case "NewVector":
|
|
1007
|
+
case "NewMatrix":
|
|
1008
|
+
return ["East.value", callArgs([this.literalFor(p.type)(node, scope, pre, depth), this.typeRef(p.type)])];
|
|
1009
|
+
case "As":
|
|
1010
|
+
return ["East.as", callArgs([this.expr(p.value, scope, pre, d), this.typeRef(p.type)])];
|
|
1011
|
+
case "WrapRecursive": {
|
|
1012
|
+
// the wrapper's inner type governs the wrapped value: a construction prints bare
|
|
1013
|
+
const payload = p.type.value;
|
|
1014
|
+
const inner = payload.type === "wrapper" ? payload.value.inner : null;
|
|
1015
|
+
const text = inner === null ? null : this.literalFor(inner)(p.value, scope, pre, d);
|
|
1016
|
+
return ["East.wrapRecursive", callArgs([text ?? this.expr(p.value, scope, pre, d), this.typeRef(p.type)])];
|
|
1017
|
+
}
|
|
1018
|
+
case "UnwrapRecursive":
|
|
1019
|
+
return this.methodCall(this.expr(p.value, scope, pre, d), "unwrap", []);
|
|
1020
|
+
case "Error":
|
|
1021
|
+
this.used.add("Expr");
|
|
1022
|
+
return ["Expr.error", callArgs([this.expr(p.message, scope, pre, d)])];
|
|
1023
|
+
case "IfElse":
|
|
1024
|
+
return this.ifExpr(p.ifs, p.else_body, scope, pre, d);
|
|
1025
|
+
case "Match": {
|
|
1026
|
+
const subject = this.expr(p.variant, scope, pre, d);
|
|
1027
|
+
// `v.unwrap()` lowers to a match; printed back as the call, so the
|
|
1028
|
+
// rebuilt module type-checks (the error arms are Never-typed)
|
|
1029
|
+
const unwrapped = unwrapCase(p);
|
|
1030
|
+
if (unwrapped !== null)
|
|
1031
|
+
return this.methodCall(subject, "unwrap", unwrapped === "some" ? [] : [JSON.stringify(unwrapped)]);
|
|
1032
|
+
const arms = p.cases
|
|
1033
|
+
.map((c) => [objectKey(c.case), ": ", this.callbackExpr(c.body, [c.variable], scope, pre)]);
|
|
1034
|
+
return this.methodCall(subject, "match", [hug(bracket("{", arms, "}", " "))]);
|
|
1035
|
+
}
|
|
1036
|
+
case "TryCatch": {
|
|
1037
|
+
if (!isNullValue(p.finally_body)) {
|
|
1038
|
+
throw new Unprintable("a TryCatch with a finally body in expression position — TypeScript's Expr.tryCatch has no finally");
|
|
1039
|
+
}
|
|
1040
|
+
const body = this.armExpr(p.try_body, scope, pre, d);
|
|
1041
|
+
const handler = this.callbackExpr(p.catch_body, [p.message, p.stack], scope, pre);
|
|
1042
|
+
this.used.add("Expr");
|
|
1043
|
+
return ["Expr.tryCatch", callArgs([body, handler])];
|
|
1044
|
+
}
|
|
1045
|
+
case "Block":
|
|
1046
|
+
return this.blockExpr(node, scope);
|
|
1047
|
+
default:
|
|
1048
|
+
if (STATEMENT_KINDS.has(kind))
|
|
1049
|
+
throw new Unprintable(`${kind} node in expression position`);
|
|
1050
|
+
throw new Unprintable(`unknown node kind ${kind}`);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
/**
|
|
1054
|
+
* A node in a value position — a builtin's value slot, a call argument,
|
|
1055
|
+
* a returned value: a construction prints as the host literal, bare when
|
|
1056
|
+
* the position is typed by the surface (`typed`: a `SubtypeExprOrValue`
|
|
1057
|
+
* slot, a declared output, an assigned variable) or when the literal
|
|
1058
|
+
* types itself ({@link selfTyping} — a callback's return, whose type the
|
|
1059
|
+
* builder infers), and through `East.value(v, T)` otherwise; any other
|
|
1060
|
+
* node prints as the expression it is.
|
|
1061
|
+
*/
|
|
1062
|
+
valueDoc(node, scope, pre, depth, typed) {
|
|
1063
|
+
if (!this.printsBare(node, typed))
|
|
1064
|
+
return this.expr(node, scope, pre, depth);
|
|
1065
|
+
return this.literalFor(node.value.type)(node, scope, pre, depth) ?? this.expr(node, scope, pre, depth);
|
|
1066
|
+
}
|
|
1067
|
+
/** Whether {@link valueDoc} prints `node` as a bare host literal. */
|
|
1068
|
+
printsBare(node, typed) {
|
|
1069
|
+
return CONSTRUCTIONS.has(node.type) && (typed || selfTyping(node));
|
|
1070
|
+
}
|
|
1071
|
+
/**
|
|
1072
|
+
* The body of a concise arrow, `($, x) => body`: {@link valueDoc}, with a
|
|
1073
|
+
* bare struct literal in parentheses — `($, x) => ({ a: x })` — since an
|
|
1074
|
+
* object at the head of an arrow body parses as a block.
|
|
1075
|
+
*/
|
|
1076
|
+
conciseBody(node, scope, pre, typed) {
|
|
1077
|
+
const text = this.valueDoc(node, scope, pre, 0, typed);
|
|
1078
|
+
return node.type === "Struct" && this.printsBare(node, typed) ? hug(["(", text, ")"]) : text;
|
|
1079
|
+
}
|
|
1080
|
+
/**
|
|
1081
|
+
* The host-literal printer for values of type `t` — a factory over the
|
|
1082
|
+
* TYPE, as `compareFor(t)` and `equalFor(t)` are: the factory for a Dict
|
|
1083
|
+
* holds the factories for its key and value types, and so on down, so the
|
|
1084
|
+
* one type on a binding governs every position of the literal and a
|
|
1085
|
+
* construction nested anywhere prints bare (`new Map([["a", new Set([1n])]])`).
|
|
1086
|
+
* Applied to a node it returns the literal, or `null` when the node is not
|
|
1087
|
+
* the construction its position's type expects (a variable, a call, a
|
|
1088
|
+
* widening, a function) — the caller prints that as an expression.
|
|
1089
|
+
* `East.value(<literal>, T)` in expression position; `$.let(<literal>, T)`
|
|
1090
|
+
* for a binding.
|
|
1091
|
+
*/
|
|
1092
|
+
literalFor(t) {
|
|
1093
|
+
const hit = this.literals.get(t);
|
|
1094
|
+
if (hit !== undefined)
|
|
1095
|
+
return hit;
|
|
1096
|
+
const made = this.makeLiteral(t);
|
|
1097
|
+
this.literals.set(t, made);
|
|
1098
|
+
return made;
|
|
1099
|
+
}
|
|
1100
|
+
makeLiteral(t) {
|
|
1101
|
+
const child = (u) => {
|
|
1102
|
+
const inner = this.literalFor(u);
|
|
1103
|
+
return (node, scope, pre, depth) => inner(node, scope, pre, depth) ?? this.expr(node, scope, pre, depth);
|
|
1104
|
+
};
|
|
1105
|
+
const asExpr = (node, scope, pre, depth) => this.expr(node, scope, pre, depth);
|
|
1106
|
+
switch (t.type) {
|
|
1107
|
+
case "Null":
|
|
1108
|
+
case "Boolean":
|
|
1109
|
+
case "Integer":
|
|
1110
|
+
case "Float":
|
|
1111
|
+
case "String":
|
|
1112
|
+
case "DateTime":
|
|
1113
|
+
case "Blob":
|
|
1114
|
+
return node => node.type === "Value" ? literal(node.value.value) : null;
|
|
1115
|
+
case "Array": {
|
|
1116
|
+
const elem = child(t.value);
|
|
1117
|
+
return (node, scope, pre, depth) => node.type === "NewArray"
|
|
1118
|
+
? hug(bracket("[", node.value.values.map(v => elem(v, scope, pre, depth + 1)), "]"))
|
|
1119
|
+
: null;
|
|
1120
|
+
}
|
|
1121
|
+
case "Set": {
|
|
1122
|
+
const elem = child(t.value);
|
|
1123
|
+
// an empty set stays `new Set([])`: the compiler types it `Set<never>`, which every East slot admits (`new Set()` is a `Set<unknown>`, which none does)
|
|
1124
|
+
return (node, scope, pre, depth) => node.type === "NewSet"
|
|
1125
|
+
? hug(["new Set(", bracket("[", node.value.values.map(v => elem(v, scope, pre, depth + 1)), "]"), ")"])
|
|
1126
|
+
: null;
|
|
1127
|
+
}
|
|
1128
|
+
case "Dict": {
|
|
1129
|
+
const kv = t.value;
|
|
1130
|
+
const key = child(kv.key);
|
|
1131
|
+
const value = child(kv.value);
|
|
1132
|
+
return (node, scope, pre, depth) => {
|
|
1133
|
+
if (node.type !== "NewDict")
|
|
1134
|
+
return null;
|
|
1135
|
+
const entries = node.value.values;
|
|
1136
|
+
// an empty map is `new Map()`: the compiler types `new Map([])` `Map<unknown, unknown>`, which no East slot admits
|
|
1137
|
+
if (entries.length === 0)
|
|
1138
|
+
return "new Map()";
|
|
1139
|
+
return hug(["new Map(", bracket("[", entries
|
|
1140
|
+
.map(e => bracket("[", [key(e.key, scope, pre, depth + 1), value(e.value, scope, pre, depth + 1)], "]")), "]"), ")"]);
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
case "Struct": {
|
|
1144
|
+
const fields = new Map(t.value.map(f => [f.name, child(f.type)]));
|
|
1145
|
+
return (node, scope, pre, depth) => node.type === "Struct"
|
|
1146
|
+
? hug(bracket("{", node.value.fields
|
|
1147
|
+
.map((f) => [objectKey(f.name), ": ", (fields.get(f.name) ?? asExpr)(f.value, scope, pre, depth + 1)]), "}", " "))
|
|
1148
|
+
: null;
|
|
1149
|
+
}
|
|
1150
|
+
case "Variant": {
|
|
1151
|
+
const cases = new Map(t.value.map(c => [c.name, child(c.type)]));
|
|
1152
|
+
const option = isOptionValue(t);
|
|
1153
|
+
return (node, scope, pre, depth) => {
|
|
1154
|
+
if (node.type !== "Variant")
|
|
1155
|
+
return null;
|
|
1156
|
+
const c = node.value.case;
|
|
1157
|
+
if (option && c === "none") {
|
|
1158
|
+
this.used.add("none");
|
|
1159
|
+
return "none";
|
|
1160
|
+
}
|
|
1161
|
+
const payload = (cases.get(c) ?? asExpr)(node.value.value, scope, pre, depth + 1);
|
|
1162
|
+
this.used.add(option ? "some" : "variant");
|
|
1163
|
+
return option ? ["some", callArgs([payload])] : ["variant", callArgs([JSON.stringify(c), payload])];
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
case "Ref": {
|
|
1167
|
+
const inner = child(t.value);
|
|
1168
|
+
return (node, scope, pre, depth) => {
|
|
1169
|
+
if (node.type !== "NewRef")
|
|
1170
|
+
return null;
|
|
1171
|
+
this.used.add("ref");
|
|
1172
|
+
return ["ref", callArgs([inner(node.value.value, scope, pre, depth + 1)])];
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
case "Vector":
|
|
1176
|
+
return node => node.type === "NewVector" ? this.typedArray(node.value.values, t.value, "NewVector") : null;
|
|
1177
|
+
case "Matrix":
|
|
1178
|
+
return node => {
|
|
1179
|
+
if (node.type !== "NewMatrix")
|
|
1180
|
+
return null;
|
|
1181
|
+
this.used.add("matrix");
|
|
1182
|
+
return ["matrix", callArgs([this.typedArray(node.value.values, t.value, "NewMatrix"), String(node.value.rows), String(node.value.cols)])];
|
|
1183
|
+
};
|
|
1184
|
+
default:
|
|
1185
|
+
// Never, Function, AsyncFunction, Recursive: no host literal at this position
|
|
1186
|
+
return () => null;
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
/** A literal-only NewVector / NewMatrix payload as its typed array. */
|
|
1190
|
+
typedArray(values, elem, what) {
|
|
1191
|
+
const lits = values.map(v => {
|
|
1192
|
+
if (v.type !== "Value")
|
|
1193
|
+
throw new Unprintable(`a ${what} of expressions — TypeScript builds vectors from typed arrays of literals`);
|
|
1194
|
+
return v.value.value;
|
|
1195
|
+
});
|
|
1196
|
+
if (elem.type === "Float")
|
|
1197
|
+
return ["new Float64Array(", bracket("[", lits.map(l => literal(l)), "]"), ")"];
|
|
1198
|
+
if (elem.type === "Integer")
|
|
1199
|
+
return ["new BigInt64Array(", bracket("[", lits.map(l => literal(l)), "]"), ")"];
|
|
1200
|
+
if (elem.type === "Boolean")
|
|
1201
|
+
return ["new Uint8ClampedArray(", bracket("[", lits.map(l => (l.value ? "1" : "0")), "]"), ")"];
|
|
1202
|
+
throw new Unprintable(`a ${what} of ${elem.type} elements`);
|
|
1203
|
+
}
|
|
1204
|
+
/** An expression that must be an Expr, not a bare literal (a method receiver). */
|
|
1205
|
+
tracedExpr(node, scope, pre, depth) {
|
|
1206
|
+
if (node.type === "Value") {
|
|
1207
|
+
const lit = node.value.value;
|
|
1208
|
+
if (lit.type === "Null")
|
|
1209
|
+
return "East.value(null)";
|
|
1210
|
+
return ["East.value", callArgs([literal(lit)])];
|
|
1211
|
+
}
|
|
1212
|
+
return this.expr(node, scope, pre, depth);
|
|
1213
|
+
}
|
|
1214
|
+
/** An `ifElse` chain: TypeScript spells one predicate per node, so a
|
|
1215
|
+
* multi-branch IfElse nests in the else arm. */
|
|
1216
|
+
ifExpr(ifs, elseBody, scope, pre, depth) {
|
|
1217
|
+
const [first, ...rest] = ifs;
|
|
1218
|
+
const pred = this.tracedExpr(first.predicate, scope, pre, depth);
|
|
1219
|
+
const then = this.armCallback(first.body, scope, pre);
|
|
1220
|
+
const otherwise = rest.length === 0
|
|
1221
|
+
? this.armCallback(elseBody, scope, pre)
|
|
1222
|
+
: arrow([BLOCK], this.ifExpr(rest, elseBody, scope, pre, depth));
|
|
1223
|
+
return this.methodCall(pred, "ifElse", [then, otherwise]);
|
|
1224
|
+
}
|
|
1225
|
+
/** An if-arm body as the `($) => ...` callback `ifElse` takes. */
|
|
1226
|
+
armCallback(body, scope, pre) {
|
|
1227
|
+
return this.callbackExpr(body, [], scope, pre);
|
|
1228
|
+
}
|
|
1229
|
+
/** An expression arm that must be an Expr: a Block as `Expr.block(...)`,
|
|
1230
|
+
* a literal through `East.value`. */
|
|
1231
|
+
armExpr(body, scope, pre, depth) {
|
|
1232
|
+
if (body.type === "Block")
|
|
1233
|
+
return this.blockExpr(body, scope);
|
|
1234
|
+
return this.tracedExpr(body, scope, pre, depth);
|
|
1235
|
+
}
|
|
1236
|
+
/** A Block in expression position: `Expr.block(($) => { ... })`. */
|
|
1237
|
+
blockExpr(body, scope) {
|
|
1238
|
+
const inner = new Scope(scope);
|
|
1239
|
+
const stmts = this.bodyDocs(body, inner, "callback");
|
|
1240
|
+
this.used.add("Expr");
|
|
1241
|
+
return ["Expr.block", callArgs([arrowBlock([BLOCK], stmts)])];
|
|
1242
|
+
}
|
|
1243
|
+
/**
|
|
1244
|
+
* A callback body, the block first: `($, params) => expr` when the body
|
|
1245
|
+
* is one expression (or one statement, as its `$.` form), else `($,
|
|
1246
|
+
* params) => { ...; return expr; }`. The parameters keep the builtin's
|
|
1247
|
+
* own order. The builder infers the callback's type from what it
|
|
1248
|
+
* returns, so a construction prints bare only when it types itself.
|
|
1249
|
+
*/
|
|
1250
|
+
callbackExpr(body, params, scope, pre) {
|
|
1251
|
+
const inner = new Scope(scope);
|
|
1252
|
+
const names = [BLOCK, ...params.map(v => this.bind(inner, v))];
|
|
1253
|
+
void pre;
|
|
1254
|
+
const tryWithFinally = body.type === "TryCatch" && !isNullValue(body.value.finally_body);
|
|
1255
|
+
if (body.type !== "Block" && body.type !== "Let" && !tryWithFinally) {
|
|
1256
|
+
const sub = [];
|
|
1257
|
+
const text = this.statementExpr(body, inner, sub);
|
|
1258
|
+
if (text !== null)
|
|
1259
|
+
return sub.length === 0 ? arrow(names, text) : arrowBlock(names, [...sub, ["return ", text, ";"]]);
|
|
1260
|
+
const value = this.conciseBody(body, inner, sub, false);
|
|
1261
|
+
if (sub.length === 0)
|
|
1262
|
+
return arrow(names, value);
|
|
1263
|
+
return arrowBlock(names, [...sub, ["return ", this.valueDoc(body, inner, sub, 0, false), ";"]]);
|
|
1264
|
+
}
|
|
1265
|
+
return arrowBlock(names, this.bodyDocs(body, inner, "callback"));
|
|
1266
|
+
}
|
|
1267
|
+
// ── builtins ─────────────────────────────────────────────────────────
|
|
1268
|
+
builtinExpr(node, scope, pre, depth) {
|
|
1269
|
+
const p = node.value;
|
|
1270
|
+
const name = p.builtin;
|
|
1271
|
+
const row = spellingFor(name);
|
|
1272
|
+
if (row !== undefined) {
|
|
1273
|
+
const rendered = this.renderRow(row, p, scope, pre, depth);
|
|
1274
|
+
if (rendered !== null)
|
|
1275
|
+
return rendered;
|
|
1276
|
+
}
|
|
1277
|
+
return this.rawBuiltin(node, scope, pre, depth);
|
|
1278
|
+
}
|
|
1279
|
+
renderRow(row, p, scope, pre, depth) {
|
|
1280
|
+
const args = p.arguments;
|
|
1281
|
+
const tps = p.type_parameters;
|
|
1282
|
+
if (row.floatOnly && !(tps.length > 0 && tps[0].type === "Float"))
|
|
1283
|
+
return null;
|
|
1284
|
+
const callbacks = new Set(row.callbacks ?? []);
|
|
1285
|
+
const exprs = new Set(row.exprs ?? []);
|
|
1286
|
+
const inferred = new Set(row.inferred ?? []);
|
|
1287
|
+
const texts = [];
|
|
1288
|
+
let regex = null;
|
|
1289
|
+
let csv = null;
|
|
1290
|
+
if (row.adapter === "regex") {
|
|
1291
|
+
// the pattern and flags are String literals: a RegExp literal
|
|
1292
|
+
const [pat, flags] = [args[1], args[2]];
|
|
1293
|
+
if (!pat || !flags || pat.type !== "Value" || flags.type !== "Value")
|
|
1294
|
+
return null;
|
|
1295
|
+
regex = `new RegExp(${literal(pat.value.value)}, ${literal(flags.value.value)})`;
|
|
1296
|
+
}
|
|
1297
|
+
if (row.adapter === "csv") {
|
|
1298
|
+
const config = args[args.length - 1];
|
|
1299
|
+
const opts = this.csvOptions(config);
|
|
1300
|
+
if (opts === null)
|
|
1301
|
+
return null;
|
|
1302
|
+
csv = opts;
|
|
1303
|
+
}
|
|
1304
|
+
args.forEach((arg, i) => {
|
|
1305
|
+
if (row.adapter === "regex" && (i === 1 || i === 2)) {
|
|
1306
|
+
texts.push("");
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
if (row.adapter === "csv" && i === args.length - 1) {
|
|
1310
|
+
texts.push("");
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
if (callbacks.has(i) && arg.type === "Function") {
|
|
1314
|
+
const fp = arg.value;
|
|
1315
|
+
texts.push(this.callbackExpr(fp.body, [...fp.parameters], scope, pre));
|
|
1316
|
+
return;
|
|
1317
|
+
}
|
|
1318
|
+
// The first operand is always an Expr (a literal receiver has no
|
|
1319
|
+
// methods, and the namespace helpers read their first argument's
|
|
1320
|
+
// type), as is every slot the row marks `exprs`; a slot the row marks
|
|
1321
|
+
// `inferred` takes its East type from the argument, so a construction
|
|
1322
|
+
// prints bare only when it types itself; every other slot is typed by
|
|
1323
|
+
// the surface (`SubtypeExprOrValue`, checked against the signatures by
|
|
1324
|
+
// spellings.spec.ts), so a construction prints bare.
|
|
1325
|
+
texts.push(i === 0 || exprs.has(i)
|
|
1326
|
+
? this.tracedExpr(arg, scope, pre, depth)
|
|
1327
|
+
: this.valueDoc(arg, scope, pre, depth, !inferred.has(i)));
|
|
1328
|
+
});
|
|
1329
|
+
let template = row.template;
|
|
1330
|
+
if (csv === "")
|
|
1331
|
+
template = template.replace(", {C}", "").replace("{C}", "");
|
|
1332
|
+
const slot = (name) => {
|
|
1333
|
+
if (name === "R")
|
|
1334
|
+
return regex ?? "";
|
|
1335
|
+
if (name === "C")
|
|
1336
|
+
return csv ?? "";
|
|
1337
|
+
if (name.startsWith("T")) {
|
|
1338
|
+
const t = tps[Number(name.slice(1))];
|
|
1339
|
+
if (t === undefined)
|
|
1340
|
+
throw new Unprintable(`${p.builtin}: type parameter ${name.slice(1)} missing`);
|
|
1341
|
+
return this.typeRef(t);
|
|
1342
|
+
}
|
|
1343
|
+
const a = texts[Number(name)];
|
|
1344
|
+
if (a === undefined)
|
|
1345
|
+
throw new Unprintable(`${p.builtin}: argument ${name} missing`);
|
|
1346
|
+
return a;
|
|
1347
|
+
};
|
|
1348
|
+
return this.templateDoc(template, slot);
|
|
1349
|
+
}
|
|
1350
|
+
/**
|
|
1351
|
+
* A spelling template as a document: a method on a slot (`{0}.map({1})`)
|
|
1352
|
+
* is a member-chain segment on that argument, any other call lays its
|
|
1353
|
+
* arguments out as one, and a template that is not a call is filled in
|
|
1354
|
+
* as it stands.
|
|
1355
|
+
*/
|
|
1356
|
+
templateDoc(template, slot) {
|
|
1357
|
+
const call = parseCallTemplate(template);
|
|
1358
|
+
if (call === null)
|
|
1359
|
+
return fill(template, slot);
|
|
1360
|
+
const args = call.args.map(a => fill(a, slot));
|
|
1361
|
+
const method = METHOD_HEAD.exec(call.head);
|
|
1362
|
+
if (method !== null)
|
|
1363
|
+
return this.methodCall(slot(method[1]), method[2], args);
|
|
1364
|
+
return [fill(call.head, slot), callArgs(args)];
|
|
1365
|
+
}
|
|
1366
|
+
/**
|
|
1367
|
+
* A CSV config struct as the options object the surface takes, or `null`
|
|
1368
|
+
* when the config is not a literal: every `some` field becomes a key, a
|
|
1369
|
+
* `none` field is omitted, and an all-`none` config is the omitted argument.
|
|
1370
|
+
*/
|
|
1371
|
+
csvOptions(config) {
|
|
1372
|
+
if (config.type !== "Struct")
|
|
1373
|
+
return null;
|
|
1374
|
+
const entries = [];
|
|
1375
|
+
for (const f of config.value.fields) {
|
|
1376
|
+
const opt = f.value;
|
|
1377
|
+
if (opt.type !== "Variant")
|
|
1378
|
+
return null;
|
|
1379
|
+
if (opt.value.case === "none")
|
|
1380
|
+
continue;
|
|
1381
|
+
if (opt.value.case !== "some")
|
|
1382
|
+
return null;
|
|
1383
|
+
const js = this.jsLiteral(opt.value.value);
|
|
1384
|
+
if (js === null)
|
|
1385
|
+
return null;
|
|
1386
|
+
entries.push(`${objectKey(f.name)}: ${js}`);
|
|
1387
|
+
}
|
|
1388
|
+
return entries.length === 0 ? "" : `{ ${entries.join(", ")} }`;
|
|
1389
|
+
}
|
|
1390
|
+
/** A literal-only IR subtree as a JavaScript value, or `null`. */
|
|
1391
|
+
jsLiteral(node) {
|
|
1392
|
+
if (node.type === "Value")
|
|
1393
|
+
return literal(node.value.value);
|
|
1394
|
+
if (node.type === "NewArray") {
|
|
1395
|
+
const items = node.value.values.map(v => this.jsLiteral(v));
|
|
1396
|
+
return items.some(i => i === null) ? null : `[${items.join(", ")}]`;
|
|
1397
|
+
}
|
|
1398
|
+
if (node.type === "NewDict") {
|
|
1399
|
+
const items = node.value.values.map(e => {
|
|
1400
|
+
const k = this.jsLiteral(e.key);
|
|
1401
|
+
const v = this.jsLiteral(e.value);
|
|
1402
|
+
return k === null || v === null ? null : `[${k}, ${v}]`;
|
|
1403
|
+
});
|
|
1404
|
+
return items.some(i => i === null) ? null : `new Map([${items.join(", ")}])`;
|
|
1405
|
+
}
|
|
1406
|
+
return null;
|
|
1407
|
+
}
|
|
1408
|
+
rawBuiltin(node, scope, pre, depth) {
|
|
1409
|
+
const p = node.value;
|
|
1410
|
+
this.rawBuiltins.add(p.builtin);
|
|
1411
|
+
const args = p.arguments.map((a, i) => i === 0 ? this.tracedExpr(a, scope, pre, depth) : this.expr(a, scope, pre, depth));
|
|
1412
|
+
const tps = p.type_parameters.map(t => this.typeRef(t));
|
|
1413
|
+
return ["East.builtin", callArgs([JSON.stringify(p.builtin), bracket("[", tps, "]"), bracket("[", args, "]"), this.typeRef(p.type)])];
|
|
1414
|
+
}
|
|
1415
|
+
// ── the module ───────────────────────────────────────────────────────
|
|
1416
|
+
module(ir, importFrom) {
|
|
1417
|
+
let root = ir;
|
|
1418
|
+
let consts = [];
|
|
1419
|
+
if (root.type === "Block") {
|
|
1420
|
+
const stmts = [...root.value.statements];
|
|
1421
|
+
consts = stmts.slice(0, -1);
|
|
1422
|
+
root = stmts[stmts.length - 1];
|
|
1423
|
+
}
|
|
1424
|
+
if (root.type !== "Function" && root.type !== "AsyncFunction") {
|
|
1425
|
+
throw new Unprintable(`the root must be a Function or AsyncFunction, got ${root.type}`);
|
|
1426
|
+
}
|
|
1427
|
+
this.varCounter = nextVIndex(ir);
|
|
1428
|
+
this.prepare(ir);
|
|
1429
|
+
// A python artifact's hoisted constants become the body's own consts.
|
|
1430
|
+
const fnDoc = this.functionExpr(root, new Scope(null), consts);
|
|
1431
|
+
// exactly the names the module uses, in one fixed order
|
|
1432
|
+
const names = ["East", "Expr", "variant", "some", "none", "ref", "matrix", ...TYPE_IMPORTS].filter(n => this.used.has(n));
|
|
1433
|
+
const parts = [
|
|
1434
|
+
"// Generated by east-node transpile — East IR printed as the East.function",
|
|
1435
|
+
"// builder surface. Rebuilding this module yields the same IR (normalized).",
|
|
1436
|
+
`import { ${names.join(", ")} } from ${JSON.stringify(importFrom)};`,
|
|
1437
|
+
];
|
|
1438
|
+
for (const specifier of [...this.libraryImports.keys()].sort()) {
|
|
1439
|
+
const roots = [...this.libraryImports.get(specifier)].sort();
|
|
1440
|
+
parts.push(`import { ${roots.join(", ")} } from ${JSON.stringify(specifier)};`);
|
|
1441
|
+
}
|
|
1442
|
+
parts.push("");
|
|
1443
|
+
for (const [name, src] of this.types.values())
|
|
1444
|
+
parts.push(["const ", name, " = ", src, ";"]);
|
|
1445
|
+
if (this.types.size > 0)
|
|
1446
|
+
parts.push("");
|
|
1447
|
+
parts.push(...this.platformDecls);
|
|
1448
|
+
if (this.platformDecls.length > 0)
|
|
1449
|
+
parts.push("");
|
|
1450
|
+
for (const decl of this.hoistedDecls)
|
|
1451
|
+
parts.push(decl, "");
|
|
1452
|
+
parts.push(["export const ", this.rootName, " = ", fnDoc, ";"]);
|
|
1453
|
+
parts.push("");
|
|
1454
|
+
return render(join(hardline, parts), this.width);
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
/** The printer's own spelling for a variable it cannot name as the IR does. */
|
|
1458
|
+
const V_NAME = /^v_(\d+)$/;
|
|
1459
|
+
/** One above the highest `v_N` variable name in `ir` — a minted `v_N` never collides with one the program authored. */
|
|
1460
|
+
function nextVIndex(ir) {
|
|
1461
|
+
let highest = -1;
|
|
1462
|
+
const walk = (v) => {
|
|
1463
|
+
if (Array.isArray(v)) {
|
|
1464
|
+
for (const x of v)
|
|
1465
|
+
walk(x);
|
|
1466
|
+
}
|
|
1467
|
+
else if (v !== null && typeof v === "object" && !(v instanceof Date) && !(v instanceof Uint8Array)) {
|
|
1468
|
+
const node = v;
|
|
1469
|
+
if (node.type === "Variable" && node.value !== null && typeof node.value === "object" && typeof node.value.name === "string") {
|
|
1470
|
+
const m = V_NAME.exec(node.value.name);
|
|
1471
|
+
if (m)
|
|
1472
|
+
highest = Math.max(highest, Number(m[1]));
|
|
1473
|
+
}
|
|
1474
|
+
for (const x of Object.values(v))
|
|
1475
|
+
walk(x);
|
|
1476
|
+
}
|
|
1477
|
+
};
|
|
1478
|
+
walk(ir);
|
|
1479
|
+
return highest + 1;
|
|
1480
|
+
}
|
|
1481
|
+
/** The IR value behind a function expression, an `EastIR`, or an IR value. */
|
|
1482
|
+
function irOf(fnOrIr) {
|
|
1483
|
+
if (fnOrIr instanceof Expr) {
|
|
1484
|
+
const toIR = fnOrIr.toIR;
|
|
1485
|
+
if (typeof toIR !== "function")
|
|
1486
|
+
throw new TypeError("toSource takes a function expression, not a plain expression");
|
|
1487
|
+
return toIR.call(fnOrIr).ir;
|
|
1488
|
+
}
|
|
1489
|
+
const v = fnOrIr;
|
|
1490
|
+
if (v !== null && typeof v === "object" && v.ir !== undefined && typeof v.ir.type === "string") {
|
|
1491
|
+
return v.ir; // an EastIR / AsyncEastIR
|
|
1492
|
+
}
|
|
1493
|
+
if (v !== null && typeof v === "object" && typeof v.type === "string" && "value" in v) {
|
|
1494
|
+
return v;
|
|
1495
|
+
}
|
|
1496
|
+
throw new TypeError("toSource takes an East.function result, an EastIR, or an IR value");
|
|
1497
|
+
}
|
|
1498
|
+
/**
|
|
1499
|
+
* Prints East IR as a TypeScript module that rebuilds it.
|
|
1500
|
+
*
|
|
1501
|
+
* @param fnOrIr - A built `East.function` / `East.asyncFunction` expression,
|
|
1502
|
+
* its `toIR()` result, or a homoiconic IR value (a Function / AsyncFunction
|
|
1503
|
+
* node, or the `Block[Let…, Function]` a python build with hoisted
|
|
1504
|
+
* constants emits — its constants become the body's first `$.const`s)
|
|
1505
|
+
* @param options - The export name (default `main`), the module the
|
|
1506
|
+
* printed source imports from (default `@elaraai/east`), the line width,
|
|
1507
|
+
* and the library modules whose exported declarations spell the platform
|
|
1508
|
+
* calls (`libraries`)
|
|
1509
|
+
* @returns The module source: importing it binds `name` to an
|
|
1510
|
+
* `East.function(...)` whose IR normalizes equal to the input's
|
|
1511
|
+
* @throws {Unprintable} For a shape the TypeScript surface cannot spell — a
|
|
1512
|
+
* statement in expression position, a jump to no loop, a `NewVector` of
|
|
1513
|
+
* expressions, a `finally` on an expression `TryCatch`. Builtins without a
|
|
1514
|
+
* spelling print through `East.builtin(...)` and are never unprintable.
|
|
1515
|
+
*
|
|
1516
|
+
* @example
|
|
1517
|
+
* ```ts
|
|
1518
|
+
* const double = East.function([IntegerType], IntegerType, ($, x) => x.multiply(2n));
|
|
1519
|
+
* console.log(East.toSource(double));
|
|
1520
|
+
* // import { East, Expr, ... } from "@elaraai/east";
|
|
1521
|
+
* //
|
|
1522
|
+
* // export const main = East.function([IntegerType], IntegerType, ($, x) => x.multiply(2n));
|
|
1523
|
+
* ```
|
|
1524
|
+
*/
|
|
1525
|
+
export function toSource(fnOrIr, options = {}) {
|
|
1526
|
+
const printer = new Printer(options.name ?? "main", options.width ?? LINE_WIDTH, librarySpellings(options.libraries ?? {}));
|
|
1527
|
+
return printer.module(irOf(fnOrIr), options.importFrom ?? "@elaraai/east");
|
|
1528
|
+
}
|
|
1529
|
+
//# sourceMappingURL=printer.js.map
|