@ball-lang/compiler 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1742 @@
1
+ /**
2
+ * Ball → TypeScript compiler.
3
+ *
4
+ * Walks a Ball `Program` and emits idiomatic TypeScript via ts-morph.
5
+ * Runs in-process in TS — no Dart subprocess.
6
+ *
7
+ * Mirrors the semantics of the original Dart `ts_compiler.dart`:
8
+ * - Declarations (functions, classes, enums, typedefs) go through
9
+ * ts-morph's structure API — indentation / ordering / formatting
10
+ * handled by the library.
11
+ * - Expressions / statements are emitted as raw TS source strings
12
+ * into an internal buffer. ts-morph re-indents them inside each
13
+ * declaration block.
14
+ */
15
+ import { Project, StructureKind, type Scope as _Scope } from "ts-morph";
16
+ import type {
17
+ Expression,
18
+ FieldValuePair,
19
+ FunctionCall,
20
+ FunctionDef,
21
+ Literal,
22
+ Module,
23
+ Program,
24
+ Statement,
25
+ Struct,
26
+ TypeDefinition,
27
+ } from "./types.ts";
28
+ import { TS_RUNTIME_PREAMBLE } from "./preamble.ts";
29
+
30
+ export interface CompileOptions {
31
+ /** Include the runtime preamble at the top of the output. Default true. */
32
+ includePreamble?: boolean;
33
+ /** Output file path hint (affects ts-morph's internal resolution). */
34
+ fileName?: string;
35
+ }
36
+
37
+ interface CtorParam {
38
+ name: string;
39
+ isThis: boolean;
40
+ isNamed: boolean;
41
+ }
42
+
43
+ export class BallCompiler {
44
+ private readonly program: Program;
45
+
46
+ /** Buffer that _emit* functions write into. */
47
+ private out = "";
48
+ private depth = 0;
49
+
50
+ /** Catch-bound variables currently in scope — subject to bracket access. */
51
+ private readonly catchVars = new Set<string>();
52
+
53
+ /** Fields of the class currently being emitted (method bodies). */
54
+ private currentClassFields: Set<string> = new Set();
55
+
56
+ /** Parameters of the currently-emitting method (shadow fields). */
57
+ private currentMethodParams: Set<string> = new Set();
58
+
59
+ /** Short method names of the current class (for `this.foo()` routing). */
60
+ private currentClassMethodNames: Set<string> = new Set();
61
+
62
+ /** All function names in the entry module (function vs ctor routing). */
63
+ private allFunctionNames: Set<string> = new Set();
64
+
65
+ /** typeDefs in the entry module (typeName → definition). */
66
+ private typeDefByName: Map<string, TypeDefinition> = new Map();
67
+
68
+ constructor(program: Program) {
69
+ this.program = program;
70
+ }
71
+
72
+ /** Compile to TS source. */
73
+ compile(options: CompileOptions = {}): string {
74
+ const { includePreamble = true, fileName = "program.ts" } = options;
75
+ const project = new Project({
76
+ useInMemoryFileSystem: true,
77
+ compilerOptions: { target: 99 /* ESNext */ },
78
+ });
79
+ const sf = project.createSourceFile(fileName, "", { overwrite: true });
80
+
81
+ const entryMod = this.program.modules.find(
82
+ (m) => m.name === this.program.entryModule,
83
+ );
84
+ if (!entryMod) {
85
+ throw new Error(
86
+ `Entry module "${this.program.entryModule}" not found`,
87
+ );
88
+ }
89
+
90
+ // Seed the function-name + typeDef lookup tables.
91
+ this.allFunctionNames = new Set(entryMod.functions.map((f) => f.name));
92
+ this.typeDefByName = new Map(
93
+ (entryMod.typeDefs ?? []).map((td) => [td.name, td]),
94
+ );
95
+
96
+ // Group functions by their enclosing class (if any) — matches the
97
+ // `<typeDef.name>.<member>` naming convention from the encoder.
98
+ const classMembers = new Map<string, FunctionDef[]>();
99
+ const freeFunctions: FunctionDef[] = [];
100
+ for (const fn of entryMod.functions) {
101
+ if (fn.isBase) continue;
102
+ if (fn.name === this.program.entryFunction) continue;
103
+ const enclosing = this.enclosingTypeName(fn.name);
104
+ if (enclosing) {
105
+ const list = classMembers.get(enclosing) ?? [];
106
+ list.push(fn);
107
+ classMembers.set(enclosing, list);
108
+ } else {
109
+ freeFunctions.push(fn);
110
+ }
111
+ }
112
+
113
+ // Typedefs → TsTypeAlias.
114
+ for (const ta of entryMod.typeAliases ?? []) {
115
+ sf.addTypeAlias({
116
+ name: ta.name,
117
+ type: this.dartTypeToTs(ta.targetType),
118
+ isExported: true,
119
+ });
120
+ }
121
+
122
+ // Classes.
123
+ for (const td of entryMod.typeDefs ?? []) {
124
+ this.emitClass(sf, td, classMembers.get(td.name) ?? []);
125
+ }
126
+
127
+ // Top-level variables (kind == 'top_level_variable') emit as
128
+ // `const <name> = <body>;` before free functions.
129
+ for (const fn of freeFunctions.filter(
130
+ (f) => (f.metadata as any)?.kind === "top_level_variable",
131
+ )) {
132
+ const name = sanitize(fn.name);
133
+ const body = fn.body ? this.captureInto(() => {
134
+ this.writeln(`return ${this.expr(fn.body!)};`);
135
+ }) : "undefined";
136
+ sf.addStatements(`const ${name} = (() => { ${body} })();`);
137
+ }
138
+
139
+ // Free top-level functions (exclude top-level variables).
140
+ for (const fn of freeFunctions.filter(
141
+ (f) => (f.metadata as any)?.kind !== "top_level_variable",
142
+ )) {
143
+ this.emitFreeFunction(sf, fn);
144
+ }
145
+
146
+ // Entry function as `main()` + immediate call.
147
+ const entryFn = entryMod.functions.find(
148
+ (f) => f.name === this.program.entryFunction,
149
+ );
150
+ if (entryFn) {
151
+ this.emitFreeFunction(sf, entryFn, "main");
152
+ sf.addStatements("main();");
153
+ }
154
+
155
+ sf.formatText({ indentSize: 2, convertTabsToSpaces: true });
156
+ const body = sf.getFullText();
157
+ return includePreamble ? TS_RUNTIME_PREAMBLE + "\n" + body : body;
158
+ }
159
+
160
+ // ───────────────────────── Declarations ────────────────────────────
161
+
162
+ private emitFreeFunction(
163
+ sf: ReturnType<Project["createSourceFile"]>,
164
+ fn: FunctionDef,
165
+ forceName?: string,
166
+ ): void {
167
+ const params = extractParams(fn);
168
+ const name = forceName ?? sanitize(fn.name);
169
+ const body = this.captureInto(() => {
170
+ if (params.length === 1 && params[0] !== "input") {
171
+ this.writeln(`const input = ${sanitize(params[0])};`);
172
+ }
173
+ if (fn.body) this.emitStatementOrExpression(fn.body, true);
174
+ });
175
+ const isAsync = functionIsAsync(fn);
176
+ sf.addFunction({
177
+ kind: StructureKind.Function,
178
+ name,
179
+ isAsync,
180
+ parameters: params.map((p) => ({ name: sanitize(p), type: "any" })),
181
+ returnType: isAsync ? "Promise<any>" : "any",
182
+ statements: body,
183
+ });
184
+ }
185
+
186
+ private emitClass(
187
+ sf: ReturnType<Project["createSourceFile"]>,
188
+ td: TypeDefinition,
189
+ members: FunctionDef[],
190
+ ): void {
191
+ const meta: Struct = td.metadata ?? {};
192
+ const tsName = classTsName(td.name);
193
+
194
+ // Fields: prefer metadata.fields (richer) with descriptor fallback.
195
+ const fieldSpecs = Array.isArray(meta["fields"])
196
+ ? (meta["fields"] as unknown[])
197
+ : [];
198
+ const properties: Array<{
199
+ name: string;
200
+ type: string;
201
+ rawDartType: string;
202
+ isStatic: boolean;
203
+ isReadonly: boolean;
204
+ }> = [];
205
+ const fieldNames = new Set<string>();
206
+ for (const raw of fieldSpecs) {
207
+ if (raw == null || typeof raw !== "object") continue;
208
+ const r = raw as Record<string, unknown>;
209
+ const fname = typeof r.name === "string" ? r.name : undefined;
210
+ if (!fname) continue;
211
+ fieldNames.add(fname);
212
+ properties.push({
213
+ name: fname,
214
+ type: typeof r.type === "string" ? this.dartTypeToTs(r.type) : "any",
215
+ rawDartType: typeof r.type === "string" ? r.type : "",
216
+ isStatic: r.is_static === true,
217
+ isReadonly: r.is_final === true,
218
+ });
219
+ }
220
+ if (properties.length === 0 && td.descriptor?.field) {
221
+ for (const f of td.descriptor.field) {
222
+ fieldNames.add(f.name);
223
+ properties.push({ name: f.name, type: "any", rawDartType: "", isStatic: false, isReadonly: false });
224
+ }
225
+ }
226
+
227
+ // Method-name set for `this.foo()` routing inside bodies.
228
+ // Exclude static fields — they're emitted as module-level consts
229
+ // and referenced WITHOUT `this.`.
230
+ const methodNames = new Set<string>();
231
+ const staticFieldNames = new Set<string>();
232
+ for (const fn of members) {
233
+ const mMeta: Struct = fn.metadata ?? {};
234
+ if ((mMeta as any).kind === "static_field") {
235
+ staticFieldNames.add(memberShortName(fn.name));
236
+ } else {
237
+ methodNames.add(memberShortName(fn.name));
238
+ }
239
+ }
240
+ const savedClassMethods = this.currentClassMethodNames;
241
+ const deferredStaticFields: string[] = [];
242
+ this.currentClassMethodNames = methodNames;
243
+
244
+ const hasExtends = typeof meta["superclass"] === "string";
245
+
246
+ const ctors: Array<{ parameters: Array<{ name: string; type: string }>; statements: string }> = [];
247
+ const methods: Array<{
248
+ name: string;
249
+ isAsync: boolean;
250
+ isStatic: boolean;
251
+ parameters: Array<{ name: string; type: string }>;
252
+ returnType: string;
253
+ statements: string;
254
+ }> = [];
255
+ const getters: Array<{ name: string; isStatic: boolean; returnType: string; statements: string }> = [];
256
+ const setters: Array<{
257
+ name: string;
258
+ isStatic: boolean;
259
+ parameters: Array<{ name: string; type: string }>;
260
+ statements: string;
261
+ }> = [];
262
+
263
+ for (const fn of members) {
264
+ const mMeta: Struct = fn.metadata ?? {};
265
+ const kind = typeof mMeta["kind"] === "string" ? mMeta["kind"] : "method";
266
+ if (kind === "constructor") {
267
+ // Named ctors become static factory methods; default `.new`
268
+ // becomes the real ctor.
269
+ const lastDot = fn.name.lastIndexOf(".");
270
+ const rawShort = lastDot < 0 ? fn.name : fn.name.slice(lastDot + 1);
271
+ if (rawShort === "new") {
272
+ ctors.push(this.buildCtor(fn, mMeta, fieldNames, hasExtends));
273
+ } else {
274
+ methods.push(
275
+ this.buildMethod(fn, { ...mMeta, is_static: true }, fieldNames),
276
+ );
277
+ }
278
+ } else if (mMeta["is_getter"] === true) {
279
+ getters.push(this.buildGetter(fn, mMeta, fieldNames));
280
+ } else if (mMeta["is_setter"] === true) {
281
+ setters.push(this.buildSetter(fn, mMeta, fieldNames));
282
+ } else if (kind === "static_field") {
283
+ // Static field → module-level const emitted BEFORE the class
284
+ // so it's accessible as a bare name inside instance methods
285
+ // (matching Dart's behavior where static fields are visible
286
+ // without qualification). We capture the initializer body
287
+ // and emit it above the class via a deferred statement.
288
+ const sfName = memberShortName(fn.name);
289
+ const initBody = fn.body ? this.expr(fn.body) : "undefined";
290
+ deferredStaticFields.push(`const ${sfName} = ${initBody};`);
291
+ } else {
292
+ methods.push(this.buildMethod(fn, mMeta, fieldNames));
293
+ }
294
+ }
295
+
296
+ this.currentClassMethodNames = savedClassMethods;
297
+
298
+ // Inheritance.
299
+ const superName =
300
+ typeof meta["superclass"] === "string" ? meta["superclass"] : undefined;
301
+ const interfaces = Array.isArray(meta["interfaces"])
302
+ ? (meta["interfaces"] as unknown[])
303
+ .filter((i): i is string => typeof i === "string")
304
+ : undefined;
305
+ const tsInterfaces = interfaces
306
+ ?.filter((i) => i !== "Exception")
307
+ .map((i) => this.dartTypeToTs(i));
308
+
309
+ // Static fields are emitted as module-level constants before the
310
+ // class so they're accessible without qualification.
311
+ for (const stmt of deferredStaticFields) {
312
+ sf.addStatements(stmt);
313
+ }
314
+
315
+ sf.addClass({
316
+ name: tsName,
317
+ isExported: true,
318
+ isAbstract: meta["is_abstract"] === true,
319
+ extends: superName ? this.dartTypeToTs(superName) : undefined,
320
+ implements: tsInterfaces && tsInterfaces.length > 0 ? tsInterfaces : undefined,
321
+ properties: properties.map((p) => ({
322
+ name: p.name,
323
+ type: p.type,
324
+ isStatic: p.isStatic,
325
+ isReadonly: p.isReadonly,
326
+ initializer: defaultInitializer(p.type, p.rawDartType),
327
+ })),
328
+ ctors,
329
+ methods,
330
+ getAccessors: getters,
331
+ setAccessors: setters,
332
+ });
333
+ }
334
+
335
+ private buildCtor(
336
+ fn: FunctionDef,
337
+ meta: Struct,
338
+ classFields: Set<string>,
339
+ hasExtends: boolean,
340
+ ): { parameters: Array<{ name: string; type: string }>; statements: string } {
341
+ const rawParams = extractCtorParams(meta);
342
+ // Dart constructors may mix positional + named params. When named
343
+ // params exist AND there are also positional params, callers may
344
+ // pass named params as a trailing object `{label: x, value: y}`.
345
+ //
346
+ // Emit ALL params as positional but add a prologue that tries to
347
+ // destructure the LAST arg as a named-params object when there's
348
+ // a mix. This handles both calling conventions:
349
+ // new Foo('a', {label: 'b'}) → named destructured
350
+ // new Foo('a', 'b', 'c') → positional passthrough
351
+ const positionalParams = rawParams.filter((p) => !p.isNamed);
352
+ const namedParams = rawParams.filter((p) => p.isNamed);
353
+ const parameters = rawParams.map((p) => ({ name: sanitize(p.name), type: "any" }));
354
+ const prologueParts: string[] = [];
355
+ if (hasExtends) prologueParts.push("super();");
356
+ // If there are named params AND the last positional+1 arg is an
357
+ // object, destructure named params from it (handles the encoder's
358
+ // MessageCreation calling convention where named args are packed).
359
+ if (positionalParams.length > 0 && namedParams.length > 0) {
360
+ // First named param slot might contain a {named args} object.
361
+ // Detect and destructure if so.
362
+ const firstNamedName = sanitize(namedParams[0].name);
363
+ prologueParts.push(
364
+ `if (typeof ${firstNamedName} === 'object' && ${firstNamedName} !== null && !Array.isArray(${firstNamedName}) && (` +
365
+ namedParams.map((p) => `'${p.name}' in ${firstNamedName}`).join(" || ") +
366
+ `)) { let __n = ${firstNamedName}; ` +
367
+ namedParams.map((p) => `${sanitize(p.name)} = __n.${p.name}`).join("; ") +
368
+ `; }`
369
+ );
370
+ }
371
+ for (const p of rawParams) {
372
+ if (p.isThis || classFields.has(p.name)) {
373
+ prologueParts.push(`this.${p.name} = ${sanitize(p.name)};`);
374
+ }
375
+ }
376
+ const prologue = prologueParts.join("\n");
377
+ const captured = this.withMethodContext(
378
+ new Set(rawParams.map((p) => p.name)),
379
+ classFields,
380
+ () =>
381
+ this.captureInto(() => {
382
+ if (fn.body) this.emitStatementOrExpression(fn.body, false);
383
+ }),
384
+ );
385
+ const body = prologue === "" ? captured : captured === "" ? prologue : `${prologue}\n${captured}`;
386
+ return { parameters, statements: body };
387
+ }
388
+
389
+ private buildMethod(fn: FunctionDef, meta: Struct, classFields: Set<string>) {
390
+ const params = extractParams(fn);
391
+ const body = this.withMethodContext(
392
+ new Set(params),
393
+ classFields,
394
+ () =>
395
+ this.captureInto(() => {
396
+ if (params.length === 1 && params[0] !== "input") {
397
+ this.writeln(`const input = ${sanitize(params[0])};`);
398
+ }
399
+ if (fn.body) this.emitStatementOrExpression(fn.body, true);
400
+ }),
401
+ );
402
+ const isAsync = functionIsAsync(fn);
403
+ return {
404
+ name: memberShortName(fn.name),
405
+ isAsync,
406
+ isStatic: meta["is_static"] === true,
407
+ parameters: params.map((p) => ({ name: sanitize(p), type: "any" })),
408
+ returnType: isAsync ? "Promise<any>" : "any",
409
+ statements: body,
410
+ };
411
+ }
412
+
413
+ private buildGetter(fn: FunctionDef, meta: Struct, classFields: Set<string>) {
414
+ const body = this.withMethodContext(
415
+ new Set<string>(),
416
+ classFields,
417
+ () =>
418
+ this.captureInto(() => {
419
+ if (fn.body) this.emitStatementOrExpression(fn.body, true);
420
+ }),
421
+ );
422
+ return {
423
+ name: memberShortName(fn.name),
424
+ isStatic: meta["is_static"] === true,
425
+ returnType: "any",
426
+ statements: body,
427
+ };
428
+ }
429
+
430
+ private buildSetter(fn: FunctionDef, meta: Struct, classFields: Set<string>) {
431
+ const params = extractParams(fn);
432
+ const body = this.withMethodContext(
433
+ new Set(params),
434
+ classFields,
435
+ () =>
436
+ this.captureInto(() => {
437
+ if (fn.body) this.emitStatementOrExpression(fn.body, false);
438
+ }),
439
+ );
440
+ return {
441
+ name: memberShortName(fn.name),
442
+ isStatic: meta["is_static"] === true,
443
+ parameters: params.map((p) => ({ name: sanitize(p), type: "any" })),
444
+ statements: body,
445
+ };
446
+ }
447
+
448
+ // ───────────────────────── Buffer helpers ──────────────────────────
449
+
450
+ private captureInto(body: () => void): string {
451
+ const savedOut = this.out;
452
+ const savedDepth = this.depth;
453
+ this.out = "";
454
+ this.depth = 0;
455
+ try {
456
+ body();
457
+ return this.out.replace(/\s+$/, "");
458
+ } finally {
459
+ this.out = savedOut;
460
+ this.depth = savedDepth;
461
+ }
462
+ }
463
+
464
+ private writeln(s: string): void {
465
+ this.out += " ".repeat(this.depth) + s + "\n";
466
+ }
467
+
468
+ private get ind(): string {
469
+ return " ".repeat(this.depth);
470
+ }
471
+
472
+ private withMethodContext<T>(
473
+ params: Set<string>,
474
+ fields: Set<string>,
475
+ fn: () => T,
476
+ ): T {
477
+ const sParams = this.currentMethodParams;
478
+ const sFields = this.currentClassFields;
479
+ this.currentMethodParams = params;
480
+ this.currentClassFields = fields;
481
+ try {
482
+ return fn();
483
+ } finally {
484
+ this.currentMethodParams = sParams;
485
+ this.currentClassFields = sFields;
486
+ }
487
+ }
488
+
489
+ // ───────────────────────── Statements ──────────────────────────────
490
+
491
+ private emitStatementOrExpression(
492
+ expr: Expression,
493
+ isFunctionBody: boolean,
494
+ ): void {
495
+ // Bare expression used as a function body → emit `return`.
496
+ if (isFunctionBody && !expr.block) {
497
+ this.writeln(`return ${this.expr(expr)};`);
498
+ return;
499
+ }
500
+ if (expr.block) {
501
+ this.emitBlock(expr.block, isFunctionBody);
502
+ return;
503
+ }
504
+ if (expr.call && this.isControlFlow(expr.call)) {
505
+ this.emitControlFlowStatement(expr.call);
506
+ return;
507
+ }
508
+ this.writeln(`${this.expr(expr)};`);
509
+ }
510
+
511
+ private emitBlock(block: NonNullable<Expression["block"]>, isFunctionBody: boolean): void {
512
+ for (const s of block.statements ?? []) this.emitStatement(s);
513
+ if (block.result !== undefined && isFunctionBody) {
514
+ const r = block.result;
515
+ // notSet literal → skip.
516
+ const isNotSet =
517
+ r.literal !== undefined &&
518
+ r.literal.intValue === undefined &&
519
+ r.literal.doubleValue === undefined &&
520
+ r.literal.stringValue === undefined &&
521
+ r.literal.boolValue === undefined &&
522
+ r.literal.listValue === undefined &&
523
+ r.literal.bytesValue === undefined;
524
+ if (!isNotSet) this.writeln(`return ${this.expr(r)};`);
525
+ } else if (block.result !== undefined) {
526
+ this.writeln(`${this.expr(block.result)};`);
527
+ }
528
+ }
529
+
530
+ private emitStatement(stmt: Statement): void {
531
+ if (stmt.let) {
532
+ const meta: Struct = stmt.let.metadata ?? {};
533
+ const keyword = typeof meta["keyword"] === "string" ? meta["keyword"] : "final";
534
+ // Use `let` for all declarations — we can't reliably determine
535
+ // if a variable is ever reassigned without whole-method analysis,
536
+ // and Dart's `final` guarantee doesn't help when the compiled
537
+ // output re-assigns in patterns the encoder generates (e.g.
538
+ // _tryOperatorOverride's `left = input['left']`).
539
+ const kw = "let";
540
+ const name = sanitize(stmt.let.name);
541
+ if (stmt.let.value !== undefined) {
542
+ this.writeln(`${kw} ${name} = ${this.expr(stmt.let.value)};`);
543
+ } else {
544
+ this.writeln(`${kw} ${name};`);
545
+ }
546
+ return;
547
+ }
548
+ if (stmt.expression) {
549
+ const e = stmt.expression;
550
+ if (e.call && this.isControlFlow(e.call)) {
551
+ this.emitControlFlowStatement(e.call);
552
+ return;
553
+ }
554
+ // Block-expression-as-statement with no result: hoist inner stmts.
555
+ if (e.block && e.block.result === undefined) {
556
+ for (const inner of e.block.statements ?? []) this.emitStatement(inner);
557
+ return;
558
+ }
559
+ this.writeln(`${this.expr(e)};`);
560
+ }
561
+ }
562
+
563
+ // ───────────────────────── Control flow ────────────────────────────
564
+
565
+ private isControlFlow(call: FunctionCall): boolean {
566
+ const kinds = new Set([
567
+ "if", "for", "for_in", "while", "do_while", "try",
568
+ "return", "break", "continue", "labeled", "throw", "rethrow",
569
+ "assign", "switch", "switch_expr",
570
+ ]);
571
+ if (!kinds.has(call.function)) return false;
572
+ // Accept both explicit std module AND empty module (the encoder
573
+ // sometimes omits the module for control-flow operations).
574
+ return isStd(call.module) || !call.module;
575
+ }
576
+
577
+ private emitControlFlowStatement(call: FunctionCall): void {
578
+ switch (call.function) {
579
+ case "if": this.emitIfStmt(call); break;
580
+ case "for": this.emitForStmt(call); break;
581
+ case "for_in": this.emitForInStmt(call); break;
582
+ case "while": this.emitWhileStmt(call); break;
583
+ case "do_while": this.emitDoWhileStmt(call); break;
584
+ case "try": this.emitTryStmt(call); break;
585
+ case "return": {
586
+ const v = field(call, "value");
587
+ this.writeln(v ? `return ${this.expr(v)};` : "return;");
588
+ break;
589
+ }
590
+ case "break": {
591
+ const label = stringField(call, "label");
592
+ this.writeln(label ? `break ${label};` : "break;");
593
+ break;
594
+ }
595
+ case "continue": {
596
+ const label = stringField(call, "label");
597
+ this.writeln(label ? `continue ${label};` : "continue;");
598
+ break;
599
+ }
600
+ case "labeled": {
601
+ const label = stringField(call, "label");
602
+ this.writeln(`${label}:`);
603
+ const body = field(call, "body");
604
+ if (body) this.emitStatementOrExpression(body, false);
605
+ break;
606
+ }
607
+ case "throw": {
608
+ const v = field(call, "value");
609
+ if (v) {
610
+ const str = this.compileThrowValue(v) ?? this.expr(v);
611
+ this.writeln(`throw ${str};`);
612
+ } else {
613
+ this.writeln("throw null;");
614
+ }
615
+ break;
616
+ }
617
+ case "rethrow":
618
+ this.writeln("throw __ball_active_error;");
619
+ break;
620
+ case "assign":
621
+ this.emitAssignStmt(call);
622
+ break;
623
+ case "switch":
624
+ case "switch_expr": {
625
+ // When a switch appears as a STATEMENT (not expression), emit
626
+ // as an if/else chain so each case can `return` independently
627
+ // without the ternary's default arm causing an early exit.
628
+ const subjectExpr = field(call, "subject");
629
+ const casesField = field(call, "cases");
630
+ if (!subjectExpr || !casesField) {
631
+ this.writeln("/* malformed switch */");
632
+ break;
633
+ }
634
+ const subjectStr = this.expr(subjectExpr);
635
+ this.writeln(`{ const __sw = ${subjectStr};`);
636
+ this.depth++;
637
+ const caseExprs = casesField.literal?.listValue?.elements ?? [];
638
+ let defaultBody: Expression | undefined;
639
+ let first = true;
640
+ // Parse all cases, detecting fall-through (empty body = merge
641
+ // with next case via ||).
642
+ const parsedCases: Array<{ conds: string[]; body?: Expression }> = [];
643
+ const pendingConds: string[] = [];
644
+ for (const ce of caseExprs) {
645
+ if (!ce.messageCreation) continue;
646
+ let pattern: Expression | undefined;
647
+ let body: Expression | undefined;
648
+ for (const fd of ce.messageCreation.fields ?? []) {
649
+ if (fd.name === "pattern") pattern = fd.value;
650
+ if (fd.name === "body") body = fd.value;
651
+ }
652
+ if (!pattern) { defaultBody = body; continue; }
653
+ const patText = patternLiteralText(pattern);
654
+ const cond = patText !== undefined
655
+ ? patternToTsCondition(patText, "__sw")
656
+ : `((__sw) === ${this.expr(pattern)})`;
657
+ if (cond === "true") { defaultBody = body; break; }
658
+ // Empty body = fall-through: accumulate conditions.
659
+ const isEmpty = body && body.block &&
660
+ (body.block.statements ?? []).length === 0 &&
661
+ body.block.result === undefined;
662
+ if (!body || isEmpty) {
663
+ pendingConds.push(cond);
664
+ continue;
665
+ }
666
+ pendingConds.push(cond);
667
+ parsedCases.push({ conds: [...pendingConds], body });
668
+ pendingConds.length = 0;
669
+ }
670
+ for (const pc of parsedCases) {
671
+ const combinedCond = pc.conds.join(" || ");
672
+ const kw = first ? "if" : "else if";
673
+ this.writeln(`${kw} (${combinedCond}) {`);
674
+ this.depth++;
675
+ this.emitStatementOrExpression(pc.body!, false);
676
+ this.depth--;
677
+ this.writeln("}");
678
+ first = false;
679
+ }
680
+ if (defaultBody) {
681
+ if (!first) {
682
+ this.writeln("else {");
683
+ this.depth++;
684
+ }
685
+ this.emitStatementOrExpression(defaultBody, false);
686
+ if (!first) {
687
+ this.depth--;
688
+ this.writeln("}");
689
+ }
690
+ }
691
+ this.depth--;
692
+ this.writeln("}");
693
+ break;
694
+ }
695
+ }
696
+ }
697
+
698
+ private emitIfStmt(call: FunctionCall): void {
699
+ const cond = field(call, "condition");
700
+ const then_ = field(call, "then");
701
+ const else_ = field(call, "else");
702
+ this.writeln(`if (${this.expr(cond!)}) {`);
703
+ this.depth++;
704
+ if (then_) this.emitStatementOrExpression(then_, false);
705
+ this.depth--;
706
+ if (else_) {
707
+ this.writeln(`} else {`);
708
+ this.depth++;
709
+ this.emitStatementOrExpression(else_, false);
710
+ this.depth--;
711
+ }
712
+ this.writeln(`}`);
713
+ }
714
+
715
+ private emitForStmt(call: FunctionCall): void {
716
+ const init = field(call, "init");
717
+ const cond = field(call, "condition");
718
+ const update = field(call, "update");
719
+ const body = field(call, "body");
720
+ let initStr: string;
721
+ if (init && init.literal?.stringValue !== undefined) {
722
+ initStr = translateInitString(init.literal.stringValue);
723
+ } else if (init) {
724
+ initStr = this.expr(init);
725
+ } else {
726
+ initStr = "";
727
+ }
728
+ const condStr = cond ? this.expr(cond) : "";
729
+ const updateStr = update ? this.expr(update) : "";
730
+ this.writeln(`for (${initStr}; ${condStr}; ${updateStr}) {`);
731
+ this.depth++;
732
+ if (body) this.emitStatementOrExpression(body, false);
733
+ this.depth--;
734
+ this.writeln(`}`);
735
+ }
736
+
737
+ private emitForInStmt(call: FunctionCall): void {
738
+ const variable = stringField(call, "variable") ?? "item";
739
+ const iterable = field(call, "iterable")!;
740
+ const body = field(call, "body");
741
+ this.writeln(`for (const ${variable} of ${this.expr(iterable)}) {`);
742
+ this.depth++;
743
+ if (body) this.emitStatementOrExpression(body, false);
744
+ this.depth--;
745
+ this.writeln(`}`);
746
+ }
747
+
748
+ private emitWhileStmt(call: FunctionCall): void {
749
+ const cond = field(call, "condition");
750
+ const body = field(call, "body");
751
+ this.writeln(`while (${this.expr(cond!)}) {`);
752
+ this.depth++;
753
+ if (body) this.emitStatementOrExpression(body, false);
754
+ this.depth--;
755
+ this.writeln(`}`);
756
+ }
757
+
758
+ private emitDoWhileStmt(call: FunctionCall): void {
759
+ const cond = field(call, "condition");
760
+ const body = field(call, "body");
761
+ this.writeln(`do {`);
762
+ this.depth++;
763
+ if (body) this.emitStatementOrExpression(body, false);
764
+ this.depth--;
765
+ this.writeln(`} while (${this.expr(cond!)});`);
766
+ }
767
+
768
+ private emitTryStmt(call: FunctionCall): void {
769
+ const body = field(call, "body");
770
+ const catches = field(call, "catches");
771
+ const fin = field(call, "finally");
772
+
773
+ this.writeln(`try {`);
774
+ this.depth++;
775
+ if (body) this.emitStatementOrExpression(body, false);
776
+ this.depth--;
777
+
778
+ this.writeln(`} catch (__ball_active_error) {`);
779
+ this.depth++;
780
+ if (catches && catches.literal?.listValue) {
781
+ const clauses = catches.literal.listValue.elements ?? [];
782
+ let first = true;
783
+ let untypedBody: Expression | undefined;
784
+ let untypedVar = "e";
785
+ let untypedStackVar: string | undefined;
786
+ for (const ce of clauses) {
787
+ if (!ce.messageCreation) continue;
788
+ const cf = fieldMap(ce.messageCreation.fields ?? []);
789
+ const type = stringFieldVal(cf, "type");
790
+ const variable = stringFieldVal(cf, "variable") ?? "e";
791
+ const stackVar = stringFieldVal(cf, "stack_trace");
792
+ const cbody = cf.get("body");
793
+ if (!type) {
794
+ untypedBody = cbody;
795
+ untypedVar = variable;
796
+ untypedStackVar = stackVar;
797
+ continue;
798
+ }
799
+ const cond = this.typedCatchCondition(type);
800
+ const keyword = first ? "if" : "else if";
801
+ this.writeln(`${keyword} (${cond}) {`);
802
+ this.depth++;
803
+ this.writeln(`const ${variable} = __ball_active_error;`);
804
+ if (stackVar) {
805
+ this.writeln(
806
+ `const ${stackVar} = (__ball_active_error instanceof Error && __ball_active_error.stack != null ? __ball_active_error.stack : (new Error().stack ?? ''));`,
807
+ );
808
+ }
809
+ const treatAsMap = !this.typeIsUserDefinedClass(`main:${type}`) &&
810
+ !this.typeIsUserDefinedClass(type);
811
+ if (treatAsMap) this.catchVars.add(variable);
812
+ if (cbody) this.emitStatementOrExpression(cbody, false);
813
+ if (treatAsMap) this.catchVars.delete(variable);
814
+ this.depth--;
815
+ this.writeln(`}`);
816
+ first = false;
817
+ }
818
+ if (!first) this.writeln(`else {`);
819
+ if (!first) this.depth++;
820
+ if (untypedBody) {
821
+ this.writeln(`const ${untypedVar} = __ball_active_error;`);
822
+ if (untypedStackVar) {
823
+ this.writeln(
824
+ `const ${untypedStackVar} = (__ball_active_error instanceof Error && __ball_active_error.stack != null ? __ball_active_error.stack : (new Error().stack ?? ''));`,
825
+ );
826
+ }
827
+ this.catchVars.add(untypedVar);
828
+ this.emitStatementOrExpression(untypedBody, false);
829
+ this.catchVars.delete(untypedVar);
830
+ } else {
831
+ this.writeln(`throw __ball_active_error;`);
832
+ }
833
+ if (!first) {
834
+ this.depth--;
835
+ this.writeln(`}`);
836
+ }
837
+ } else {
838
+ this.writeln(`throw __ball_active_error;`);
839
+ }
840
+ this.depth--;
841
+
842
+ if (fin) {
843
+ this.writeln(`} finally {`);
844
+ this.depth++;
845
+ this.emitStatementOrExpression(fin, false);
846
+ this.depth--;
847
+ }
848
+ this.writeln(`}`);
849
+ }
850
+
851
+ private emitAssignStmt(call: FunctionCall): void {
852
+ const target = field(call, "target");
853
+ const value = field(call, "value");
854
+ if (!target || !value) return;
855
+ const op = stringField(call, "op") || "=";
856
+ this.writeln(`${this.expr(target)} ${op} ${this.expr(value)};`);
857
+ }
858
+
859
+ private typedCatchCondition(type: string): string {
860
+ const builtins = new Set([
861
+ "Error", "TypeError", "RangeError", "SyntaxError",
862
+ "ReferenceError", "URIError", "EvalError",
863
+ ]);
864
+ if (builtins.has(type)) return `__ball_active_error instanceof ${type}`;
865
+ if (type === "FormatException") {
866
+ return "(__ball_active_error instanceof Error && __ball_active_error.message.startsWith('FormatException'))";
867
+ }
868
+ return `(__ball_active_error instanceof ${type} || (typeof __ball_active_error === 'object' && __ball_active_error !== null && __ball_active_error['__type'] === '${type}'))`;
869
+ }
870
+
871
+ // ───────────────────────── Expressions ─────────────────────────────
872
+
873
+ private expr(e: Expression): string {
874
+ if (e.call) return this.compileCall(e.call);
875
+ if (e.literal) return this.compileLiteral(e.literal);
876
+ if (e.reference) {
877
+ const name = e.reference.name;
878
+ if (name === "this") return "this";
879
+ // Inside a class method: bare references to fields need this.
880
+ // prefix. Method references also need .bind(this) because Dart
881
+ // tear-offs auto-bind but JS method references do not.
882
+ if (!this.currentMethodParams.has(name)) {
883
+ if (this.currentClassFields.has(name)) {
884
+ return `this.${sanitize(name)}`;
885
+ }
886
+ if (this.currentClassMethodNames.has(name)) {
887
+ return `this.${sanitize(name)}.bind(this)`;
888
+ }
889
+ }
890
+ return sanitize(name);
891
+ }
892
+ if (e.fieldAccess) return this.compileFieldAccess(e.fieldAccess);
893
+ if (e.messageCreation) return this.compileMessageCreation(e.messageCreation);
894
+ if (e.block) return this.compileBlockExpression(e.block);
895
+ if (e.lambda) return this.compileLambda(e.lambda);
896
+ return "null /* notSet */";
897
+ }
898
+
899
+ private compileLiteral(lit: Literal): string {
900
+ if (lit.intValue !== undefined) return String(lit.intValue);
901
+ if (lit.doubleValue !== undefined) return String(lit.doubleValue);
902
+ if (lit.stringValue !== undefined) return jsStringLiteral(lit.stringValue);
903
+ if (lit.boolValue !== undefined) return lit.boolValue ? "true" : "false";
904
+ if (lit.listValue) {
905
+ const parts = (lit.listValue.elements ?? []).map((x) => this.expr(x));
906
+ return `[${parts.join(", ")}]`;
907
+ }
908
+ if (lit.bytesValue !== undefined) return "/* bytes */ new Uint8Array()";
909
+ return "null";
910
+ }
911
+
912
+ private compileFieldAccess(fa: NonNullable<Expression["fieldAccess"]>): string {
913
+ const obj = this.expr(fa.object);
914
+ const f = fa.field;
915
+ if (f === "length") return `${obj}.length`;
916
+ // Positional record field: `.$1` / `.$2` → [0] / [1].
917
+ const recMatch = /^\$(\d+)$/.exec(f);
918
+ if (recMatch) {
919
+ const idx = parseInt(recMatch[1], 10) - 1;
920
+ return `${obj}[${idx}]`;
921
+ }
922
+ if (
923
+ fa.object.reference !== undefined &&
924
+ this.catchVars.has(fa.object.reference.name)
925
+ ) {
926
+ return `${obj}['${f}']`;
927
+ }
928
+ return `${obj}.${f}`;
929
+ }
930
+
931
+ private compileMessageCreation(
932
+ mc: NonNullable<Expression["messageCreation"]>,
933
+ ): string {
934
+ const tn = mc.typeName ?? "";
935
+ const fields = mc.fields ?? [];
936
+ if (tn === "") {
937
+ const entries = fields
938
+ .map((f) => `'${f.name}': ${this.expr(f.value)}`)
939
+ .join(", ");
940
+ return `{${entries}}`;
941
+ }
942
+
943
+ // Function call encoded as MessageCreation: `foo()` / `this.foo()`
944
+ // with no explicit receiver. typeName = function qualified name.
945
+ const shortName = memberShortName(tn);
946
+ if (this.allFunctionNames.has(tn)) {
947
+ const args = this.extractPositionalAndNamed(fields);
948
+ if (this.currentClassMethodNames.has(shortName)) {
949
+ return `this.${shortName}(${args})`;
950
+ }
951
+ return `${shortName}(${args})`;
952
+ }
953
+ if (this.currentClassMethodNames.has(shortName)) {
954
+ const args = this.extractPositionalAndNamed(fields);
955
+ return `this.${shortName}(${args})`;
956
+ }
957
+
958
+ // User-defined class → `new X(...)`.
959
+ if (this.typeIsUserDefinedClass(tn)) {
960
+ const args = this.extractPositionalAndNamed(fields);
961
+ return `new ${classTsName(tn)}(${args})`;
962
+ }
963
+
964
+ // Dart/JS built-in constructors: RegExp, Map, Set, Error, etc.
965
+ // The encoder emits these as MessageCreation with typeName
966
+ // including the module prefix (e.g., 'main:RegExp'). Strip the
967
+ // prefix and emit as native constructors.
968
+ const builtinCtors = new Set([
969
+ "RegExp", "Map", "Set", "Error", "TypeError", "RangeError",
970
+ "DateTime", "Duration", "Uri", "BigInt", "Int64",
971
+ ]);
972
+ const shortTn = classTsName(tn);
973
+ if (builtinCtors.has(shortTn)) {
974
+ const args = this.extractPositionalAndNamed(fields);
975
+ return `new ${shortTn}(${args})`;
976
+ }
977
+
978
+ // Fallback: tagged object literal.
979
+ const entries = [
980
+ `'__type': '${tn}'`,
981
+ ...fields.map((f) => `'${f.name}': ${this.expr(f.value)}`),
982
+ ].join(", ");
983
+ return `{${entries}}`;
984
+ }
985
+
986
+ private extractPositionalAndNamed(fields: FieldValuePair[]): string {
987
+ const positional: string[] = [];
988
+ const named: Array<[string, string]> = [];
989
+ const argRe = /^arg(\d+)$/;
990
+ for (const f of fields) {
991
+ if (f.name === "__type_args__" || f.name === "__const__") continue;
992
+ if (argRe.test(f.name)) {
993
+ positional.push(this.expr(f.value));
994
+ } else {
995
+ named.push([f.name, this.expr(f.value)]);
996
+ }
997
+ }
998
+ const parts = [
999
+ ...positional,
1000
+ ...(named.length > 0
1001
+ ? [`{ ${named.map(([k, v]) => `${k}: ${v}`).join(", ")} }`]
1002
+ : []),
1003
+ ];
1004
+ return parts.join(", ");
1005
+ }
1006
+
1007
+ private typeIsUserDefinedClass(tn: string): boolean {
1008
+ return tn !== "" && this.typeDefByName.has(tn);
1009
+ }
1010
+
1011
+ private compileBlockExpression(block: NonNullable<Expression["block"]>): string {
1012
+ const innerText = this.captureInto(() => {
1013
+ this.writeln(""); // leading newline
1014
+ for (const s of block.statements ?? []) this.emitStatement(s);
1015
+ if (block.result !== undefined) {
1016
+ this.writeln(`return ${this.expr(block.result)};`);
1017
+ }
1018
+ }) + "\n";
1019
+ const usesAwait = containsBareKeyword(innerText, "await");
1020
+ const usesYield = containsBareKeyword(innerText, "yield");
1021
+ if (usesAwait) return `(await (async () => {${innerText}})())`;
1022
+ if (usesYield) return `(yield* (function* () {${innerText}})())`;
1023
+ return `(() => {${innerText}})()`;
1024
+ }
1025
+
1026
+ private compileLambda(fn: FunctionDef): string {
1027
+ const params = extractParams(fn);
1028
+ const paramList = params.map(sanitize).join(", ");
1029
+ const innerText = this.captureInto(() => {
1030
+ this.writeln("");
1031
+ if (params.length === 1 && params[0] !== "input") {
1032
+ this.writeln(`const input = ${sanitize(params[0])};`);
1033
+ }
1034
+ if (fn.body) this.emitStatementOrExpression(fn.body, true);
1035
+ }) + "\n";
1036
+ const isAsync = functionIsAsync(fn) || containsBareKeyword(innerText, "await");
1037
+ const isGenerator = containsBareKeyword(innerText, "yield");
1038
+ if (isGenerator) {
1039
+ return `(function* (${paramList}) {${innerText}})`;
1040
+ }
1041
+ return `(${isAsync ? "async " : ""}(${paramList}) => {${innerText}})`;
1042
+ }
1043
+
1044
+ // ───────────────────────── Calls ──────────────────────────────────
1045
+
1046
+ private compileCall(call: FunctionCall): string {
1047
+ const emptyModuleStd = new Set([
1048
+ "labeled", "paren", "switch_expr", "set_create", "map_create",
1049
+ "yield_each", "rethrow", "assert",
1050
+ ]);
1051
+ if (
1052
+ isStd(call.module) ||
1053
+ ((call.module === undefined || call.module === "") && emptyModuleStd.has(call.function))
1054
+ ) {
1055
+ return this.compileStdCall(call);
1056
+ }
1057
+ const fn = sanitize(call.function);
1058
+ // Prefix with this. when the function name is a class method OR
1059
+ // a class field holding a callable (like `stdout` which is a
1060
+ // void Function(String) field). Both need this. in TS since Dart
1061
+ // resolves implicitly.
1062
+ const thisPrefix =
1063
+ !this.currentMethodParams.has(call.function) &&
1064
+ (this.currentClassMethodNames.has(call.function) ||
1065
+ this.currentClassFields.has(call.function))
1066
+ ? "this."
1067
+ : "";
1068
+ if (!call.input) return `${thisPrefix}${fn}()`;
1069
+ const input = call.input;
1070
+ if (input.messageCreation) {
1071
+ const fields = input.messageCreation.fields ?? [];
1072
+ const selfField = fields.find((f) => f.name === "self");
1073
+ if (selfField) {
1074
+ const selfStr = this.expr(selfField.value);
1075
+ const otherArgs = fields
1076
+ .filter((f) => f.name !== "self" && f.name !== "__type_args__")
1077
+ .map((f) => this.expr(f.value))
1078
+ .join(", ");
1079
+ return otherArgs === ""
1080
+ ? `${selfStr}.${fn}()`
1081
+ : `${selfStr}.${fn}(${otherArgs})`;
1082
+ }
1083
+ const args = fields.map((f) => this.expr(f.value)).join(", ");
1084
+ return `${thisPrefix}${fn}(${args})`;
1085
+ }
1086
+ return `${thisPrefix}${fn}(${this.expr(input)})`;
1087
+ }
1088
+
1089
+ private compileStdCall(call: FunctionCall): string {
1090
+ const fn = call.function;
1091
+ const f = fieldMap(call.input?.messageCreation?.fields ?? []);
1092
+ const bin = (op: string) => `(${this.expr(f.get("left")!)} ${op} ${this.expr(f.get("right")!)})`;
1093
+ const un = (op: string) => {
1094
+ const inner = this.expr(f.get("value")!);
1095
+ if (op === "-" && inner.startsWith("-")) return `-(${inner})`;
1096
+ return `${op}${inner}`;
1097
+ };
1098
+
1099
+ switch (fn) {
1100
+ // Arithmetic
1101
+ case "add": return bin("+");
1102
+ case "subtract": return bin("-");
1103
+ case "multiply": return bin("*");
1104
+ case "divide": return `Math.trunc(${this.expr(f.get("left")!)} / ${this.expr(f.get("right")!)})`;
1105
+ case "divide_double":return bin("/");
1106
+ case "modulo": return bin("%");
1107
+ case "negate": return un("-");
1108
+ // Comparison
1109
+ case "equals": {
1110
+ // Use loose == when comparing against null so undefined matches
1111
+ // too (Dart has no undefined; JS returns undefined for missing
1112
+ // map keys, unset fields, etc.)
1113
+ const l = f.get("left"), r = f.get("right");
1114
+ if (l && r) {
1115
+ const le = this.expr(l), re = this.expr(r);
1116
+ const op = le === "null" || re === "null" ? "==" : "===";
1117
+ return `(${le} ${op} ${re})`;
1118
+ }
1119
+ return bin("===");
1120
+ }
1121
+ case "not_equals": {
1122
+ const l = f.get("left"), r = f.get("right");
1123
+ if (l && r) {
1124
+ const le = this.expr(l), re = this.expr(r);
1125
+ const op = le === "null" || re === "null" ? "!=" : "!==";
1126
+ return `(${le} ${op} ${re})`;
1127
+ }
1128
+ return bin("!==");
1129
+ }
1130
+ case "less_than": return bin("<");
1131
+ case "greater_than": return bin(">");
1132
+ case "lte": return bin("<=");
1133
+ case "gte": return bin(">=");
1134
+ // Logical
1135
+ case "and": return bin("&&");
1136
+ case "or": return bin("||");
1137
+ case "not": return un("!");
1138
+ // Bitwise
1139
+ case "bitwise_and": return bin("&");
1140
+ case "bitwise_or": return bin("|");
1141
+ case "bitwise_xor": return bin("^");
1142
+ case "bitwise_not": return un("~");
1143
+ case "left_shift": return bin("<<");
1144
+ case "right_shift": return bin(">>");
1145
+ case "unsigned_right_shift": return bin(">>>");
1146
+ case "integer_divide":
1147
+ return `Math.trunc(${this.expr(f.get("left")!)} / ${this.expr(f.get("right")!)})`;
1148
+ case "concat": return bin("+");
1149
+ case "to_string": return `__ball_to_string(${this.expr(f.get("value")!)})`;
1150
+ case "int_to_string":return `String(${this.expr(f.get("value")!)})`;
1151
+ case "double_to_string": return `__ball_double_to_string(${this.expr(f.get("value")!)})`;
1152
+ case "string_to_int":return `__ball_parse_int(${this.expr(f.get("value")!)})`;
1153
+ case "string_to_double": return `__ball_parse_double(${this.expr(f.get("value")!)})`;
1154
+ case "string_length":return `${this.expr(f.get("value")!)}.length`;
1155
+ case "string_to_upper": return `${this.wrapIfNeeded(f.get("value")!)}.toUpperCase()`;
1156
+ case "string_to_lower": return `${this.wrapIfNeeded(f.get("value")!)}.toLowerCase()`;
1157
+ case "string_trim": return `${this.wrapIfNeeded(f.get("value")!)}.trim()`;
1158
+ case "string_trim_start": return `${this.wrapIfNeeded(f.get("value")!)}.trimStart()`;
1159
+ case "string_trim_end": return `${this.wrapIfNeeded(f.get("value")!)}.trimEnd()`;
1160
+ case "string_contains": return `${this.expr(f.get("left")!)}.includes(${this.expr(f.get("right")!)})`;
1161
+ case "string_starts_with": return `${this.expr(f.get("left")!)}.startsWith(${this.expr(f.get("right")!)})`;
1162
+ case "string_ends_with": return `${this.expr(f.get("left")!)}.endsWith(${this.expr(f.get("right")!)})`;
1163
+ case "string_is_empty": return `(${this.expr(f.get("value")!)}.length === 0)`;
1164
+ case "string_split": return `${this.expr(f.get("value")!)}.split(${this.expr(f.get("separator")!)})`;
1165
+ case "string_substring": {
1166
+ const v = this.expr(f.get("value")!);
1167
+ const s = this.expr(f.get("start")!);
1168
+ const end = f.get("end");
1169
+ return end ? `${v}.substring(${s}, ${this.expr(end)})` : `${v}.substring(${s})`;
1170
+ }
1171
+ case "string_interpolation": {
1172
+ const parts = f.get("parts");
1173
+ if (parts?.literal?.listValue) {
1174
+ const pieces = (parts.literal.listValue.elements ?? [])
1175
+ .map((p) => `(${this.expr(p)})`)
1176
+ .join(" + ");
1177
+ return `(${pieces})`;
1178
+ }
1179
+ return `''`;
1180
+ }
1181
+ // Math
1182
+ case "math_abs": return `Math.abs(${this.expr(f.get("value")!)})`;
1183
+ case "math_round": return `Math.round(${this.expr(f.get("value")!)})`;
1184
+ case "math_floor": return `Math.floor(${this.expr(f.get("value")!)})`;
1185
+ case "math_ceil": return `Math.ceil(${this.expr(f.get("value")!)})`;
1186
+ case "math_trunc": return `Math.trunc(${this.expr(f.get("value")!)})`;
1187
+ case "math_sqrt": return `Math.sqrt(${this.expr(f.get("value")!)})`;
1188
+ case "math_pow": return `Math.pow(${this.expr(f.get("left")!)}, ${this.expr(f.get("right")!)})`;
1189
+ case "math_min": return `Math.min(${this.expr(f.get("left")!)}, ${this.expr(f.get("right")!)})`;
1190
+ case "math_max": return `Math.max(${this.expr(f.get("left")!)}, ${this.expr(f.get("right")!)})`;
1191
+ case "math_pi": return "Math.PI";
1192
+ case "math_e": return "Math.E";
1193
+ case "print": return `console.log(__ball_to_string(${this.expr(f.get("message")!)}))`;
1194
+ case "index": return `${this.expr(f.get("target")!)}[${this.expr(f.get("index")!)}]`;
1195
+ case "null_coalesce": return `(${this.expr(f.get("left")!)} ?? ${this.expr(f.get("right")!)})`;
1196
+ case "null_check": return this.expr(f.get("value")!);
1197
+ case "is": {
1198
+ const val = f.get("value");
1199
+ const typ = f.get("type");
1200
+ if (val && typ) {
1201
+ const v = this.expr(val);
1202
+ const t = typ.literal?.stringValue ?? "";
1203
+ return this.emitIsCheck(v, t);
1204
+ }
1205
+ return `(${this.expr(f.get("value")!)} != null)`;
1206
+ }
1207
+ case "is_not": {
1208
+ const val = f.get("value");
1209
+ const typ = f.get("type");
1210
+ if (val && typ) {
1211
+ const v = this.expr(val);
1212
+ const t = typ.literal?.stringValue ?? "";
1213
+ return `!(${this.emitIsCheck(v, t)})`;
1214
+ }
1215
+ return `(${this.expr(f.get("value")!)} == null)`;
1216
+ }
1217
+ case "as": return this.expr(f.get("value")!);
1218
+ case "if": {
1219
+ const cond = this.expr(f.get("condition")!);
1220
+ const t = this.expr(f.get("then")!);
1221
+ const elseE = f.get("else");
1222
+ const e = elseE ? this.expr(elseE) : "undefined";
1223
+ return `(${cond} ? ${t} : ${e})`;
1224
+ }
1225
+ case "paren": return `(${this.expr(f.get("value")!)})`;
1226
+ case "assert": return `console.assert(${this.expr(f.get("condition")!)})`;
1227
+ case "assign": {
1228
+ const op = stringField(call, "op") || "=";
1229
+ return `(${this.expr(f.get("target")!)} ${op} ${this.expr(f.get("value")!)})`;
1230
+ }
1231
+ case "pre_increment": return `(++${this.expr(f.get("value")!)})`;
1232
+ case "pre_decrement": return `(--${this.expr(f.get("value")!)})`;
1233
+ case "post_increment": return `(${this.expr(f.get("value")!)}++)`;
1234
+ case "post_decrement": return `(${this.expr(f.get("value")!)}--)`;
1235
+ case "throw": {
1236
+ const v = f.get("value");
1237
+ if (!v) return "(() => { throw null; })()";
1238
+ const str = this.compileThrowValue(v) ?? this.expr(v);
1239
+ return `(() => { throw ${str}; })()`;
1240
+ }
1241
+ case "rethrow": return "(() => { throw __ball_active_error; })()";
1242
+ case "await": return `await ${this.expr(f.get("value")!)}`;
1243
+ case "switch":
1244
+ case "switch_expr": return this.compileSwitchExpr(call);
1245
+ case "return": {
1246
+ // In expression position: unwrap to the bare value. When this
1247
+ // appears inside a switch-expr case body, the switch handler
1248
+ // detects the return and emits `return <ternary>` at statement
1249
+ // level.
1250
+ const v = f.get("value");
1251
+ return v ? this.expr(v) : "undefined";
1252
+ }
1253
+ case "null_aware_call": {
1254
+ const target = f.get("target");
1255
+ const method = f.get("method");
1256
+ if (!target || !method) return "/* null_aware_call missing */";
1257
+ const methodName = method.literal?.stringValue ?? "";
1258
+ const inputFields = call.input?.messageCreation?.fields ?? [];
1259
+ const otherArgs = inputFields
1260
+ .filter((fd) => fd.name !== "target" && fd.name !== "method" && fd.name !== "__type_args__")
1261
+ .map((fd) => this.expr(fd.value))
1262
+ .join(", ");
1263
+ return `${this.expr(target)}?.${methodName}(${otherArgs})`;
1264
+ }
1265
+ case "null_aware_index": {
1266
+ const self_ = f.get("self") ?? f.get("target");
1267
+ const idx = f.get("index") ?? f.get("key");
1268
+ if (!self_ || !idx) return "/* null_aware_index missing */";
1269
+ return `${this.expr(self_)}[${this.expr(idx)}]`;
1270
+ }
1271
+ case "null_aware_access": {
1272
+ const target = f.get("target");
1273
+ const fieldE = f.get("field");
1274
+ if (!target || !fieldE) return "/* null_aware_access missing */";
1275
+ return `${this.expr(target)}?.${fieldE.literal?.stringValue ?? ""}`;
1276
+ }
1277
+ case "typed_list": {
1278
+ const elements = f.get("elements");
1279
+ if (elements?.literal?.listValue) {
1280
+ const parts = (elements.literal.listValue.elements ?? []).map((x) => this.expr(x));
1281
+ return `[${parts.join(", ")}]`;
1282
+ }
1283
+ if (elements) return this.expr(elements);
1284
+ return "[]";
1285
+ }
1286
+ case "typed_map": {
1287
+ const entries = f.get("entries");
1288
+ if (!entries) return "new Map()";
1289
+ const entryExprs = entries.literal?.listValue?.elements ?? [];
1290
+ if (entryExprs.length === 0) return "new Map()";
1291
+ const pairs: string[] = [];
1292
+ for (const e of entryExprs) {
1293
+ if (e.messageCreation) {
1294
+ const mc = e.messageCreation;
1295
+ const mFields = mc.fields ?? [];
1296
+ const k = mFields.find((fd) => fd.name === "key")?.value;
1297
+ const v = mFields.find((fd) => fd.name === "value")?.value;
1298
+ if (k && v) pairs.push(`[${this.expr(k)}, ${this.expr(v)}]`);
1299
+ }
1300
+ }
1301
+ return `new Map([${pairs.join(", ")}])`;
1302
+ }
1303
+ case "set_create": {
1304
+ const elements: string[] = [];
1305
+ const inputFields = call.input?.messageCreation?.fields ?? [];
1306
+ for (const fd of inputFields) {
1307
+ elements.push(this.expr(fd.value));
1308
+ }
1309
+ if (elements.length === 0) return "new Set()";
1310
+ return `new Set([${elements.join(", ")}])`;
1311
+ }
1312
+ case "map_create": {
1313
+ // map_create can have entries passed as `entry` fields on the
1314
+ // input MessageCreation. Each entry is a message with key/value.
1315
+ // Emit as plain object {} (not new Map()) — JS Map doesn't
1316
+ // support bracket access (m['k'] = v) but the compiled engine
1317
+ // uses it throughout.
1318
+ const mapEntries: string[] = [];
1319
+ const inputFields = call.input?.messageCreation?.fields ?? [];
1320
+ for (const fd of inputFields) {
1321
+ if (fd.name === "entry" && fd.value.messageCreation) {
1322
+ const mc = fd.value.messageCreation;
1323
+ const mFields = mc.fields ?? [];
1324
+ const kf = mFields.find((f: any) => f.name === "key");
1325
+ const vf = mFields.find((f: any) => f.name === "value");
1326
+ if (kf && vf) {
1327
+ mapEntries.push(`${this.expr(kf.value)}: ${this.expr(vf.value)}`);
1328
+ }
1329
+ }
1330
+ }
1331
+ if (mapEntries.length === 0) return "{}";
1332
+ return `{${mapEntries.join(", ")}}`;
1333
+ }
1334
+ case "record": {
1335
+ const positional: string[] = [];
1336
+ const named: Array<[string, string]> = [];
1337
+ const posRe = /^(?:\$|arg)(\d+)$/;
1338
+ for (const fd of call.input?.messageCreation?.fields ?? []) {
1339
+ if (fd.name === "__type_args__") continue;
1340
+ if (posRe.test(fd.name)) {
1341
+ positional.push(this.expr(fd.value));
1342
+ } else {
1343
+ named.push([fd.name, this.expr(fd.value)]);
1344
+ }
1345
+ }
1346
+ if (named.length === 0) return `[${positional.join(", ")}]`;
1347
+ if (positional.length === 0) {
1348
+ return `{ ${named.map(([k, v]) => `${k}: ${v}`).join(", ")} }`;
1349
+ }
1350
+ const entries = [
1351
+ ...positional.map((v, i) => `"${i}": ${v}`),
1352
+ ...named.map(([k, v]) => `${k}: ${v}`),
1353
+ ];
1354
+ return `{ ${entries.join(", ")} }`;
1355
+ }
1356
+ case "yield": return `yield ${this.expr(f.get("value")!)}`;
1357
+ case "yield_each": return `yield* ${this.expr(f.get("value")!)}`;
1358
+ default: {
1359
+ const args = Array.from(f.values()).map((e) => this.expr(e)).join(", ");
1360
+ return `/* std.${fn} */ ${sanitize(fn)}(${args})`;
1361
+ }
1362
+ }
1363
+ }
1364
+
1365
+ private emitIsCheck(value: string, type: string): string {
1366
+ // Strip generic args: Map<String, Object?> → Map
1367
+ const baseType = type.includes("<") ? type.slice(0, type.indexOf("<")).trim() : type.trim();
1368
+ // Strip nullable: Map? → Map
1369
+ const t = baseType.endsWith("?") ? baseType.slice(0, -1) : baseType;
1370
+ switch (t) {
1371
+ case "int": return `(typeof ${value} === 'number' && Number.isInteger(${value}))`;
1372
+ case "double": case "num": case "number": return `(typeof ${value} === 'number')`;
1373
+ case "String": case "string": return `(typeof ${value} === 'string')`;
1374
+ case "bool": case "boolean": return `(typeof ${value} === 'boolean')`;
1375
+ case "List": case "Iterable": return `Array.isArray(${value})`;
1376
+ case "Map": return `(typeof ${value} === 'object' && ${value} !== null && !Array.isArray(${value}))`;
1377
+ case "Set": return `(${value} instanceof Set)`;
1378
+ case "Null": return `(${value} == null)`;
1379
+ case "Function": return `(typeof ${value} === 'function')`;
1380
+ default:
1381
+ if (this.typeIsUserDefinedClass(`main:${t}`) || this.typeIsUserDefinedClass(t)) {
1382
+ return `(${value} instanceof ${classTsName(t)})`;
1383
+ }
1384
+ return `(${value} != null)`;
1385
+ }
1386
+ }
1387
+
1388
+ private wrapIfNeeded(e: Expression): string {
1389
+ const s = this.expr(e);
1390
+ if (s === "") return s;
1391
+ const first = s.charCodeAt(0);
1392
+ if (first === 0x21 /* ! */ || first === 0x7e /* ~ */ || first === 0x2d /* - */) {
1393
+ return `(${s})`;
1394
+ }
1395
+ return s;
1396
+ }
1397
+
1398
+ private compileSwitchExpr(call: FunctionCall): string {
1399
+ const subjectExpr = field(call, "subject");
1400
+ const casesField = field(call, "cases");
1401
+ if (!subjectExpr || !casesField) return "/* malformed switch */ undefined";
1402
+ const subjectStr = this.expr(subjectExpr);
1403
+ const caseExprs = casesField.literal?.listValue?.elements ?? [];
1404
+ let defaultBody: Expression | undefined;
1405
+ const branches: Array<{ cond: string; body: string }> = [];
1406
+ for (const ce of caseExprs) {
1407
+ if (!ce.messageCreation) continue;
1408
+ let pattern: Expression | undefined;
1409
+ let body: Expression | undefined;
1410
+ for (const fd of ce.messageCreation.fields ?? []) {
1411
+ if (fd.name === "pattern") pattern = fd.value;
1412
+ if (fd.name === "body") body = fd.value;
1413
+ }
1414
+ if (!body) continue;
1415
+ if (!pattern) {
1416
+ defaultBody = body;
1417
+ continue;
1418
+ }
1419
+ const patText = patternLiteralText(pattern);
1420
+ if (patText === undefined) {
1421
+ branches.push({
1422
+ cond: `((${subjectStr}) === ${this.expr(pattern)})`,
1423
+ body: this.expr(body),
1424
+ });
1425
+ continue;
1426
+ }
1427
+ const cond = patternToTsCondition(patText, subjectStr);
1428
+ if (cond === "true") {
1429
+ defaultBody = body;
1430
+ break;
1431
+ }
1432
+ branches.push({ cond, body: this.expr(body) });
1433
+ }
1434
+ const tail = defaultBody ? this.expr(defaultBody) : "undefined";
1435
+ if (branches.length === 0) return tail;
1436
+ let result = tail;
1437
+ for (const { cond, body } of [...branches].reverse()) {
1438
+ result = `(${cond} ? (${body}) : ${result})`;
1439
+ }
1440
+ return result;
1441
+ }
1442
+
1443
+ private compileThrowValue(v: Expression): string | undefined {
1444
+ if (!v.messageCreation) return undefined;
1445
+ if (v.messageCreation.typeName) return undefined;
1446
+ const entries = (v.messageCreation.fields ?? [])
1447
+ .map((fd) => `'${fd.name}': ${this.expr(fd.value)}`)
1448
+ .join(", ");
1449
+ return `{${entries}}`;
1450
+ }
1451
+
1452
+ // ───────────────────────── Helpers ─────────────────────────────────
1453
+
1454
+ private enclosingTypeName(fnName: string): string | undefined {
1455
+ let best: string | undefined;
1456
+ for (const name of this.typeDefByName.keys()) {
1457
+ if (fnName.startsWith(`${name}.`) && (best === undefined || name.length > best.length)) {
1458
+ best = name;
1459
+ }
1460
+ }
1461
+ return best;
1462
+ }
1463
+
1464
+ private dartTypeToTs(dart: string): string {
1465
+ const t = dart.trim();
1466
+ if (t === "") return "any";
1467
+ if (t.startsWith("(")) return "any";
1468
+ if (t.includes(" Function(")) return "any";
1469
+ const nonNull = t.endsWith("?") ? t.slice(0, -1) : t;
1470
+ const lt = nonNull.indexOf("<");
1471
+ if (lt > 0 && nonNull.endsWith(">")) {
1472
+ const outer = nonNull.slice(0, lt);
1473
+ const inner = nonNull.slice(lt + 1, -1);
1474
+ const innerArgs = splitTopLevelCommas(inner).map((x) => this.dartTypeToTs(x));
1475
+ switch (outer) {
1476
+ case "List":
1477
+ case "Iterable":
1478
+ case "Set":
1479
+ return `Array<${innerArgs.join(", ")}>`;
1480
+ case "Map":
1481
+ return `Map<${innerArgs.join(", ")}>`;
1482
+ case "Future":
1483
+ return innerArgs.length === 0 ? "Promise<any>" : `Promise<${innerArgs.join(", ")}>`;
1484
+ case "FutureOr": {
1485
+ const a = innerArgs.length === 0 ? "any" : innerArgs[0];
1486
+ return `${a} | Promise<${a}>`;
1487
+ }
1488
+ default:
1489
+ return `${this.dartTypeToTs(outer)}<${innerArgs.join(", ")}>`;
1490
+ }
1491
+ }
1492
+ switch (nonNull) {
1493
+ case "int":
1494
+ case "double":
1495
+ case "num":
1496
+ return "number";
1497
+ case "bool":
1498
+ return "boolean";
1499
+ case "String":
1500
+ return "string";
1501
+ case "void":
1502
+ return "void";
1503
+ case "dynamic":
1504
+ case "Object":
1505
+ return "any";
1506
+ }
1507
+ if (nonNull.startsWith("main:")) return nonNull.slice(5);
1508
+ return nonNull;
1509
+ }
1510
+ }
1511
+
1512
+ // ───────────────────────── Free helpers ───────────────────────────────
1513
+
1514
+ function extractParams(fn: FunctionDef): string[] {
1515
+ const params = fn.metadata?.["params"];
1516
+ if (!Array.isArray(params)) return [];
1517
+ const out: string[] = [];
1518
+ for (const p of params) {
1519
+ if (p && typeof p === "object" && "name" in p && typeof (p as any).name === "string") {
1520
+ out.push((p as any).name);
1521
+ }
1522
+ }
1523
+ return out;
1524
+ }
1525
+
1526
+ function extractCtorParams(meta: Struct): CtorParam[] {
1527
+ const raw = meta["params"];
1528
+ if (!Array.isArray(raw)) return [];
1529
+ const out: CtorParam[] = [];
1530
+ for (const p of raw) {
1531
+ if (p && typeof p === "object" && "name" in p && typeof (p as any).name === "string") {
1532
+ out.push({
1533
+ name: (p as any).name,
1534
+ isThis: (p as any).is_this === true,
1535
+ isNamed: (p as any).is_named === true,
1536
+ });
1537
+ }
1538
+ }
1539
+ return out;
1540
+ }
1541
+
1542
+ function functionIsAsync(fn: FunctionDef): boolean {
1543
+ return fn.metadata?.["is_async"] === true;
1544
+ }
1545
+
1546
+ function field(call: FunctionCall, name: string): Expression | undefined {
1547
+ const fields = call.input?.messageCreation?.fields;
1548
+ if (!fields) return undefined;
1549
+ for (const f of fields) if (f.name === name) return f.value;
1550
+ return undefined;
1551
+ }
1552
+
1553
+ function stringField(call: FunctionCall, name: string): string | undefined {
1554
+ const e = field(call, name);
1555
+ return e?.literal?.stringValue;
1556
+ }
1557
+
1558
+ function stringFieldVal(
1559
+ m: Map<string, Expression>,
1560
+ name: string,
1561
+ ): string | undefined {
1562
+ return m.get(name)?.literal?.stringValue;
1563
+ }
1564
+
1565
+ function fieldMap(fields: FieldValuePair[]): Map<string, Expression> {
1566
+ const m = new Map<string, Expression>();
1567
+ for (const f of fields) m.set(f.name, f.value);
1568
+ return m;
1569
+ }
1570
+
1571
+ function memberShortName(qualified: string): string {
1572
+ const dot = qualified.lastIndexOf(".");
1573
+ return sanitize(dot < 0 ? qualified : qualified.slice(dot + 1));
1574
+ }
1575
+
1576
+ function classTsName(qualified: string): string {
1577
+ const colon = qualified.lastIndexOf(":");
1578
+ return colon < 0 ? qualified : qualified.slice(colon + 1);
1579
+ }
1580
+
1581
+ function isStd(module: string | undefined): boolean {
1582
+ return module === "std" || module === "dart_std";
1583
+ }
1584
+
1585
+ function containsBareKeyword(text: string, kw: string): boolean {
1586
+ return new RegExp(`(^|[^A-Za-z0-9_$])${kw}([^A-Za-z0-9_$]|$)`).test(text);
1587
+ }
1588
+
1589
+ function translateInitString(raw: string): string {
1590
+ for (const kw of ["var", "final", "int", "double", "String", "bool", "num"]) {
1591
+ if (raw.length > kw.length + 1 && raw.slice(0, kw.length) === kw && raw[kw.length] === " ") {
1592
+ return `let ${raw.slice(kw.length + 1)}`;
1593
+ }
1594
+ }
1595
+ return `let ${raw}`;
1596
+ }
1597
+
1598
+ function splitTopLevelCommas(s: string): string[] {
1599
+ const out: string[] = [];
1600
+ let depth = 0;
1601
+ let start = 0;
1602
+ for (let i = 0; i < s.length; i++) {
1603
+ const c = s.charCodeAt(i);
1604
+ if (c === 0x3c /* < */ || c === 0x28 /* ( */ || c === 0x5b /* [ */) depth++;
1605
+ else if (c === 0x3e /* > */ || c === 0x29 /* ) */ || c === 0x5d /* ] */) depth--;
1606
+ else if (c === 0x2c /* , */ && depth === 0) {
1607
+ out.push(s.slice(start, i).trim());
1608
+ start = i + 1;
1609
+ }
1610
+ }
1611
+ if (start < s.length) out.push(s.slice(start).trim());
1612
+ return out;
1613
+ }
1614
+
1615
+ function splitTopLevel(text: string, delim: string): string[] {
1616
+ const out: string[] = [];
1617
+ let depth = 0;
1618
+ let quote = 0;
1619
+ let start = 0;
1620
+ for (let i = 0; i <= text.length - delim.length; i++) {
1621
+ const c = text.charCodeAt(i);
1622
+ if (quote === 0) {
1623
+ if (c === 0x28 || c === 0x5b || c === 0x3c) depth++;
1624
+ else if (c === 0x29 || c === 0x5d || c === 0x3e) depth--;
1625
+ else if (c === 0x27 || c === 0x22) quote = c;
1626
+ else if (depth === 0 && text.slice(i, i + delim.length) === delim) {
1627
+ out.push(text.slice(start, i).trim());
1628
+ start = i + delim.length;
1629
+ i += delim.length - 1;
1630
+ }
1631
+ } else if (c === quote) {
1632
+ quote = 0;
1633
+ }
1634
+ }
1635
+ out.push(text.slice(start).trim());
1636
+ return out;
1637
+ }
1638
+
1639
+ function patternLiteralText(pat: Expression): string | undefined {
1640
+ return pat.literal?.stringValue;
1641
+ }
1642
+
1643
+ function patternToTsCondition(pat: string, subject: string): string {
1644
+ const trimmed = pat.trim();
1645
+ if (trimmed === "") return "true";
1646
+ if (trimmed === "_") return "true";
1647
+ const whenMatch = /^_\s+when\s+(.+)$/.exec(trimmed);
1648
+ if (whenMatch) return `(${whenMatch[1]})`;
1649
+ if (trimmed.includes("||")) {
1650
+ const parts = splitTopLevel(trimmed, "||");
1651
+ if (parts.length > 1) {
1652
+ return `(${parts.map((p) => patternToTsCondition(p, subject)).join(" || ")})`;
1653
+ }
1654
+ }
1655
+ if (/^-?\d+(\.\d+)?$/.test(trimmed)) return `(${subject} === ${trimmed})`;
1656
+ if (trimmed === "true" || trimmed === "false" || trimmed === "null") {
1657
+ return `(${subject} === ${trimmed})`;
1658
+ }
1659
+ if (
1660
+ (trimmed.startsWith("'") && trimmed.endsWith("'")) ||
1661
+ (trimmed.startsWith('"') && trimmed.endsWith('"'))
1662
+ ) {
1663
+ const inner = trimmed.slice(1, -1);
1664
+ return `(${subject} === ${jsStringLiteral(inner)})`;
1665
+ }
1666
+ return `(${subject} === (${trimmed}))`;
1667
+ }
1668
+
1669
+ function jsStringLiteral(s: string): string {
1670
+ let out = "'";
1671
+ for (let i = 0; i < s.length; i++) {
1672
+ const cu = s.charCodeAt(i);
1673
+ if (cu === 0x27) out += "\\'";
1674
+ else if (cu === 0x5c) out += "\\\\";
1675
+ else if (cu === 0x0a) out += "\\n";
1676
+ else if (cu === 0x0d) out += "\\r";
1677
+ else if (cu === 0x09) out += "\\t";
1678
+ else if (cu >= 0x20 && cu < 0x7f) out += s[i];
1679
+ else out += "\\u" + cu.toString(16).padStart(4, "0");
1680
+ }
1681
+ return out + "'";
1682
+ }
1683
+
1684
+ /** Return a default initializer expression for a TS type string so
1685
+ * class fields don't start as `undefined`. Dart fields are implicitly
1686
+ * initialized (Map→{}, List→[], bool→false, etc.); TS fields are not.
1687
+ *
1688
+ * Also accepts the raw Dart type string for cases where the TS mapper
1689
+ * lost precision (e.g. `Map<K, V Function(X)>` → `any` because the
1690
+ * function type triggered the early-return). Checking the Dart type
1691
+ * catches these.
1692
+ */
1693
+ function defaultInitializer(type: string, rawDartType?: string): string | undefined {
1694
+ const t = type.trim();
1695
+ const raw = (rawDartType ?? "").trim();
1696
+ // Nullable types (ending in ?) default to null, not the type's
1697
+ // default value. Dart's `Set<String>? _allowlist = null` must stay
1698
+ // null, not become `new Set()` which breaks null-guard patterns.
1699
+ // Nullable types default to null EXCEPT Maps — null-aware access
1700
+ // (?.[] / ?.) isn't in the compiled output and null Maps crash
1701
+ // on bracket access. Empty {} is safe. Sets and Lists stay null
1702
+ // because code like `if (allowlist != null)` uses null to mean
1703
+ // "not active" and an empty collection would incorrectly activate
1704
+ // the filter.
1705
+ if (raw.endsWith("?")) {
1706
+ const inner = raw.slice(0, -1).trim();
1707
+ if (inner.startsWith("Map<") || inner === "Map") return "{}";
1708
+ return "null";
1709
+ }
1710
+ const d = raw.replace(/\?$/, "");
1711
+ // Check both mapped TS type and raw Dart type.
1712
+ // Use plain {} instead of new Map() — JS Map doesn't support
1713
+ // bracket access (m['k'] = v), but the compiled engine uses it
1714
+ // throughout for dispatch tables and caches.
1715
+ if (t.startsWith("Map<") || d.startsWith("Map<") || d === "Map") return "{}";
1716
+ if (t.startsWith("Array<") || t === "Array" || d.startsWith("List<") || d === "List") return "[]";
1717
+ if (t.startsWith("Set<") || t === "Set" || d.startsWith("Set<") || d === "Set") return "new Set()";
1718
+ if (t === "number" || d === "int" || d === "double" || d === "num") return "0";
1719
+ if (t === "boolean" || d === "bool") return "false";
1720
+ if (t === "string" || d === "String") return "''";
1721
+ return undefined;
1722
+ }
1723
+
1724
+ function sanitize(name: string): string {
1725
+ let out = name;
1726
+ const colon = out.indexOf(":");
1727
+ if (colon >= 0) out = out.slice(colon + 1);
1728
+ out = out.replace(/[.-]/g, "_");
1729
+ const reserved = new Set([
1730
+ "class", "function", "return", "new", "delete", "var", "let", "const",
1731
+ "typeof", "instanceof", "interface", "enum", "export", "import", "yield",
1732
+ "package", "private", "protected", "public", "static", "super", "this",
1733
+ "true", "false", "null", "undefined",
1734
+ ]);
1735
+ if (reserved.has(out)) out += "_";
1736
+ return out;
1737
+ }
1738
+
1739
+ /** Convenience: compile a Program directly. */
1740
+ export function compile(program: Program, options?: CompileOptions): string {
1741
+ return new BallCompiler(program).compile(options);
1742
+ }