@jalvin/compiler 2.0.41 → 2.0.43
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/codegen.d.ts +53 -11
- package/dist/codegen.d.ts.map +1 -1
- package/dist/codegen.js +831 -339
- package/dist/codegen.js.map +1 -1
- package/dist/diagnostics.d.ts +0 -2
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js +1 -3
- package/dist/diagnostics.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/typechecker.d.ts +0 -12
- package/dist/typechecker.d.ts.map +1 -1
- package/dist/typechecker.js +152 -244
- package/dist/typechecker.js.map +1 -1
- package/package.json +1 -1
package/dist/codegen.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
3
|
-
// Jalvin Code Generator — AST → TypeScript
|
|
3
|
+
// Jalvin Code Generator — AST → TypeScript/TSX
|
|
4
4
|
//
|
|
5
5
|
// Design principles:
|
|
6
|
-
// • component declarations →
|
|
6
|
+
// • component declarations → React functional components (TSX)
|
|
7
7
|
// • data class → class with auto-generated copy(), equals(), toString()
|
|
8
8
|
// • sealed class → TypeScript discriminated union + base class
|
|
9
9
|
// • extension functions → module-level functions with receiver as first param,
|
|
@@ -89,6 +89,7 @@ class Writer {
|
|
|
89
89
|
setSourceLine(line) { this.currentLine = line + 1; }
|
|
90
90
|
}
|
|
91
91
|
exports.DEFAULT_CODEGEN_OPTIONS = {
|
|
92
|
+
jsx: false,
|
|
92
93
|
module: "esm",
|
|
93
94
|
emitTypes: false,
|
|
94
95
|
runtimeImport: "@jalvin/runtime",
|
|
@@ -98,10 +99,8 @@ class CodeGenerator {
|
|
|
98
99
|
opts;
|
|
99
100
|
hasComponents = false;
|
|
100
101
|
runtimeSymbolsNeeded = new Set();
|
|
101
|
-
/** Names of
|
|
102
|
-
|
|
103
|
-
/** Names of all potential components (including UI primitives) — used to detect props-object calls */
|
|
104
|
-
allComponentLikeNames = new Set();
|
|
102
|
+
/** Names of component functions — used to detect component calls and emit them as JSX */
|
|
103
|
+
componentNames = new Set();
|
|
105
104
|
/**
|
|
106
105
|
* Param names for components resolved from imported local .jalvin files.
|
|
107
106
|
* Maps component name → ordered list of param names.
|
|
@@ -109,8 +108,6 @@ class CodeGenerator {
|
|
|
109
108
|
componentParamNames = new Map();
|
|
110
109
|
/** True when the program contains `import @jalvin/ui.*` — used to detect UI primitive calls */
|
|
111
110
|
hasUiStarImport = false;
|
|
112
|
-
/** Names of symbols explicitly imported from @jalvin/ui */
|
|
113
|
-
uiNamedImports = new Set();
|
|
114
111
|
/** Operator overload resolutions from the type checker */
|
|
115
112
|
operatorOverloadMap = new Map();
|
|
116
113
|
/** Type map from the type checker */
|
|
@@ -134,7 +131,6 @@ class CodeGenerator {
|
|
|
134
131
|
* Maps module specifier (e.g. "src/views") → sorted list of exported names.
|
|
135
132
|
*/
|
|
136
133
|
localStarExports = new Map();
|
|
137
|
-
isInClass = false;
|
|
138
134
|
constructor(opts = {}) {
|
|
139
135
|
this.opts = { ...exports.DEFAULT_CODEGEN_OPTIONS, ...opts };
|
|
140
136
|
}
|
|
@@ -145,40 +141,34 @@ class CodeGenerator {
|
|
|
145
141
|
this.typeMap = typeMap;
|
|
146
142
|
// Pre-scan for components (local declarations OR @jalvin/ui imports)
|
|
147
143
|
this.hasComponents = program.declarations.some((d) => d.kind === "ComponentDecl" ||
|
|
148
|
-
(d.kind === "ClassDecl" && d.body?.members.some((m) => m.kind === "ComponentDecl"))) || program.imports.some((imp) => imp.path[0] === "@jalvin" && imp.path[1] === "ui")
|
|
149
|
-
|| this.containsJsx(program);
|
|
144
|
+
(d.kind === "ClassDecl" && d.body?.members.some((m) => m.kind === "ComponentDecl"))) || program.imports.some((imp) => imp.path[0] === "@jalvin" && imp.path[1] === "ui");
|
|
150
145
|
// Collect component names for component call detection
|
|
151
|
-
this.
|
|
152
|
-
this.allComponentLikeNames = new Set();
|
|
146
|
+
this.componentNames = new Set();
|
|
153
147
|
this.componentParamNames = new Map();
|
|
154
148
|
for (const decl of program.declarations) {
|
|
155
149
|
if (decl.kind === "ComponentDecl") {
|
|
156
|
-
this.
|
|
157
|
-
this.allComponentLikeNames.add(decl.name);
|
|
150
|
+
this.componentNames.add(decl.name);
|
|
158
151
|
this.componentParamNames.set(decl.name, decl.params.map((p) => p.name));
|
|
159
152
|
}
|
|
160
153
|
if (decl.kind === "ClassDecl" && decl.body) {
|
|
161
154
|
for (const m of decl.body.members) {
|
|
162
155
|
if (m.kind === "ComponentDecl") {
|
|
163
|
-
this.
|
|
164
|
-
this.allComponentLikeNames.add(m.name);
|
|
156
|
+
this.componentNames.add(m.name);
|
|
165
157
|
this.componentParamNames.set(m.name, m.params.map((p) => p.name));
|
|
166
158
|
}
|
|
167
159
|
}
|
|
168
160
|
}
|
|
169
161
|
}
|
|
170
162
|
this.hasUiStarImport = false;
|
|
171
|
-
this.uiNamedImports = new Set();
|
|
172
163
|
for (const imp of program.imports) {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
}
|
|
164
|
+
// Named imports from @jalvin/ui are treated as potential component functions.
|
|
165
|
+
// Hooks (e.g. useIsHovered) are filtered out at call-site by the type-based guard below.
|
|
166
|
+
if (imp.path[0] === "@jalvin" && imp.path[1] === "ui" && !imp.star) {
|
|
167
|
+
this.componentNames.add(imp.path[imp.path.length - 1]);
|
|
168
|
+
}
|
|
169
|
+
// Star import from @jalvin/ui — UI primitives won't be in componentNames
|
|
170
|
+
if (imp.path[0] === "@jalvin" && imp.path[1] === "ui" && imp.star) {
|
|
171
|
+
this.hasUiStarImport = true;
|
|
182
172
|
}
|
|
183
173
|
}
|
|
184
174
|
// Resolve component fun symbols from locally imported .jalvin files
|
|
@@ -212,42 +202,14 @@ class CodeGenerator {
|
|
|
212
202
|
return {
|
|
213
203
|
code,
|
|
214
204
|
lineMap: this.w.lineMap,
|
|
215
|
-
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
/** Walk AST to see if it contains any JsxElement nodes */
|
|
219
|
-
containsJsx(program) {
|
|
220
|
-
let found = false;
|
|
221
|
-
const walk = (val) => {
|
|
222
|
-
if (found || !val || typeof val !== "object")
|
|
223
|
-
return;
|
|
224
|
-
if (Array.isArray(val)) {
|
|
225
|
-
for (const item of val)
|
|
226
|
-
walk(item);
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
229
|
-
const obj = val;
|
|
230
|
-
if (obj["kind"] === "JsxElement") {
|
|
231
|
-
found = true;
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
234
|
-
for (const propVal of Object.values(obj)) {
|
|
235
|
-
walk(propVal);
|
|
236
|
-
}
|
|
205
|
+
isJsx: this.hasComponents,
|
|
237
206
|
};
|
|
238
|
-
for (const decl of program.declarations)
|
|
239
|
-
walk(decl);
|
|
240
|
-
return found;
|
|
241
207
|
}
|
|
242
208
|
// ── Preamble & imports ─────────────────────────────────────────────────────
|
|
243
209
|
buildPreamble() {
|
|
244
210
|
const lines = [];
|
|
245
|
-
lines.push("// ─────────────────────────────────────────────────────────────────────────────");
|
|
246
|
-
lines.push("// JALVIN COMPILER V5 - PURE VANILLA - NO REACT ALLOWED");
|
|
247
|
-
lines.push(`// BUILD ID: ${Date.now()}`);
|
|
248
|
-
lines.push("// ─────────────────────────────────────────────────────────────────────────────");
|
|
249
211
|
if (this.hasComponents) {
|
|
250
|
-
|
|
212
|
+
lines.push('import React from "react";');
|
|
251
213
|
}
|
|
252
214
|
// Emit compiler-injected runtime symbols, but only those NOT already covered
|
|
253
215
|
// by a star-import's named-import expansion (to avoid duplicate imports).
|
|
@@ -264,6 +226,19 @@ class CodeGenerator {
|
|
|
264
226
|
const sourceRoot = this.opts.sourceRoot ?? process.cwd();
|
|
265
227
|
// Re-emit user imports as ES imports
|
|
266
228
|
for (const imp of program.imports) {
|
|
229
|
+
// Build module specifier.
|
|
230
|
+
//
|
|
231
|
+
// Scoped packages (@org/pkg.Symbol): symbol is an export from the package,
|
|
232
|
+
// strip the last segment.
|
|
233
|
+
// import @jalvin/ui.Column → import { Column } from "@jalvin/ui"
|
|
234
|
+
// import @jalvin/runtime.* → import * as runtime from "@jalvin/runtime"
|
|
235
|
+
//
|
|
236
|
+
// Local imports (a.b.C): check whether the preceding segments already
|
|
237
|
+
// resolve to a file on disk.
|
|
238
|
+
// import src.models.Rotation → src/models/Rotation.ts does NOT exist
|
|
239
|
+
// → import { Rotation } from "src/models/Rotation"
|
|
240
|
+
// import src.models.css.Css → src/models/css.ts DOES exist
|
|
241
|
+
// → import { Css } from "src/models/css"
|
|
267
242
|
const isScoped = imp.path[0].startsWith("@");
|
|
268
243
|
let moduleSpecifier;
|
|
269
244
|
if (imp.star) {
|
|
@@ -278,28 +253,35 @@ class CodeGenerator {
|
|
|
278
253
|
moduleSpecifier = moduleParts[0] + "/" + moduleParts.slice(1).join("/");
|
|
279
254
|
}
|
|
280
255
|
else {
|
|
281
|
-
// Non-scoped import: prefer local-file convention
|
|
256
|
+
// Non-scoped import: prefer local-file convention unless this looks like
|
|
257
|
+
// an installed npm package path (e.g. cubing/twisty.TwistyPlayer).
|
|
282
258
|
const precedingParts = imp.path.slice(0, -1);
|
|
283
259
|
const precedingRelPath = precedingParts.join("/");
|
|
284
260
|
const fileExts = [".ts", ".tsx", ".jalvin"];
|
|
285
261
|
const precedingIsFile = precedingParts.length > 0 && fileExts.some((ext) => fs.existsSync(nodePath.join(sourceRoot, precedingRelPath + ext)));
|
|
286
262
|
const looksLikeNpmPackage = this.isLikelyNodeModuleImport(imp.path, sourceRoot);
|
|
287
263
|
moduleSpecifier = precedingIsFile
|
|
288
|
-
? precedingRelPath
|
|
264
|
+
? precedingRelPath // src.models.css.Css → "src/models/css"
|
|
289
265
|
: looksLikeNpmPackage
|
|
290
|
-
? precedingRelPath
|
|
291
|
-
: imp.path.join("/");
|
|
266
|
+
? precedingRelPath // cubing.twisty.TwistyPlayer → "cubing/twisty"
|
|
267
|
+
: imp.path.join("/"); // src.models.Rotation → "src/models/Rotation"
|
|
292
268
|
}
|
|
293
269
|
if (imp.star && isScoped && this.externalStarCandidates.size > 0) {
|
|
270
|
+
// Named-import expansion of a scoped star import.
|
|
271
|
+
// Emit explicit named imports for each external symbol used in the file so
|
|
272
|
+
// that symbols like `ViewModel`, `mutableStateOf` are directly in scope.
|
|
294
273
|
const symbols = [...this.externalStarCandidates].sort();
|
|
295
274
|
this.w.writeIndentedLine(`import { ${symbols.join(", ")} } from "${moduleSpecifier}";`);
|
|
296
275
|
this.handledStarImportModules.add(moduleSpecifier);
|
|
297
276
|
}
|
|
298
277
|
else if (imp.star && !isScoped && this.localStarExports.has(moduleSpecifier)) {
|
|
278
|
+
// Local wildcard import: import src.module.* — expand to named imports
|
|
279
|
+
// so all exported symbols are directly in scope (no namespace qualifier needed).
|
|
299
280
|
const symbols = this.localStarExports.get(moduleSpecifier).slice().sort();
|
|
300
281
|
this.w.writeIndentedLine(`import { ${symbols.join(", ")} } from "${moduleSpecifier}";`);
|
|
301
282
|
}
|
|
302
283
|
else if (imp.star) {
|
|
284
|
+
// Fallback: namespace import (multiple scoped star imports or no candidates).
|
|
303
285
|
this.w.writeIndentedLine(`import * as ${imp.path[imp.path.length - 1]} from "${moduleSpecifier}";`);
|
|
304
286
|
}
|
|
305
287
|
else if (imp.alias) {
|
|
@@ -313,6 +295,11 @@ class CodeGenerator {
|
|
|
313
295
|
if (program.imports.length > 0)
|
|
314
296
|
this.w.writeLine();
|
|
315
297
|
}
|
|
298
|
+
/**
|
|
299
|
+
* Heuristic for non-scoped imports: if the first path segment resolves to an
|
|
300
|
+
* installed package in node_modules, treat the import as npm/ESM and strip
|
|
301
|
+
* the trailing symbol name from the module specifier.
|
|
302
|
+
*/
|
|
316
303
|
isLikelyNodeModuleImport(pathParts, sourceRoot) {
|
|
317
304
|
if (pathParts.length < 2)
|
|
318
305
|
return false;
|
|
@@ -330,12 +317,14 @@ class CodeGenerator {
|
|
|
330
317
|
return false;
|
|
331
318
|
}
|
|
332
319
|
// ── Top-level declarations ─────────────────────────────────────────────────
|
|
320
|
+
/** Emit a @deprecated JSDoc block if the declaration has @Nuked. */
|
|
333
321
|
emitAnnotations(mods) {
|
|
334
322
|
for (const ann of mods.annotations) {
|
|
335
323
|
if (ann.name === "Nuked") {
|
|
336
324
|
const reason = ann.args ? ` ${ann.args.replace(/^["']|["']$/g, "")}` : "";
|
|
337
325
|
this.w.writeIndentedLine(`/** @deprecated${reason} */`);
|
|
338
326
|
}
|
|
327
|
+
// Other annotations are silently ignored for now (pass-through model)
|
|
339
328
|
}
|
|
340
329
|
}
|
|
341
330
|
emitTopLevelDecl(decl) {
|
|
@@ -390,6 +379,7 @@ class CodeGenerator {
|
|
|
390
379
|
? `: ${this.emitTypeRef(decl.returnType)}`
|
|
391
380
|
: "";
|
|
392
381
|
if (!decl.body) {
|
|
382
|
+
// Abstract / interface method
|
|
393
383
|
this.w.writeIndentedLine(`${vis}${asyncKw}${decl.name}${typeParams}(${params})${retType};`);
|
|
394
384
|
return;
|
|
395
385
|
}
|
|
@@ -406,6 +396,7 @@ class CodeGenerator {
|
|
|
406
396
|
this.w.writeIndentedLine(`}`);
|
|
407
397
|
}
|
|
408
398
|
else {
|
|
399
|
+
// Expression body
|
|
409
400
|
if (memberOf) {
|
|
410
401
|
this.w.writeIndentedLine(`${vis}${asyncKw}${decl.name}${typeParams}(${params})${retType} {`);
|
|
411
402
|
}
|
|
@@ -446,19 +437,21 @@ class CodeGenerator {
|
|
|
446
437
|
this.w.writeIndentedLine(`}`);
|
|
447
438
|
}
|
|
448
439
|
}
|
|
449
|
-
// ── component declarations →
|
|
440
|
+
// ── component declarations → React function components ────────────────────
|
|
450
441
|
emitComponentDecl(decl) {
|
|
451
442
|
this.emitAnnotations(decl.modifiers);
|
|
452
443
|
this.hasComponents = true;
|
|
453
|
-
const vis = this.
|
|
454
|
-
|
|
444
|
+
const vis = this.exportPrefix(decl.modifiers);
|
|
445
|
+
// "children" is passed as the second positional argument by emitComponentCall,
|
|
446
|
+
// so it must NOT be destructured from the props object.
|
|
455
447
|
const propsParams = decl.params.filter((p) => p.name !== "children");
|
|
448
|
+
const hasChildren = decl.params.some((p) => p.name === "children");
|
|
456
449
|
// Props interface
|
|
457
|
-
if (
|
|
450
|
+
if (decl.params.length > 0) {
|
|
458
451
|
this.w.writeIndentedLine(`interface ${decl.name}Props {`);
|
|
459
452
|
this.w.pushIndent();
|
|
460
|
-
for (const p of
|
|
461
|
-
const optional = p.defaultValue ? "?" : "";
|
|
453
|
+
for (const p of decl.params) {
|
|
454
|
+
const optional = p.defaultValue || p.name === "children" ? "?" : "";
|
|
462
455
|
const typeStr = this.opts.emitTypes ? this.emitTypeRef(p.type) : "any";
|
|
463
456
|
this.w.writeIndentedLine(`readonly ${p.name}${optional}: ${typeStr};`);
|
|
464
457
|
}
|
|
@@ -466,32 +459,21 @@ class CodeGenerator {
|
|
|
466
459
|
this.w.writeIndentedLine(`}`);
|
|
467
460
|
this.w.writeLine();
|
|
468
461
|
}
|
|
469
|
-
const
|
|
470
|
-
? `{ ${
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
return `readonly ${p.name}${optional}: ${typeStr}`;
|
|
474
|
-
}).join("; ")} }`
|
|
475
|
-
: `${decl.name}Props`;
|
|
476
|
-
let signature = "";
|
|
477
|
-
if (propsParams.length > 0) {
|
|
478
|
-
signature = `{ ${propsParams.map((p) => p.name + (p.defaultValue ? ` = ${this.emitExpr(p.defaultValue)}` : "")).join(", ")} }: ${propsType}`;
|
|
479
|
-
}
|
|
480
|
-
else if (hasChildren) {
|
|
481
|
-
signature = "{}";
|
|
482
|
-
}
|
|
483
|
-
if (hasChildren) {
|
|
484
|
-
signature += signature ? ", children?: any" : "children?: any";
|
|
485
|
-
}
|
|
486
|
-
const sig = this.isInClass
|
|
487
|
-
? `${decl.name}(${signature})`
|
|
488
|
-
: `function ${decl.name}(${signature})`;
|
|
489
|
-
this.w.writeIndentedLine(`${vis}${sig} {`);
|
|
462
|
+
const propsDestructure = decl.params.length > 0
|
|
463
|
+
? `{ ${decl.params.map((p) => p.name + (p.defaultValue ? ` = ${this.emitExpr(p.defaultValue)}` : "")).join(", ")} }: ${decl.name}Props`
|
|
464
|
+
: "";
|
|
465
|
+
this.w.writeIndentedLine(`${vis}function ${decl.name}(${propsDestructure}) {`);
|
|
490
466
|
this.w.pushIndent();
|
|
491
467
|
this.emitComponentBlock(decl.body);
|
|
492
468
|
this.w.popIndent();
|
|
493
469
|
this.w.writeIndentedLine(`}`);
|
|
494
470
|
}
|
|
471
|
+
/**
|
|
472
|
+
* Emit the body of a `component fun` block.
|
|
473
|
+
* The last statement is emitted in tail position — if it is an expression,
|
|
474
|
+
* an if/else chain, or a when block, the innermost UI call is implicitly
|
|
475
|
+
* returned (implicit return of the last expression).
|
|
476
|
+
*/
|
|
495
477
|
emitComponentBlock(block) {
|
|
496
478
|
const stmts = block.statements;
|
|
497
479
|
for (let i = 0; i < stmts.length; i++) {
|
|
@@ -504,8 +486,10 @@ class CodeGenerator {
|
|
|
504
486
|
}
|
|
505
487
|
}
|
|
506
488
|
}
|
|
489
|
+
/** Emit a statement in tail position, adding implicit return where applicable. */
|
|
507
490
|
emitTailStmt(stmt) {
|
|
508
491
|
if (stmt.kind === "ExprStmt") {
|
|
492
|
+
// Implicit return: last expression in a component block
|
|
509
493
|
this.w.writeIndentedLine(`return ${this.emitExpr(stmt.expr)};`);
|
|
510
494
|
}
|
|
511
495
|
else if (stmt.kind === "IfStmt") {
|
|
@@ -518,6 +502,7 @@ class CodeGenerator {
|
|
|
518
502
|
this.emitStmt(stmt);
|
|
519
503
|
}
|
|
520
504
|
}
|
|
505
|
+
/** Emit a block in tail position, treating its last statement as a tail expression. */
|
|
521
506
|
emitTailBlock(block) {
|
|
522
507
|
const stmts = block.statements;
|
|
523
508
|
for (let i = 0; i < stmts.length; i++) {
|
|
@@ -530,6 +515,7 @@ class CodeGenerator {
|
|
|
530
515
|
}
|
|
531
516
|
}
|
|
532
517
|
}
|
|
518
|
+
/** Like emitIfStmt but with tail-return applied recursively to each branch body. */
|
|
533
519
|
emitIfStmtWithTailReturn(stmt) {
|
|
534
520
|
this.w.writeIndentedLine(`if (${this.emitExpr(stmt.condition)}) {`);
|
|
535
521
|
this.w.pushIndent();
|
|
@@ -552,6 +538,7 @@ class CodeGenerator {
|
|
|
552
538
|
this.w.writeIndentedLine(`}`);
|
|
553
539
|
}
|
|
554
540
|
}
|
|
541
|
+
/** Like emitWhenStmt but with tail-return applied recursively to each branch body. */
|
|
555
542
|
emitWhenStmtWithTailReturn(stmt) {
|
|
556
543
|
const subjectVar = stmt.subject ? "__when_subject__" : null;
|
|
557
544
|
if (stmt.subject) {
|
|
@@ -588,12 +575,15 @@ class CodeGenerator {
|
|
|
588
575
|
const superTypes = this.emitSuperTypesStr(decl.superTypes);
|
|
589
576
|
this.w.writeIndentedLine(`${vis}${abstract}class ${decl.name}${typeParams}${superTypes} {`);
|
|
590
577
|
this.w.pushIndent();
|
|
578
|
+
// The first supertype's delegateArgs become the `super(args)` call inside the constructor.
|
|
591
579
|
const superDelegateArgs = decl.superTypes[0]?.delegateArgs ?? null;
|
|
592
580
|
if (decl.primaryConstructor) {
|
|
593
581
|
const initBlocks = decl.body?.members.filter((m) => m.kind === "InitBlock") ?? [];
|
|
594
582
|
this.emitPrimaryConstructorProps(decl.primaryConstructor.params, initBlocks, superDelegateArgs);
|
|
595
583
|
}
|
|
596
584
|
else if (superDelegateArgs !== null) {
|
|
585
|
+
// No primary constructor but the supertype has explicit delegation args —
|
|
586
|
+
// emit a minimal constructor that forwards them to super().
|
|
597
587
|
const superArgsStr = superDelegateArgs.map((a) => this.emitExpr(a.value)).join(", ");
|
|
598
588
|
this.w.writeIndentedLine(`constructor() {`);
|
|
599
589
|
this.w.pushIndent();
|
|
@@ -616,20 +606,24 @@ class CodeGenerator {
|
|
|
616
606
|
const props = decl.primaryConstructor.params;
|
|
617
607
|
this.w.writeIndentedLine(`${vis}class ${decl.name}${typeParams}${superTypes} {`);
|
|
618
608
|
this.w.pushIndent();
|
|
609
|
+
// Discriminant for sealed class sub-types
|
|
619
610
|
if (kindName) {
|
|
620
611
|
this.w.writeIndentedLine(`readonly __kind = "${kindName}" as const;`);
|
|
621
612
|
}
|
|
613
|
+
// Constructor params → readonly properties
|
|
622
614
|
for (const p of props) {
|
|
623
615
|
const t = this.opts.emitTypes ? `: ${this.emitTypeRef(p.type)}` : "";
|
|
624
616
|
this.w.writeIndentedLine(`readonly ${p.name}${t};`);
|
|
625
617
|
}
|
|
626
618
|
this.w.writeLine();
|
|
619
|
+
// Constructor
|
|
627
620
|
const ctorParams = props.map((p) => {
|
|
628
621
|
const t = this.opts.emitTypes ? `: ${this.emitTypeRef(p.type)}` : "";
|
|
629
622
|
return `${p.name}${t}`;
|
|
630
623
|
}).join(", ");
|
|
631
624
|
this.w.writeIndentedLine(`constructor(${ctorParams}) {`);
|
|
632
625
|
this.w.pushIndent();
|
|
626
|
+
// Emit super() with actual delegation args (or empty call if delegateArgs is []).
|
|
633
627
|
const dataSuperDelegateArgs = decl.superTypes[0]?.delegateArgs ?? null;
|
|
634
628
|
if (dataSuperDelegateArgs !== null) {
|
|
635
629
|
const superArgsStr = dataSuperDelegateArgs.map((a) => this.emitExpr(a.value)).join(", ");
|
|
@@ -641,6 +635,7 @@ class CodeGenerator {
|
|
|
641
635
|
this.w.popIndent();
|
|
642
636
|
this.w.writeIndentedLine(`}`);
|
|
643
637
|
this.w.writeLine();
|
|
638
|
+
// copy()
|
|
644
639
|
const copyParams = props.map((p) => {
|
|
645
640
|
const t = this.opts.emitTypes ? `: ${this.emitTypeRef(p.type)}` : "";
|
|
646
641
|
return `${p.name}${t} = this.${p.name}`;
|
|
@@ -652,6 +647,7 @@ class CodeGenerator {
|
|
|
652
647
|
this.w.popIndent();
|
|
653
648
|
this.w.writeIndentedLine(`}`);
|
|
654
649
|
this.w.writeLine();
|
|
650
|
+
// equals()
|
|
655
651
|
this.runtimeSymbolsNeeded.add("jalvinEquals");
|
|
656
652
|
const eqChecks = props.map((p) => `jalvinEquals(this.${p.name}, other.${p.name})`).join(" && ") || "true";
|
|
657
653
|
this.w.writeIndentedLine(`equals(other: unknown): boolean {`);
|
|
@@ -661,6 +657,7 @@ class CodeGenerator {
|
|
|
661
657
|
this.w.popIndent();
|
|
662
658
|
this.w.writeIndentedLine(`}`);
|
|
663
659
|
this.w.writeLine();
|
|
660
|
+
// toString()
|
|
664
661
|
const toStringParts = props.map((p) => `${p.name}=\${this.${p.name}}`).join(", ");
|
|
665
662
|
this.w.writeIndentedLine(`toString(): string {`);
|
|
666
663
|
this.w.pushIndent();
|
|
@@ -668,6 +665,7 @@ class CodeGenerator {
|
|
|
668
665
|
this.w.popIndent();
|
|
669
666
|
this.w.writeIndentedLine(`}`);
|
|
670
667
|
this.w.writeLine();
|
|
668
|
+
// hashCode() — djb2
|
|
671
669
|
this.w.writeIndentedLine(`hashCode(): number {`);
|
|
672
670
|
this.w.pushIndent();
|
|
673
671
|
this.w.writeIndentedLine(`let h = 17;`);
|
|
@@ -703,6 +701,7 @@ class CodeGenerator {
|
|
|
703
701
|
}
|
|
704
702
|
}
|
|
705
703
|
}
|
|
704
|
+
// Emit the abstract base class (only non-subtype members in the body)
|
|
706
705
|
this.w.writeIndentedLine(`${vis}abstract class ${decl.name}${typeParams} {`);
|
|
707
706
|
this.w.pushIndent();
|
|
708
707
|
this.w.writeIndentedLine(`abstract readonly __kind: string;`);
|
|
@@ -715,6 +714,7 @@ class CodeGenerator {
|
|
|
715
714
|
this.w.writeLine();
|
|
716
715
|
if (subDecls.length === 0)
|
|
717
716
|
return;
|
|
717
|
+
// Emit non-object sub-declarations at module level (before namespace)
|
|
718
718
|
for (const sub of subDecls) {
|
|
719
719
|
if (sub.kind === "DataClassDecl") {
|
|
720
720
|
this.emitDataClassDecl(sub, sub.name);
|
|
@@ -725,10 +725,12 @@ class CodeGenerator {
|
|
|
725
725
|
this.w.writeLine();
|
|
726
726
|
}
|
|
727
727
|
}
|
|
728
|
+
// Emit namespace merging so SealedClass.SubType is accessible
|
|
728
729
|
this.w.writeIndentedLine(`${vis}namespace ${decl.name} {`);
|
|
729
730
|
this.w.pushIndent();
|
|
730
731
|
for (const sub of subDecls) {
|
|
731
732
|
if (sub.kind === "ObjectDecl") {
|
|
733
|
+
// Singleton object — emit inline in namespace with __kind discriminant
|
|
732
734
|
const superStr = sub.superTypes.length > 0
|
|
733
735
|
? this.emitSuperTypesStr(sub.superTypes)
|
|
734
736
|
: ` extends ${decl.name}`;
|
|
@@ -744,6 +746,7 @@ class CodeGenerator {
|
|
|
744
746
|
this.w.writeIndentedLine(`export const ${sub.name} = new (class${superStr} { ${membersStr} })();`);
|
|
745
747
|
}
|
|
746
748
|
else {
|
|
749
|
+
// DataClassDecl / ClassDecl — already emitted at module level, re-export
|
|
747
750
|
this.w.writeIndentedLine(`export { ${sub.name} };`);
|
|
748
751
|
}
|
|
749
752
|
}
|
|
@@ -754,8 +757,12 @@ class CodeGenerator {
|
|
|
754
757
|
emitEnumClassDecl(decl) {
|
|
755
758
|
const vis = this.exportPrefix(decl.modifiers);
|
|
756
759
|
const typeParams = this.emitTypeParamsStr(decl.typeParams);
|
|
760
|
+
// Emit the enum as a TypeScript class with static singleton instances.
|
|
761
|
+
// This preserves the `EnumClass.ENTRY` access pattern and allows
|
|
762
|
+
// methods/properties to be attached to entries.
|
|
757
763
|
this.w.writeIndentedLine(`${vis}class ${decl.name}${typeParams} {`);
|
|
758
764
|
this.w.pushIndent();
|
|
765
|
+
// Private constructor so entries are the only instances
|
|
759
766
|
if (decl.primaryConstructor && decl.primaryConstructor.params.length > 0) {
|
|
760
767
|
const ctorParams = decl.primaryConstructor.params
|
|
761
768
|
.map((p) => {
|
|
@@ -770,6 +777,7 @@ class CodeGenerator {
|
|
|
770
777
|
this.w.writeIndentedLine(`private constructor(readonly name: string, readonly ordinal: number) {}`);
|
|
771
778
|
}
|
|
772
779
|
this.w.writeLine();
|
|
780
|
+
// Static entry instances
|
|
773
781
|
for (let i = 0; i < decl.entries.length; i++) {
|
|
774
782
|
const entry = decl.entries[i];
|
|
775
783
|
const args = entry.args.length > 0
|
|
@@ -779,6 +787,7 @@ class CodeGenerator {
|
|
|
779
787
|
}
|
|
780
788
|
if (decl.entries.length > 0) {
|
|
781
789
|
this.w.writeLine();
|
|
790
|
+
// values() helper
|
|
782
791
|
const entryList = decl.entries.map((e) => `${decl.name}.${e.name}`).join(", ");
|
|
783
792
|
this.w.writeIndentedLine(`static values(): ${decl.name}[] { return [${entryList}]; }`);
|
|
784
793
|
this.w.writeIndentedLine(`static valueOf(name: string): ${decl.name} {`);
|
|
@@ -796,12 +805,14 @@ class CodeGenerator {
|
|
|
796
805
|
this.w.popIndent();
|
|
797
806
|
this.w.writeIndentedLine(`}`);
|
|
798
807
|
}
|
|
808
|
+
// ── destructuring declaration — val (a, b) = expr ─────────────────────────
|
|
799
809
|
emitDestructuringDecl(decl, _memberOf) {
|
|
800
810
|
const kw = decl.mutable ? "let" : "const";
|
|
801
811
|
const names = decl.names.map((n) => n ?? "_").join(", ");
|
|
802
812
|
const init = this.emitExpr(decl.initializer);
|
|
803
813
|
this.w.writeIndentedLine(`${kw} [${names}] = ${init};`);
|
|
804
814
|
}
|
|
815
|
+
// ── interface ──────────────────────────────────────────────────────────────
|
|
805
816
|
emitInterfaceDecl(decl) {
|
|
806
817
|
this.emitAnnotations(decl.modifiers);
|
|
807
818
|
const vis = this.exportPrefix(decl.modifiers);
|
|
@@ -819,9 +830,12 @@ class CodeGenerator {
|
|
|
819
830
|
this.w.popIndent();
|
|
820
831
|
this.w.writeIndentedLine(`}`);
|
|
821
832
|
}
|
|
833
|
+
// ── object declaration → singleton ────────────────────────────────────────
|
|
822
834
|
emitObjectDecl(decl) {
|
|
823
|
-
if (!decl.name)
|
|
835
|
+
if (!decl.name) {
|
|
836
|
+
// Anonymous object — emitted inline
|
|
824
837
|
return;
|
|
838
|
+
}
|
|
825
839
|
this.emitAnnotations(decl.modifiers);
|
|
826
840
|
const vis = this.exportPrefix(decl.modifiers);
|
|
827
841
|
const superTypes = this.emitSuperTypesStr(decl.superTypes);
|
|
@@ -831,11 +845,16 @@ class CodeGenerator {
|
|
|
831
845
|
this.w.popIndent();
|
|
832
846
|
this.w.writeIndentedLine(`})();`);
|
|
833
847
|
}
|
|
848
|
+
// ── type alias ─────────────────────────────────────────────────────────────
|
|
834
849
|
emitTypeAliasDecl(decl) {
|
|
835
850
|
const vis = this.exportPrefix(decl.modifiers);
|
|
836
851
|
const typeParams = this.emitTypeParamsStr(decl.typeParams);
|
|
837
852
|
this.w.writeIndentedLine(`${vis}type ${decl.name}${typeParams} = ${this.emitTypeRef(decl.type)};`);
|
|
838
853
|
}
|
|
854
|
+
// ── extension functions ────────────────────────────────────────────────────
|
|
855
|
+
// Emitted as standalone free functions with receiver as explicit first param.
|
|
856
|
+
// For class types, also patched onto the prototype for dot-call syntax.
|
|
857
|
+
// For primitive types, call sites are rewritten via `primitiveExtensions`.
|
|
839
858
|
emitExtensionFunDecl(decl) {
|
|
840
859
|
const isSuspend = AST.isSuspend(decl.modifiers);
|
|
841
860
|
const asyncKw = isSuspend ? "async " : "";
|
|
@@ -846,6 +865,9 @@ class CodeGenerator {
|
|
|
846
865
|
? `: ${this.emitTypeRef(decl.returnType)}`
|
|
847
866
|
: "";
|
|
848
867
|
const fnName = `__ext_${receiverType.replace(/[^a-zA-Z0-9_]/g, "_")}_${decl.name}`;
|
|
868
|
+
// Emit as a free function: receiver is the first explicit parameter `$receiver`.
|
|
869
|
+
// Inside the function body, `this` is rebound to `$receiver` so Jalvin's
|
|
870
|
+
// `this.prop` references work correctly.
|
|
849
871
|
this.w.writeIndentedLine(`${asyncKw}function ${fnName}${typeParams}($receiver: ${receiverType}${params ? ", " + params : ""})${retType} {`);
|
|
850
872
|
this.w.pushIndent();
|
|
851
873
|
if (decl.body.kind === "Block") {
|
|
@@ -864,12 +886,14 @@ class CodeGenerator {
|
|
|
864
886
|
const simpleReceiverName = this.receiverClassName(decl.receiver);
|
|
865
887
|
if (simpleReceiverName) {
|
|
866
888
|
if (PRIMITIVE_TYPES.has(simpleReceiverName)) {
|
|
889
|
+
// Register for call-site rewriting — cannot patch primitive prototypes
|
|
867
890
|
if (!this.primitiveExtensions.has(simpleReceiverName)) {
|
|
868
891
|
this.primitiveExtensions.set(simpleReceiverName, new Map());
|
|
869
892
|
}
|
|
870
893
|
this.primitiveExtensions.get(simpleReceiverName).set(decl.name, fnName);
|
|
871
894
|
}
|
|
872
895
|
else {
|
|
896
|
+
// Class types — prototype monkey-patch for dot-call syntax
|
|
873
897
|
this.w.writeIndentedLine(`// Extension: ${receiverType}.${decl.name}`);
|
|
874
898
|
this.w.writeIndentedLine(`(${simpleReceiverName}.prototype as any).${decl.name} = function(this: ${receiverType}, ...args: unknown[]) { return (${fnName} as Function)(this, ...args); };`);
|
|
875
899
|
}
|
|
@@ -882,6 +906,11 @@ class CodeGenerator {
|
|
|
882
906
|
return ref.base.name[ref.base.name.length - 1] ?? null;
|
|
883
907
|
return null;
|
|
884
908
|
}
|
|
909
|
+
/**
|
|
910
|
+
* Maps a JType to the primitive receiver type name used as a key
|
|
911
|
+
* in `primitiveExtensions` (e.g. JType `{ tag: "string" }` → `"String"`).
|
|
912
|
+
* Returns null for non-primitive / unknown types.
|
|
913
|
+
*/
|
|
885
914
|
jTypeToReceiverName(type) {
|
|
886
915
|
const tagToReceiverName = {
|
|
887
916
|
string: "String",
|
|
@@ -901,16 +930,19 @@ class CodeGenerator {
|
|
|
901
930
|
m.name === "invoke" &&
|
|
902
931
|
m.modifiers.modifiers.includes("operator"));
|
|
903
932
|
}
|
|
933
|
+
// ── property declaration ───────────────────────────────────────────────────
|
|
904
934
|
emitPropertyDecl(decl, member, isLocal = false) {
|
|
905
935
|
this.emitAnnotations(decl.modifiers);
|
|
906
936
|
const vis = member ? this.visibilityPrefix(decl.modifiers) : isLocal ? "" : this.exportPrefix(decl.modifiers);
|
|
907
937
|
const isConst = decl.modifiers.modifiers.includes("const");
|
|
908
938
|
const isLateinit = decl.modifiers.modifiers.includes("lateinit");
|
|
939
|
+
// `const val` → `const` at module level; inside classes treated as `static readonly`
|
|
909
940
|
const kw = member
|
|
910
941
|
? (decl.mutable ? "" : "readonly ")
|
|
911
942
|
: (isConst ? "const " : decl.mutable ? "let " : "const ");
|
|
912
943
|
const type = decl.type && this.opts.emitTypes ? `: ${this.emitTypeRef(decl.type)}` : "";
|
|
913
944
|
if (decl.delegate) {
|
|
945
|
+
// Delegated property — wrap in a getter/setter pair using the delegate
|
|
914
946
|
this.runtimeSymbolsNeeded.add("delegate");
|
|
915
947
|
const delegateExpr = this.emitExpr(decl.delegate);
|
|
916
948
|
if (member) {
|
|
@@ -920,22 +952,28 @@ class CodeGenerator {
|
|
|
920
952
|
}
|
|
921
953
|
}
|
|
922
954
|
else {
|
|
955
|
+
// Non-member delegate: resolve via delegate().getValue() so the identifier
|
|
956
|
+
// holds the delegated value, not the raw delegate wrapper object.
|
|
923
957
|
this.w.writeIndentedLine(`${kw}${decl.name}${type} = delegate(${delegateExpr}, "${decl.name}", null).getValue();`);
|
|
924
958
|
}
|
|
925
959
|
return;
|
|
926
960
|
}
|
|
927
|
-
const init = decl.initializer ? ` = ${this.emitExpr(decl.initializer)}` : "";
|
|
961
|
+
const init = decl.initializer ? ` = ${this.emitExpr(decl.initializer)}` : (isLateinit ? "" : "");
|
|
928
962
|
if (member && !decl.getter) {
|
|
963
|
+
// Skip backing field when a getter is defined — TypeScript forbids both
|
|
964
|
+
// `readonly name: T` and `get name()` with the same name in a class.
|
|
929
965
|
const modStr = isConst ? "static readonly " : decl.mutable ? "" : "readonly ";
|
|
930
966
|
this.w.writeIndentedLine(`${vis}${modStr}${decl.name}${type}${init};`);
|
|
931
967
|
}
|
|
932
968
|
else if (!member) {
|
|
933
969
|
this.w.writeIndentedLine(`${vis}${kw}${decl.name}${type}${init};`);
|
|
934
970
|
}
|
|
935
|
-
if (decl.getter)
|
|
971
|
+
if (decl.getter) {
|
|
936
972
|
this.emitPropertyAccessor("get", decl.name, decl.getter, member, type);
|
|
937
|
-
|
|
973
|
+
}
|
|
974
|
+
if (decl.setter) {
|
|
938
975
|
this.emitPropertyAccessor("set", decl.name, decl.setter, member, type);
|
|
976
|
+
}
|
|
939
977
|
}
|
|
940
978
|
emitPropertyAccessor(kind, name, acc, member, type) {
|
|
941
979
|
const vis = this.visibilityPrefix(acc.modifiers);
|
|
@@ -955,19 +993,14 @@ class CodeGenerator {
|
|
|
955
993
|
this.w.popIndent();
|
|
956
994
|
this.w.writeIndentedLine(`}`);
|
|
957
995
|
}
|
|
996
|
+
// ── class body ─────────────────────────────────────────────────────────────
|
|
958
997
|
emitClassBody(body) {
|
|
959
|
-
const
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
this.emitClassMember(member);
|
|
966
|
-
this.w.writeLine();
|
|
967
|
-
}
|
|
968
|
-
}
|
|
969
|
-
finally {
|
|
970
|
-
this.isInClass = old;
|
|
998
|
+
for (const member of body.members) {
|
|
999
|
+
// InitBlocks are emitted inside the constructor by emitPrimaryConstructorProps
|
|
1000
|
+
if (member.kind === "InitBlock")
|
|
1001
|
+
continue;
|
|
1002
|
+
this.emitClassMember(member);
|
|
1003
|
+
this.w.writeLine();
|
|
971
1004
|
}
|
|
972
1005
|
}
|
|
973
1006
|
emitClassMember(member) {
|
|
@@ -999,6 +1032,7 @@ class CodeGenerator {
|
|
|
999
1032
|
case "CompanionObject":
|
|
1000
1033
|
this.emitCompanionObject(member);
|
|
1001
1034
|
break;
|
|
1035
|
+
case "InitBlock": /* handled in constructor */ break;
|
|
1002
1036
|
case "SecondaryConstructor":
|
|
1003
1037
|
this.emitSecondaryConstructor(member);
|
|
1004
1038
|
break;
|
|
@@ -1015,13 +1049,16 @@ class CodeGenerator {
|
|
|
1015
1049
|
this.w.writeIndentedLine(`${ro}${p.name}${type};`);
|
|
1016
1050
|
}
|
|
1017
1051
|
}
|
|
1052
|
+
// Constructor
|
|
1018
1053
|
const ctorParams = params.map((p) => {
|
|
1054
|
+
const ro = p.propertyKind === "val" ? "readonly " : p.propertyKind === "var" ? "" : "";
|
|
1019
1055
|
const type = this.opts.emitTypes ? `: ${this.emitTypeRef(p.type)}` : "";
|
|
1020
1056
|
const def = p.defaultValue ? ` = ${this.emitExpr(p.defaultValue)}` : "";
|
|
1021
1057
|
return `${p.name}${type}${def}`;
|
|
1022
1058
|
});
|
|
1023
1059
|
this.w.writeIndentedLine(`constructor(${ctorParams.join(", ")}) {`);
|
|
1024
1060
|
this.w.pushIndent();
|
|
1061
|
+
// super() call must be the first statement if there is a supertype.
|
|
1025
1062
|
if (superDelegateArgs !== null && superDelegateArgs !== undefined) {
|
|
1026
1063
|
const superArgsStr = superDelegateArgs.map((a) => this.emitExpr(a.value)).join(", ");
|
|
1027
1064
|
this.w.writeIndentedLine(`super(${superArgsStr});`);
|
|
@@ -1031,13 +1068,17 @@ class CodeGenerator {
|
|
|
1031
1068
|
this.w.writeIndentedLine(`this.${p.name} = ${p.name};`);
|
|
1032
1069
|
}
|
|
1033
1070
|
}
|
|
1034
|
-
|
|
1071
|
+
// Emit init{} blocks inside the constructor, in declaration order
|
|
1072
|
+
for (const ib of (initBlocks ?? [])) {
|
|
1035
1073
|
this.emitBlock(ib.body);
|
|
1074
|
+
}
|
|
1036
1075
|
this.w.popIndent();
|
|
1037
1076
|
this.w.writeIndentedLine(`}`);
|
|
1038
1077
|
this.w.writeLine();
|
|
1039
1078
|
}
|
|
1040
1079
|
emitCompanionObject(co) {
|
|
1080
|
+
// Emit companion object members as `static` members of the outer class so that
|
|
1081
|
+
// `MyClass.factoryMethod()` works directly (standard semantics).
|
|
1041
1082
|
for (const member of co.body.members) {
|
|
1042
1083
|
if (member.kind === "FunDecl") {
|
|
1043
1084
|
const vis = this.visibilityPrefix(member.modifiers);
|
|
@@ -1082,9 +1123,11 @@ class CodeGenerator {
|
|
|
1082
1123
|
this.w.popIndent();
|
|
1083
1124
|
this.w.writeIndentedLine(`}`);
|
|
1084
1125
|
}
|
|
1126
|
+
// ── Statements ─────────────────────────────────────────────────────────────
|
|
1085
1127
|
emitBlock(block) {
|
|
1086
|
-
for (const stmt of block.statements)
|
|
1128
|
+
for (const stmt of block.statements) {
|
|
1087
1129
|
this.emitStmt(stmt);
|
|
1130
|
+
}
|
|
1088
1131
|
}
|
|
1089
1132
|
emitStmt(stmt) {
|
|
1090
1133
|
switch (stmt.kind) {
|
|
@@ -1173,6 +1216,8 @@ class CodeGenerator {
|
|
|
1173
1216
|
}
|
|
1174
1217
|
}
|
|
1175
1218
|
emitWhenStmt(stmt) {
|
|
1219
|
+
// when(subject) { is Foo -> ... else -> ... }
|
|
1220
|
+
// Compiles to a series of if/else-if chains
|
|
1176
1221
|
const subjectVar = stmt.subject ? "__when_subject__" : null;
|
|
1177
1222
|
if (stmt.subject) {
|
|
1178
1223
|
const binding = stmt.subject.binding ?? subjectVar;
|
|
@@ -1189,10 +1234,12 @@ class CodeGenerator {
|
|
|
1189
1234
|
}
|
|
1190
1235
|
first = false;
|
|
1191
1236
|
this.w.pushIndent();
|
|
1192
|
-
if (branch.body.kind === "Block")
|
|
1237
|
+
if (branch.body.kind === "Block") {
|
|
1193
1238
|
this.emitBlock(branch.body);
|
|
1194
|
-
|
|
1239
|
+
}
|
|
1240
|
+
else {
|
|
1195
1241
|
this.w.writeIndentedLine(`${this.emitExpr(branch.body)};`);
|
|
1242
|
+
}
|
|
1196
1243
|
this.w.popIndent();
|
|
1197
1244
|
}
|
|
1198
1245
|
this.w.writeIndentedLine(`}`);
|
|
@@ -1200,6 +1247,8 @@ class CodeGenerator {
|
|
|
1200
1247
|
emitWhenCondition(cond, subject) {
|
|
1201
1248
|
switch (cond.kind) {
|
|
1202
1249
|
case "WhenIsCondition": {
|
|
1250
|
+
// Qualified name (e.g. UiState.Loading) → use __kind discriminant
|
|
1251
|
+
// Single name (e.g. BibiError) → use instanceof
|
|
1203
1252
|
const isQualified = cond.type.kind === "SimpleTypeRef" && cond.type.name.length > 1;
|
|
1204
1253
|
const check = isQualified
|
|
1205
1254
|
? `(${subject} as any).__kind === "${cond.type.kind === "SimpleTypeRef" ? cond.type.name[cond.type.name.length - 1] : ""}"`
|
|
@@ -1229,6 +1278,7 @@ class CodeGenerator {
|
|
|
1229
1278
|
}
|
|
1230
1279
|
else {
|
|
1231
1280
|
const { key, value } = stmt.binding;
|
|
1281
|
+
// Use .entries() for JS Map (Map<K,V>), Object.entries() for plain objects
|
|
1232
1282
|
const iterableType = this.typeMap.get(stmt.iterable);
|
|
1233
1283
|
const isMap = iterableType?.tag === "class" && iterableType.name === "Map";
|
|
1234
1284
|
const entries = isMap ? `${iter}.entries()` : `Object.entries(${iter})`;
|
|
@@ -1247,6 +1297,9 @@ class CodeGenerator {
|
|
|
1247
1297
|
for (const c of stmt.catches) {
|
|
1248
1298
|
this.w.writeIndentedLine(`} catch (${c.name}: unknown) {`);
|
|
1249
1299
|
this.w.pushIndent();
|
|
1300
|
+
// Narrow type — always emit the guard so catch clauses are type-safe
|
|
1301
|
+
// regardless of the emitTypes option (which controls explicit type annotations,
|
|
1302
|
+
// not runtime type checks).
|
|
1250
1303
|
this.w.writeIndentedLine(`if (!(${c.name} instanceof ${this.emitTypeRef(c.type)})) throw ${c.name};`);
|
|
1251
1304
|
this.emitBlock(c.body);
|
|
1252
1305
|
this.w.popIndent();
|
|
@@ -1259,6 +1312,7 @@ class CodeGenerator {
|
|
|
1259
1312
|
}
|
|
1260
1313
|
this.w.writeIndentedLine(`}`);
|
|
1261
1314
|
}
|
|
1315
|
+
// ── Expressions ────────────────────────────────────────────────────────────
|
|
1262
1316
|
emitExpr(expr) {
|
|
1263
1317
|
switch (expr.kind) {
|
|
1264
1318
|
case "IntLiteralExpr": return String(expr.value);
|
|
@@ -1268,12 +1322,17 @@ class CodeGenerator {
|
|
|
1268
1322
|
case "BooleanLiteralExpr": return String(expr.value);
|
|
1269
1323
|
case "NullLiteralExpr": return "null";
|
|
1270
1324
|
case "StringLiteralExpr":
|
|
1271
|
-
return expr.raw
|
|
1325
|
+
return expr.raw
|
|
1326
|
+
? `\`${expr.value}\``
|
|
1327
|
+
: JSON.stringify(expr.value);
|
|
1272
1328
|
case "StringTemplateExpr":
|
|
1273
1329
|
return this.emitStringTemplate(expr);
|
|
1274
1330
|
case "NameExpr":
|
|
1275
|
-
|
|
1331
|
+
// Ensure Int/Long companion objects are imported from the runtime
|
|
1332
|
+
if (expr.name === "Int" || expr.name === "Long") {
|
|
1276
1333
|
this.runtimeSymbolsNeeded.add(expr.name);
|
|
1334
|
+
}
|
|
1335
|
+
// Unit singleton emits as undefined (void semantics)
|
|
1277
1336
|
if (expr.name === "Unit")
|
|
1278
1337
|
return "undefined";
|
|
1279
1338
|
return expr.name;
|
|
@@ -1289,23 +1348,34 @@ class CodeGenerator {
|
|
|
1289
1348
|
if (opMethod) {
|
|
1290
1349
|
const l = this.emitExpr(expr.left);
|
|
1291
1350
|
const r = this.emitExpr(expr.right);
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1351
|
+
// `compareTo` overloads: wrap result in a comparison against 0
|
|
1352
|
+
if (opMethod === "compareTo") {
|
|
1353
|
+
const cmpOp = expr.op;
|
|
1354
|
+
return `(${l}.compareTo(${r}) ${cmpOp} 0)`;
|
|
1355
|
+
}
|
|
1356
|
+
// `contains` overloads: `element in collection` → `collection.contains(element)`
|
|
1357
|
+
if (opMethod === "contains") {
|
|
1295
1358
|
return `${r}.contains(${l})`;
|
|
1296
|
-
|
|
1359
|
+
}
|
|
1360
|
+
if (opMethod === "!contains") {
|
|
1297
1361
|
return `!${r}.contains(${l})`;
|
|
1362
|
+
}
|
|
1363
|
+
// Generic operator overload: emit as method call `left.method(right)`
|
|
1298
1364
|
return `${l}.${opMethod}(${r})`;
|
|
1299
1365
|
}
|
|
1366
|
+
// `==` → structural equality, `!=` → negation
|
|
1300
1367
|
if (expr.op === "==" || expr.op === "!=") {
|
|
1301
1368
|
this.runtimeSymbolsNeeded.add("jalvinEquals");
|
|
1302
1369
|
const eq = `jalvinEquals(${this.emitExpr(expr.left)}, ${this.emitExpr(expr.right)})`;
|
|
1303
1370
|
return expr.op === "==" ? eq : `!${eq}`;
|
|
1304
1371
|
}
|
|
1305
|
-
|
|
1372
|
+
// `in` / `!in` without a user-defined operator: use JS `in` / array includes
|
|
1373
|
+
if (expr.op === "in") {
|
|
1306
1374
|
return `${this.emitExpr(expr.right)}.includes?.(${this.emitExpr(expr.left)}) ?? (${this.emitExpr(expr.left)} in ${this.emitExpr(expr.right)})`;
|
|
1307
|
-
|
|
1375
|
+
}
|
|
1376
|
+
if (expr.op === "!in") {
|
|
1308
1377
|
return `!(${this.emitExpr(expr.right)}.includes?.(${this.emitExpr(expr.left)}) ?? (${this.emitExpr(expr.left)} in ${this.emitExpr(expr.right)}))`;
|
|
1378
|
+
}
|
|
1309
1379
|
return `(${this.emitExpr(expr.left)} ${this.binaryOpStr(expr.op)} ${this.emitExpr(expr.right)})`;
|
|
1310
1380
|
}
|
|
1311
1381
|
case "AssignExpr":
|
|
@@ -1316,81 +1386,124 @@ class CodeGenerator {
|
|
|
1316
1386
|
return expr.prefix
|
|
1317
1387
|
? `${expr.op}${this.emitExpr(expr.target)}`
|
|
1318
1388
|
: `${this.emitExpr(expr.target)}${expr.op}`;
|
|
1319
|
-
case "MemberExpr":
|
|
1320
|
-
|
|
1321
|
-
case "
|
|
1389
|
+
case "MemberExpr":
|
|
1390
|
+
return `${this.emitExpr(expr.target)}.${expr.member}`;
|
|
1391
|
+
case "SafeMemberExpr":
|
|
1392
|
+
return `${this.emitExpr(expr.target)}?.${expr.member}`;
|
|
1393
|
+
case "IndexExpr":
|
|
1394
|
+
return `${this.emitExpr(expr.target)}[${this.emitExpr(expr.index)}]`;
|
|
1322
1395
|
case "NotNullExpr":
|
|
1396
|
+
// Runtime check
|
|
1323
1397
|
this.runtimeSymbolsNeeded.add("notNull");
|
|
1324
1398
|
return `notNull(${this.emitExpr(expr.expr)})`;
|
|
1325
1399
|
case "SafeCallExpr": {
|
|
1400
|
+
// x?.() — safe invocation of a nullable function value
|
|
1326
1401
|
const callee = this.emitExpr(expr.callee);
|
|
1327
|
-
const restArgs =
|
|
1402
|
+
const restArgs = [];
|
|
1403
|
+
for (const a of expr.args) {
|
|
1404
|
+
restArgs.push(a.spread ? `...${this.emitExpr(a.value)}` : this.emitExpr(a.value));
|
|
1405
|
+
}
|
|
1328
1406
|
if (expr.trailingLambda)
|
|
1329
1407
|
restArgs.push(this.emitLambdaExpr(expr.trailingLambda));
|
|
1330
1408
|
return `${callee}?.(${restArgs.join(", ")})`;
|
|
1331
1409
|
}
|
|
1332
|
-
case "ElvisExpr":
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
case "
|
|
1337
|
-
|
|
1338
|
-
case "
|
|
1339
|
-
|
|
1340
|
-
case "
|
|
1410
|
+
case "ElvisExpr": {
|
|
1411
|
+
// left ?? right (null coalescing)
|
|
1412
|
+
return `(${this.emitExpr(expr.left)} ?? ${this.emitExpr(expr.right)})`;
|
|
1413
|
+
}
|
|
1414
|
+
case "CallExpr":
|
|
1415
|
+
return this.emitCallExpr(expr);
|
|
1416
|
+
case "LambdaExpr":
|
|
1417
|
+
return this.emitLambdaExpr(expr);
|
|
1418
|
+
case "IfExpr":
|
|
1419
|
+
return this.emitIfExpr(expr);
|
|
1420
|
+
case "WhenExpr":
|
|
1421
|
+
return this.emitWhenExpr(expr);
|
|
1422
|
+
case "TryCatchExpr":
|
|
1423
|
+
return this.emitTryCatchExpr(expr);
|
|
1424
|
+
case "TypeCheckExpr":
|
|
1425
|
+
return `${expr.negated ? "!(" : ""}${this.emitExpr(expr.expr)} instanceof ${this.emitTypeRef(expr.type)}${expr.negated ? ")" : ""}`;
|
|
1426
|
+
case "TypeCastExpr":
|
|
1427
|
+
// Unsafe cast — emit as `(expr as Type)` which is stripped at runtime
|
|
1428
|
+
return `(${this.emitExpr(expr.expr)} as unknown as ${this.emitTypeRef(expr.type)})`;
|
|
1429
|
+
case "SafeCastExpr": {
|
|
1341
1430
|
this.runtimeSymbolsNeeded.add("safeCast");
|
|
1342
1431
|
return `safeCast(${this.emitExpr(expr.expr)}, ${this.emitTypeRef(expr.type)})`;
|
|
1343
|
-
|
|
1432
|
+
}
|
|
1433
|
+
case "RangeExpr": {
|
|
1344
1434
|
this.runtimeSymbolsNeeded.add("range");
|
|
1345
1435
|
return `range(${this.emitExpr(expr.from)}, ${this.emitExpr(expr.to)}, ${expr.inclusive})`;
|
|
1436
|
+
}
|
|
1346
1437
|
case "LaunchExpr": {
|
|
1438
|
+
// fire-and-forget → void IIFE
|
|
1347
1439
|
const stmts = this.captureBlock(expr.body);
|
|
1348
1440
|
return `(async () => { ${stmts} })()`;
|
|
1349
1441
|
}
|
|
1350
1442
|
case "AsyncExpr": {
|
|
1443
|
+
// returns Promise<T>
|
|
1351
1444
|
const stmts = this.captureBlock(expr.body);
|
|
1352
1445
|
return `(async () => { ${stmts} })()`;
|
|
1353
1446
|
}
|
|
1354
|
-
case "CollectionLiteralExpr":
|
|
1355
|
-
|
|
1447
|
+
case "CollectionLiteralExpr":
|
|
1448
|
+
return this.emitCollectionLiteral(expr);
|
|
1449
|
+
case "ObjectExpr":
|
|
1450
|
+
return this.emitObjectExpr(expr);
|
|
1356
1451
|
case "ReturnExpr": {
|
|
1357
1452
|
const val = expr.value ? ` ${this.emitExpr(expr.value)}` : "";
|
|
1358
1453
|
return `((() => { return${val}; })())`;
|
|
1359
1454
|
}
|
|
1360
1455
|
case "BreakExpr": return "undefined /* break */";
|
|
1361
1456
|
case "ContinueExpr": return "undefined /* continue */";
|
|
1362
|
-
case "JsxElement":
|
|
1363
|
-
|
|
1457
|
+
case "JsxElement":
|
|
1458
|
+
return this.emitJsxElement(expr);
|
|
1459
|
+
default:
|
|
1460
|
+
return "undefined";
|
|
1364
1461
|
}
|
|
1365
1462
|
}
|
|
1463
|
+
// ── JSX ──────────────────────────────────────────────────────────────────
|
|
1366
1464
|
emitJsxElement(expr) {
|
|
1367
|
-
this.runtimeSymbolsNeeded.add("jalvinCreateElement");
|
|
1368
1465
|
const attrsStr = expr.attrs.length > 0
|
|
1369
|
-
? "
|
|
1370
|
-
: "
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1466
|
+
? " " + expr.attrs.map((a) => this.emitJsxAttr(a)).join(" ")
|
|
1467
|
+
: "";
|
|
1468
|
+
if (expr.children.length === 0) {
|
|
1469
|
+
return `<${expr.tag}${attrsStr} />`;
|
|
1470
|
+
}
|
|
1471
|
+
const children = expr.children.map((c) => this.emitJsxChild(c)).join("");
|
|
1472
|
+
return `<${expr.tag}${attrsStr}>${children}</${expr.tag}>`;
|
|
1375
1473
|
}
|
|
1376
1474
|
emitJsxAttr(attr) {
|
|
1377
|
-
|
|
1378
|
-
const
|
|
1379
|
-
|
|
1475
|
+
// Map HTML attribute names to React prop names
|
|
1476
|
+
const name = attr.name === "class" ? "className"
|
|
1477
|
+
: attr.name === "for" ? "htmlFor"
|
|
1478
|
+
: attr.name;
|
|
1479
|
+
if (attr.value === null)
|
|
1480
|
+
return name;
|
|
1481
|
+
if (typeof attr.value === "string")
|
|
1482
|
+
return `${name}="${attr.value}"`;
|
|
1483
|
+
return `${name}={${this.emitExpr(attr.value)}}`;
|
|
1380
1484
|
}
|
|
1381
1485
|
emitJsxChild(child) {
|
|
1382
1486
|
switch (child.kind) {
|
|
1383
1487
|
case "JsxElement": return this.emitJsxElement(child);
|
|
1384
|
-
case "JsxExprChild": return this.emitExpr(child.expr)
|
|
1385
|
-
case "JsxTextChild": return
|
|
1488
|
+
case "JsxExprChild": return `{${this.emitExpr(child.expr)}}`;
|
|
1489
|
+
case "JsxTextChild": return child.text;
|
|
1386
1490
|
}
|
|
1387
1491
|
}
|
|
1388
1492
|
emitStringTemplate(expr) {
|
|
1389
|
-
const inner = expr.parts.map((p) =>
|
|
1493
|
+
const inner = expr.parts.map((p) => {
|
|
1494
|
+
if (p.kind === "LiteralPart") {
|
|
1495
|
+
return p.value.replace(/`/g, "\\`").replace(/\\/g, "\\\\");
|
|
1496
|
+
}
|
|
1497
|
+
return `\${${this.emitExpr(p.expr)}}`;
|
|
1498
|
+
}).join("");
|
|
1390
1499
|
return `\`${inner}\``;
|
|
1391
1500
|
}
|
|
1392
1501
|
emitCallExpr(expr) {
|
|
1393
|
-
|
|
1502
|
+
// Rewrite infix numeric extension calls that JS numbers don't natively have:
|
|
1503
|
+
// `a.downTo(b)` → `downTo(a, b)` `a.until(b)` → `range(a, b, false)`
|
|
1504
|
+
// `range.step(n)` → `step(range, n)`
|
|
1505
|
+
if (expr.callee.kind === "MemberExpr" &&
|
|
1506
|
+
(expr.callee.member === "downTo" || expr.callee.member === "until" || expr.callee.member === "step")) {
|
|
1394
1507
|
const obj = this.emitExpr(expr.callee.target);
|
|
1395
1508
|
const arg = expr.args.length > 0 ? this.emitExpr(expr.args[0].value) : "0";
|
|
1396
1509
|
if (expr.callee.member === "downTo") {
|
|
@@ -1406,39 +1519,51 @@ class CodeGenerator {
|
|
|
1406
1519
|
return `step(${obj}, ${arg})`;
|
|
1407
1520
|
}
|
|
1408
1521
|
}
|
|
1522
|
+
// Rewrite calls on primitive-receiver extension functions.
|
|
1523
|
+
// `str.truncate(30)` where `String.truncate` is a user extension → `__ext_string_truncate(str, 30)`
|
|
1409
1524
|
if (expr.callee.kind === "MemberExpr") {
|
|
1410
1525
|
const scopeMember = expr.callee.member;
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
if (
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1526
|
+
// Scope function call rewriting: `x.let { ... }` → `let_(x, ...)`
|
|
1527
|
+
// `x.apply { ... }` → `apply(x, function(this:any) { ... })`
|
|
1528
|
+
if (scopeMember === "let" || scopeMember === "also" || scopeMember === "apply" || scopeMember === "run" || scopeMember === "takeIf" || scopeMember === "takeUnless") {
|
|
1529
|
+
const receiver = this.emitExpr(expr.callee.target);
|
|
1530
|
+
const lambda = expr.trailingLambda ??
|
|
1531
|
+
(expr.args.length === 1 && expr.args[0].value.kind === "LambdaExpr"
|
|
1532
|
+
? expr.args[0].value
|
|
1533
|
+
: null);
|
|
1534
|
+
if (lambda) {
|
|
1535
|
+
if (scopeMember === "let") {
|
|
1536
|
+
this.runtimeSymbolsNeeded.add("let_");
|
|
1537
|
+
return `let_(${receiver}, ${this.emitLambdaExpr(lambda)})`;
|
|
1538
|
+
}
|
|
1539
|
+
if (scopeMember === "also") {
|
|
1540
|
+
this.runtimeSymbolsNeeded.add("also");
|
|
1541
|
+
return `also(${receiver}, ${this.emitLambdaExpr(lambda)})`;
|
|
1542
|
+
}
|
|
1543
|
+
if (scopeMember === "apply") {
|
|
1544
|
+
this.runtimeSymbolsNeeded.add("apply");
|
|
1545
|
+
const body = this.captureBlockStatements(lambda.body);
|
|
1546
|
+
return `apply(${receiver}, function(this: any) { ${body} })`;
|
|
1547
|
+
}
|
|
1548
|
+
if (scopeMember === "run") {
|
|
1549
|
+
this.runtimeSymbolsNeeded.add("run_");
|
|
1550
|
+
const body = this.captureBlockStatements(lambda.body);
|
|
1551
|
+
return `run_(${receiver}, function(this: any) { ${body} })`;
|
|
1552
|
+
}
|
|
1553
|
+
if (scopeMember === "takeIf") {
|
|
1554
|
+
this.runtimeSymbolsNeeded.add("takeIf");
|
|
1555
|
+
return `takeIf(${receiver}, ${this.emitLambdaExpr(lambda)})`;
|
|
1556
|
+
}
|
|
1557
|
+
if (scopeMember === "takeUnless") {
|
|
1558
|
+
this.runtimeSymbolsNeeded.add("takeUnless");
|
|
1559
|
+
return `takeUnless(${receiver}, ${this.emitLambdaExpr(lambda)})`;
|
|
1560
|
+
}
|
|
1439
1561
|
}
|
|
1440
1562
|
}
|
|
1563
|
+
// `with(x) { ... }` is a free function handled via seedBuiltins; but rewrite if emitted as member
|
|
1441
1564
|
}
|
|
1565
|
+
// Rewrite calls on primitive-receiver extension functions.
|
|
1566
|
+
// `str.truncate(30)` where `String.truncate` is a user extension → `__ext_string_truncate(str, 30)`
|
|
1442
1567
|
if (expr.callee.kind === "MemberExpr") {
|
|
1443
1568
|
const receiverType = this.typeMap.get(expr.callee.target);
|
|
1444
1569
|
const memberName = expr.callee.member;
|
|
@@ -1458,69 +1583,105 @@ class CodeGenerator {
|
|
|
1458
1583
|
}
|
|
1459
1584
|
}
|
|
1460
1585
|
const callee = this.emitExpr(expr.callee);
|
|
1461
|
-
const typeArgs = expr.typeArgs.length > 0
|
|
1462
|
-
|
|
1586
|
+
const typeArgs = expr.typeArgs.length > 0
|
|
1587
|
+
? `<${expr.typeArgs.map((t) => t.star ? "*" : this.emitTypeRef(t.type)).join(", ")}>`
|
|
1588
|
+
: "";
|
|
1589
|
+
// Rewrite top-level `with(obj) { ... }` → `with_(obj, function(this: any) { ... })`
|
|
1590
|
+
if (expr.callee.kind === "NameExpr" && expr.callee.name === "with" &&
|
|
1591
|
+
(expr.trailingLambda || (expr.args.length === 2 && expr.args[1].value.kind === "LambdaExpr"))) {
|
|
1463
1592
|
const obj = expr.args.length > 0 ? this.emitExpr(expr.args[0].value) : "undefined";
|
|
1464
|
-
const lambda = expr.trailingLambda ??
|
|
1593
|
+
const lambda = expr.trailingLambda ??
|
|
1594
|
+
expr.args[1].value;
|
|
1465
1595
|
this.runtimeSymbolsNeeded.add("with_");
|
|
1466
1596
|
const body = this.captureBlockStatements(lambda.body);
|
|
1467
1597
|
return `with_(${obj}, function(this: any) { ${body} })`;
|
|
1468
1598
|
}
|
|
1469
|
-
|
|
1599
|
+
// Rewrite top-level `run { ... }` (no receiver) — just call the lambda
|
|
1600
|
+
if (expr.callee.kind === "NameExpr" && expr.callee.name === "run" &&
|
|
1601
|
+
expr.args.length === 0 && expr.trailingLambda) {
|
|
1470
1602
|
return `(${this.emitLambdaExpr(expr.trailingLambda)})()`;
|
|
1471
1603
|
}
|
|
1604
|
+
// Handle named arguments by reordering them to match positional parameters
|
|
1472
1605
|
const calleeType = this.typeMap.get(expr.callee);
|
|
1606
|
+
// Component call: Column(modifier = ...) { ... } → Column({ modifier: ... }, [children])
|
|
1607
|
+
// Also catches star-imported @jalvin/ui primitives (Row, Button, etc.) whose type is T_UNKNOWN
|
|
1608
|
+
//
|
|
1609
|
+
// Skip the component path when the typechecker resolved a func type with explicit paramNames.
|
|
1610
|
+
// That indicates a regular function (fun) or a built-in hook — both use positional calling.
|
|
1611
|
+
// component fun declarations intentionally omit paramNames from their func type.
|
|
1473
1612
|
const isKnownPositionalFunc = calleeType?.tag === "func" && calleeType.paramNames !== undefined;
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
isComponentLike = this.allComponentLikeNames.has(expr.callee.name) || (this.hasUiStarImport && (!calleeType || calleeType.tag === "unknown") && /^[A-Z]/.test(expr.callee.name));
|
|
1477
|
-
}
|
|
1478
|
-
else if (expr.callee.kind === "MemberExpr" || expr.callee.kind === "SafeMemberExpr") {
|
|
1479
|
-
isComponentLike = this.allComponentLikeNames.has(expr.callee.member);
|
|
1480
|
-
}
|
|
1481
|
-
if (!isKnownPositionalFunc && isComponentLike)
|
|
1613
|
+
if (!isKnownPositionalFunc && expr.callee.kind === "NameExpr" && (this.componentNames.has(expr.callee.name) ||
|
|
1614
|
+
(this.hasUiStarImport && (!calleeType || calleeType.tag === "unknown") && /^[A-Z]/.test(expr.callee.name)))) {
|
|
1482
1615
|
return this.emitComponentCall(expr);
|
|
1616
|
+
}
|
|
1483
1617
|
let finalArgs = [];
|
|
1484
1618
|
if (calleeType && calleeType.tag === "func" && calleeType.paramNames) {
|
|
1485
1619
|
const paramNames = calleeType.paramNames;
|
|
1486
1620
|
const argsByName = new Map();
|
|
1487
1621
|
const positionalArgs = [];
|
|
1488
1622
|
for (const arg of expr.args) {
|
|
1489
|
-
if (arg.name)
|
|
1623
|
+
if (arg.name) {
|
|
1490
1624
|
argsByName.set(arg.name, arg.spread ? `...${this.emitExpr(arg.value)}` : this.emitExpr(arg.value));
|
|
1491
|
-
|
|
1625
|
+
}
|
|
1626
|
+
else {
|
|
1492
1627
|
positionalArgs.push(arg.spread ? `...${this.emitExpr(arg.value)}` : this.emitExpr(arg.value));
|
|
1628
|
+
}
|
|
1493
1629
|
}
|
|
1630
|
+
// Reconstruct args based on paramNames
|
|
1494
1631
|
for (let i = 0; i < paramNames.length; i++) {
|
|
1495
1632
|
const name = paramNames[i];
|
|
1496
|
-
if (argsByName.has(name))
|
|
1633
|
+
if (argsByName.has(name)) {
|
|
1497
1634
|
finalArgs.push(argsByName.get(name));
|
|
1498
|
-
|
|
1635
|
+
}
|
|
1636
|
+
else if (i < positionalArgs.length) {
|
|
1499
1637
|
finalArgs.push(positionalArgs[i]);
|
|
1500
|
-
|
|
1638
|
+
}
|
|
1639
|
+
else {
|
|
1640
|
+
// Missing argument - JS default value logic will handle it if we pass undefined
|
|
1501
1641
|
finalArgs.push("undefined");
|
|
1642
|
+
}
|
|
1502
1643
|
}
|
|
1503
|
-
|
|
1644
|
+
// Handle trailing positional args if any
|
|
1645
|
+
if (positionalArgs.length > paramNames.length) {
|
|
1504
1646
|
finalArgs.push(...positionalArgs.slice(paramNames.length));
|
|
1647
|
+
}
|
|
1505
1648
|
}
|
|
1506
1649
|
else {
|
|
1507
|
-
|
|
1650
|
+
// Fallback for types we don't know param names for (like constructors or any)
|
|
1651
|
+
finalArgs = expr.args.map((a) => {
|
|
1652
|
+
return a.spread ? `...${this.emitExpr(a.value)}` : this.emitExpr(a.value);
|
|
1653
|
+
});
|
|
1508
1654
|
}
|
|
1509
|
-
if (expr.trailingLambda)
|
|
1655
|
+
if (expr.trailingLambda) {
|
|
1510
1656
|
finalArgs.push(this.emitLambdaExpr(expr.trailingLambda));
|
|
1511
|
-
|
|
1657
|
+
}
|
|
1658
|
+
// If the callee is a class instance (not a constructor / function), emit `.invoke(args)`
|
|
1659
|
+
if (calleeType &&
|
|
1660
|
+
calleeType.tag === "class" &&
|
|
1661
|
+
calleeType.decl &&
|
|
1662
|
+
this.classHasInvokeOperator(calleeType.decl)) {
|
|
1512
1663
|
return `${callee}.invoke(${finalArgs.join(", ")})`;
|
|
1513
1664
|
}
|
|
1514
|
-
|
|
1665
|
+
// Constructor calls: class names start with uppercase by convention
|
|
1666
|
+
if (this.isConstructorCall(expr.callee)) {
|
|
1515
1667
|
return `new ${callee}${typeArgs}(${finalArgs.join(", ")})`;
|
|
1668
|
+
}
|
|
1516
1669
|
return `${callee}${typeArgs}(${finalArgs.join(", ")})`;
|
|
1517
1670
|
}
|
|
1671
|
+
/**
|
|
1672
|
+
* For each non-scoped local import (e.g. `import src.views.MoveCounterView.MoveCounter`),
|
|
1673
|
+
* attempt to load and parse the corresponding .jalvin source file. If the named symbol is
|
|
1674
|
+
* a `component fun`, register it in `componentNames` and record its param names so that
|
|
1675
|
+
* call sites can emit the correct props-object invocation.
|
|
1676
|
+
*/
|
|
1518
1677
|
resolveLocalComponentImports(program, sourceRoot) {
|
|
1519
1678
|
this.localStarExports.clear();
|
|
1520
1679
|
for (const imp of program.imports) {
|
|
1680
|
+
// Skip scoped packages (@org/pkg)
|
|
1521
1681
|
if (imp.path[0]?.startsWith("@"))
|
|
1522
1682
|
continue;
|
|
1523
1683
|
if (imp.star) {
|
|
1684
|
+
// Local wildcard import: import src.module.* — load all exports from src/module.jalvin
|
|
1524
1685
|
if (imp.path.length < 1)
|
|
1525
1686
|
continue;
|
|
1526
1687
|
const candidatePath = nodePath.join(sourceRoot, imp.path.join("/") + ".jalvin");
|
|
@@ -1543,27 +1704,21 @@ class CodeGenerator {
|
|
|
1543
1704
|
continue;
|
|
1544
1705
|
exportedNames.push(name);
|
|
1545
1706
|
if (d.kind === "ComponentDecl") {
|
|
1546
|
-
this.
|
|
1547
|
-
this.allComponentLikeNames.add(name);
|
|
1707
|
+
this.componentNames.add(name);
|
|
1548
1708
|
this.componentParamNames.set(name, d.params.map((p) => p.name));
|
|
1549
1709
|
this.hasComponents = true;
|
|
1550
1710
|
}
|
|
1551
|
-
if (d.kind === "ClassDecl" && d.body) {
|
|
1552
|
-
for (const m of d.body.members) {
|
|
1553
|
-
if (m.kind === "ComponentDecl") {
|
|
1554
|
-
this.allComponentLikeNames.add(m.name);
|
|
1555
|
-
this.componentParamNames.set(m.name, m.params.map((p) => p.name));
|
|
1556
|
-
this.hasComponents = true;
|
|
1557
|
-
}
|
|
1558
|
-
}
|
|
1559
|
-
}
|
|
1560
1711
|
}
|
|
1561
|
-
if (exportedNames.length > 0)
|
|
1712
|
+
if (exportedNames.length > 0) {
|
|
1562
1713
|
this.localStarExports.set(moduleSpecifier, exportedNames);
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
catch {
|
|
1717
|
+
// Silently skip if the file cannot be read or parsed
|
|
1563
1718
|
}
|
|
1564
|
-
catch { }
|
|
1565
1719
|
continue;
|
|
1566
1720
|
}
|
|
1721
|
+
// Named import: last path segment is the symbol, preceding segments form the file path
|
|
1567
1722
|
if (imp.path.length < 2)
|
|
1568
1723
|
continue;
|
|
1569
1724
|
const rawSymbolName = imp.path[imp.path.length - 1];
|
|
@@ -1581,237 +1736,574 @@ class CodeGenerator {
|
|
|
1581
1736
|
const ast = (0, parser_js_1.parse)(tokens, candidatePath, diag, source);
|
|
1582
1737
|
if (diag.hasErrors)
|
|
1583
1738
|
continue;
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
this.
|
|
1588
|
-
this.componentParamNames.set(symbolName,
|
|
1739
|
+
// Look for a top-level ComponentDecl matching the imported symbol name
|
|
1740
|
+
const compDecl = ast.declarations.find((d) => d.kind === "ComponentDecl" && d.name === rawSymbolName);
|
|
1741
|
+
if (compDecl) {
|
|
1742
|
+
this.componentNames.add(symbolName);
|
|
1743
|
+
this.componentParamNames.set(symbolName, compDecl.params.map((p) => p.name));
|
|
1589
1744
|
this.hasComponents = true;
|
|
1590
1745
|
}
|
|
1591
|
-
else if (decl?.kind === "ClassDecl" && decl.body) {
|
|
1592
|
-
for (const m of decl.body.members) {
|
|
1593
|
-
if (m.kind === "ComponentDecl") {
|
|
1594
|
-
this.userComponentNames.add(m.name);
|
|
1595
|
-
this.allComponentLikeNames.add(m.name);
|
|
1596
|
-
this.componentParamNames.set(m.name, m.params.map((p) => p.name));
|
|
1597
|
-
this.hasComponents = true;
|
|
1598
|
-
}
|
|
1599
|
-
}
|
|
1600
|
-
}
|
|
1601
1746
|
}
|
|
1602
|
-
catch {
|
|
1747
|
+
catch {
|
|
1748
|
+
// Silently skip if the file cannot be read or parsed
|
|
1749
|
+
}
|
|
1603
1750
|
}
|
|
1604
1751
|
}
|
|
1752
|
+
/** Returns true if a call expression's callee looks like a class constructor.
|
|
1753
|
+
* In Jalvin, class names always start with an uppercase letter. */
|
|
1605
1754
|
isConstructorCall(calleeExpr) {
|
|
1606
1755
|
if (calleeExpr.kind === "NameExpr") {
|
|
1607
|
-
|
|
1756
|
+
// Names in componentNames are factory functions (UI primitives, components), not constructors
|
|
1757
|
+
if (this.componentNames.has(calleeExpr.name))
|
|
1608
1758
|
return false;
|
|
1759
|
+
// Class names start with uppercase; function names start with lowercase
|
|
1609
1760
|
return /^[A-Z]/.test(calleeExpr.name);
|
|
1610
1761
|
}
|
|
1611
1762
|
if (calleeExpr.kind === "MemberExpr") {
|
|
1612
|
-
|
|
1613
|
-
return false;
|
|
1763
|
+
// e.g. UiState.Success(data), Discount.Percentage(0.1)
|
|
1614
1764
|
return /^[A-Z]/.test(calleeExpr.member);
|
|
1615
1765
|
}
|
|
1616
1766
|
return false;
|
|
1617
1767
|
}
|
|
1768
|
+
// ── Component call → props object ──────────────────────────────────────
|
|
1769
|
+
/** Emit `Column(modifier = ...) { ... }` as `React.createElement(Column, { modifier: ... }, [children])` */
|
|
1618
1770
|
emitComponentCall(expr) {
|
|
1619
|
-
const
|
|
1620
|
-
const tagName = expr.callee.kind === "NameExpr" ? expr.callee.name : expr.callee.member;
|
|
1771
|
+
const tag = expr.callee.name;
|
|
1621
1772
|
const calleeType = this.typeMap.get(expr.callee);
|
|
1622
|
-
|
|
1773
|
+
// Param names from type checker (for same-file components) or resolved from imported files
|
|
1774
|
+
const paramNames = (calleeType?.tag === "func" ? calleeType.paramNames : undefined) ??
|
|
1775
|
+
this.componentParamNames.get(tag);
|
|
1776
|
+
// Build props object from named/positional args
|
|
1623
1777
|
const props = [];
|
|
1624
1778
|
for (let i = 0; i < expr.args.length; i++) {
|
|
1625
1779
|
const arg = expr.args[i];
|
|
1626
|
-
|
|
1780
|
+
// Prop name: explicit named arg > known param name > NameExpr variable name (shorthand)
|
|
1781
|
+
const propName = arg.name ??
|
|
1782
|
+
paramNames?.[i] ??
|
|
1783
|
+
(arg.value.kind === "NameExpr" ? arg.value.name : undefined);
|
|
1627
1784
|
if (!propName)
|
|
1628
1785
|
continue;
|
|
1629
1786
|
const val = this.emitExpr(arg.value);
|
|
1787
|
+
// Use JS shorthand `{ key }` when key and value are the same identifier
|
|
1630
1788
|
props.push(propName === val ? propName : `${propName}: ${val}`);
|
|
1631
1789
|
}
|
|
1632
1790
|
const propsStr = props.length > 0 ? `{ ${props.join(", ")} }` : "{}";
|
|
1633
1791
|
if (expr.trailingLambda) {
|
|
1634
1792
|
const children = this.emitLambdaBodyAsDomChildren(expr.trailingLambda);
|
|
1635
|
-
return
|
|
1793
|
+
return `React.createElement(${tag}, ${propsStr}, [${children}])`;
|
|
1636
1794
|
}
|
|
1637
|
-
return
|
|
1795
|
+
return `React.createElement(${tag}, ${propsStr})`;
|
|
1638
1796
|
}
|
|
1797
|
+
/** Collect each expression statement in a trailing-lambda body as DOM children.
|
|
1798
|
+
* For-loop statements are emitted as spread IIFEs so their dynamic output is preserved. */
|
|
1639
1799
|
emitLambdaBodyAsDomChildren(lambda) {
|
|
1640
1800
|
const parts = [];
|
|
1641
1801
|
for (const stmt of lambda.body) {
|
|
1642
1802
|
if (stmt.kind === "ExprStmt") {
|
|
1643
1803
|
parts.push(this.emitExpr(stmt.expr));
|
|
1644
1804
|
}
|
|
1645
|
-
else if (stmt.kind === "IfStmt") {
|
|
1646
|
-
const s = stmt;
|
|
1647
|
-
const cond = this.emitExpr(s.condition);
|
|
1648
|
-
const thenExprs = s.then.statements.filter((st) => st.kind === "ExprStmt").map((st) => this.emitExpr(st.expr));
|
|
1649
|
-
const elseExprs = s.else && s.else.kind === "Block" ? s.else.statements.filter((st) => st.kind === "ExprStmt").map((st) => this.emitExpr(st.expr)) : [];
|
|
1650
|
-
const thenStr = thenExprs.length > 0 ? `...(${cond} ? [${thenExprs.join(", ")}] : [])` : "";
|
|
1651
|
-
const elseStr = elseExprs.length > 0 ? `...(!(${cond}) ? [${elseExprs.join(", ")}] : [])` : "";
|
|
1652
|
-
if (thenStr)
|
|
1653
|
-
parts.push(thenStr);
|
|
1654
|
-
if (elseStr)
|
|
1655
|
-
parts.push(elseStr);
|
|
1656
|
-
}
|
|
1657
1805
|
else if (stmt.kind === "ForStmt") {
|
|
1658
1806
|
const s = stmt;
|
|
1659
1807
|
const iter = this.emitExpr(s.iterable);
|
|
1660
|
-
let bindingStr
|
|
1661
|
-
|
|
1808
|
+
let bindingStr;
|
|
1809
|
+
if (typeof s.binding === "string") {
|
|
1810
|
+
bindingStr = `const ${s.binding}`;
|
|
1811
|
+
}
|
|
1812
|
+
else if (s.binding.kind === "TupleDestructure") {
|
|
1813
|
+
const names = s.binding.names.map((n) => n ?? "_").join(", ");
|
|
1814
|
+
bindingStr = `const [${names}]`;
|
|
1815
|
+
}
|
|
1816
|
+
else {
|
|
1817
|
+
bindingStr = `const [${s.binding.key}, ${s.binding.value}]`;
|
|
1818
|
+
}
|
|
1819
|
+
const innerExprs = s.body.statements
|
|
1820
|
+
.filter((inner) => inner.kind === "ExprStmt")
|
|
1821
|
+
.map((inner) => this.emitExpr(inner.expr));
|
|
1662
1822
|
if (innerExprs.length > 0) {
|
|
1663
1823
|
const pushes = innerExprs.map((e) => `__c.push(${e});`).join(" ");
|
|
1664
1824
|
parts.push(`...(() => { const __c: any[] = []; for (${bindingStr} of ${iter}) { ${pushes} } return __c; })()`);
|
|
1665
1825
|
}
|
|
1666
1826
|
}
|
|
1827
|
+
// Other statement kinds (if, while, etc.) are not renderable as UI children inline
|
|
1667
1828
|
}
|
|
1668
1829
|
return parts.join(", ");
|
|
1669
1830
|
}
|
|
1670
1831
|
emitLambdaExpr(expr) {
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1832
|
+
// If no explicit params, emit `it` as the single implicit parameter
|
|
1833
|
+
// (Convention for single-argument lambdas)
|
|
1834
|
+
const params = expr.params.length === 0
|
|
1835
|
+
? "it"
|
|
1836
|
+
: expr.params.map((p) => p.name ?? "_").join(", ");
|
|
1837
|
+
const body = expr.body;
|
|
1838
|
+
if (body.length === 1 && body[0].kind === "ExprStmt") {
|
|
1839
|
+
return `(${params}) => ${this.emitExpr(body[0].expr)}`;
|
|
1840
|
+
}
|
|
1841
|
+
return `(${params}) => { ${body.map((s) => this.captureStmt(s)).join(" ")} }`;
|
|
1675
1842
|
}
|
|
1676
1843
|
emitIfExpr(expr) {
|
|
1677
1844
|
const cond = this.emitExpr(expr.condition);
|
|
1678
|
-
const thenStr = expr.then.kind === "Block"
|
|
1679
|
-
|
|
1845
|
+
const thenStr = expr.then.kind === "Block"
|
|
1846
|
+
? `(() => { ${this.captureBlockStatements(expr.then.statements)} })()`
|
|
1847
|
+
: this.emitExpr(expr.then);
|
|
1848
|
+
const elseExpr = expr.else;
|
|
1849
|
+
const elseStr = elseExpr.kind === "Block"
|
|
1850
|
+
? `(() => { ${this.captureBlockStatements(elseExpr.statements)} })()`
|
|
1851
|
+
: elseExpr.kind === "IfExpr"
|
|
1852
|
+
? this.emitIfExpr(elseExpr)
|
|
1853
|
+
: this.emitExpr(elseExpr);
|
|
1680
1854
|
return `(${cond} ? ${thenStr} : ${elseStr})`;
|
|
1681
1855
|
}
|
|
1682
1856
|
emitWhenExpr(expr) {
|
|
1857
|
+
// Compile as an IIFE with if/else chain
|
|
1683
1858
|
const parts = [];
|
|
1684
1859
|
const subject = expr.subject ? `const __s = ${this.emitExpr(expr.subject.expr)};` : "";
|
|
1685
1860
|
const subjectRef = expr.subject ? (expr.subject.binding ?? "__s") : "";
|
|
1686
1861
|
for (const branch of expr.branches) {
|
|
1687
|
-
|
|
1688
|
-
|
|
1862
|
+
if (branch.isElse) {
|
|
1863
|
+
const body = branch.body.kind === "Block"
|
|
1864
|
+
? this.captureBlock(branch.body)
|
|
1865
|
+
: `return ${this.emitExpr(branch.body)};`;
|
|
1689
1866
|
parts.push(`{ ${body} }`);
|
|
1690
|
-
|
|
1691
|
-
|
|
1867
|
+
}
|
|
1868
|
+
else {
|
|
1869
|
+
const cond = branch.conditions.map((c) => this.emitWhenCondition(c, subjectRef)).join(" || ");
|
|
1870
|
+
const body = branch.body.kind === "Block"
|
|
1871
|
+
? this.captureBlock(branch.body)
|
|
1872
|
+
: `return ${this.emitExpr(branch.body)};`;
|
|
1873
|
+
parts.push(`if (${cond}) { ${body} }`);
|
|
1874
|
+
}
|
|
1692
1875
|
}
|
|
1693
1876
|
return `(() => { ${subject} ${parts.join(" else ")} })()`;
|
|
1694
1877
|
}
|
|
1695
1878
|
emitTryCatchExpr(expr) {
|
|
1696
1879
|
const body = this.captureBlock(expr.body);
|
|
1697
|
-
const catches = expr.catches.map((c) =>
|
|
1880
|
+
const catches = expr.catches.map((c) => {
|
|
1881
|
+
const cb = this.captureBlock(c.body);
|
|
1882
|
+
return `catch (${c.name}) { ${cb} }`;
|
|
1883
|
+
}).join(" ");
|
|
1698
1884
|
const fin = expr.finally ? `finally { ${this.captureBlock(expr.finally)} }` : "";
|
|
1699
1885
|
return `(() => { try { ${body} } ${catches} ${fin} })()`;
|
|
1700
1886
|
}
|
|
1701
1887
|
emitCollectionLiteral(expr) {
|
|
1702
|
-
if (expr.collectionKind === "map")
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1888
|
+
if (expr.collectionKind === "map") {
|
|
1889
|
+
const entries = expr.elements.map((e) => {
|
|
1890
|
+
if ("kind" in e && e.kind === "MapEntry") {
|
|
1891
|
+
return `[${this.emitExpr(e.key)}, ${this.emitExpr(e.value)}]`;
|
|
1892
|
+
}
|
|
1893
|
+
return "null";
|
|
1894
|
+
});
|
|
1895
|
+
return `new Map([${entries.join(", ")}])`;
|
|
1896
|
+
}
|
|
1897
|
+
if (expr.collectionKind === "set") {
|
|
1898
|
+
const items = expr.elements.map((e) => this.emitExpr(e));
|
|
1899
|
+
return `new Set([${items.join(", ")}])`;
|
|
1900
|
+
}
|
|
1901
|
+
const items = expr.elements.map((e) => this.emitExpr(e));
|
|
1902
|
+
return `[${items.join(", ")}]`;
|
|
1707
1903
|
}
|
|
1708
1904
|
emitObjectExpr(expr) {
|
|
1709
1905
|
const superType = expr.superTypes[0];
|
|
1710
1906
|
const ext = superType ? ` extends ${this.emitTypeRef(superType.type)}` : "";
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1907
|
+
const members = expr.body.members.map((m) => this.captureClassMember(m)).join(" ");
|
|
1908
|
+
return `(new (class${ext} { ${members} })())`;
|
|
1909
|
+
}
|
|
1910
|
+
// ── capture helpers (emit to temp string) ─────────────────────────────────
|
|
1911
|
+
captureBlock(block) {
|
|
1912
|
+
return block.statements.map((s) => this.captureStmt(s)).join(" ");
|
|
1913
|
+
}
|
|
1914
|
+
captureBlockStatements(stmts) {
|
|
1915
|
+
return stmts.map((s) => this.captureStmt(s)).join(" ");
|
|
1916
|
+
}
|
|
1917
|
+
captureStmt(stmt) {
|
|
1918
|
+
const saved = this.w;
|
|
1919
|
+
const tmp = new Writer();
|
|
1920
|
+
this.w = tmp;
|
|
1921
|
+
this.emitStmt(stmt);
|
|
1922
|
+
this.w = saved;
|
|
1923
|
+
return tmp.output.trim();
|
|
1924
|
+
}
|
|
1925
|
+
captureClassMember(member) {
|
|
1926
|
+
const saved = this.w;
|
|
1927
|
+
const tmp = new Writer();
|
|
1928
|
+
this.w = tmp;
|
|
1929
|
+
this.emitClassMember(member);
|
|
1930
|
+
this.w = saved;
|
|
1931
|
+
return tmp.output.trim();
|
|
1932
|
+
}
|
|
1933
|
+
// ── Type reference emission ────────────────────────────────────────────────
|
|
1717
1934
|
emitTypeRef(ref) {
|
|
1718
1935
|
switch (ref.kind) {
|
|
1719
|
-
case "SimpleTypeRef":
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
case "
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1936
|
+
case "SimpleTypeRef": {
|
|
1937
|
+
const name = ref.name.join(".");
|
|
1938
|
+
return PRIMITIVE_TYPE_MAP[name] ?? name;
|
|
1939
|
+
}
|
|
1940
|
+
case "NullableTypeRef":
|
|
1941
|
+
return `${this.emitTypeRef(ref.base)} | null | undefined`;
|
|
1942
|
+
case "GenericTypeRef": {
|
|
1943
|
+
const base = ref.base.name.join(".");
|
|
1944
|
+
const mapped = GENERIC_TYPE_MAP[base] ?? base;
|
|
1945
|
+
const args = ref.args.map((a) => a.star ? "any" : a.type ? this.emitTypeRef(a.type) : "unknown");
|
|
1946
|
+
return `${mapped}<${args.join(", ")}>`;
|
|
1947
|
+
}
|
|
1948
|
+
case "FunctionTypeRef": {
|
|
1949
|
+
const params = ref.params.map((p, i) => `p${i}: ${this.emitTypeRef(p)}`).join(", ");
|
|
1950
|
+
const ret = this.emitTypeRef(ref.returnType);
|
|
1951
|
+
return `(${params}) => ${ret}`;
|
|
1952
|
+
}
|
|
1953
|
+
case "StarProjection":
|
|
1954
|
+
return "any";
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
// ── Utility helpers ────────────────────────────────────────────────────────
|
|
1958
|
+
emitTypeParamsStr(params) {
|
|
1959
|
+
if (params.length === 0)
|
|
1960
|
+
return "";
|
|
1961
|
+
const ps = params.map((p) => {
|
|
1962
|
+
const bound = p.upperBound ? ` extends ${this.emitTypeRef(p.upperBound)}` : "";
|
|
1963
|
+
return `${p.name}${bound}`;
|
|
1964
|
+
});
|
|
1965
|
+
return `<${ps.join(", ")}>`;
|
|
1966
|
+
}
|
|
1967
|
+
emitParamsStr(params) {
|
|
1968
|
+
return params.map((p) => {
|
|
1969
|
+
const spread = p.vararg ? "..." : "";
|
|
1970
|
+
// vararg params are rest params in TS: `...name: T[]`
|
|
1971
|
+
const type = this.opts.emitTypes
|
|
1972
|
+
? `: ${this.emitTypeRef(p.type)}${p.vararg ? "[]" : ""}`
|
|
1973
|
+
: "";
|
|
1974
|
+
const def = p.defaultValue ? ` = ${this.emitExpr(p.defaultValue)}` : "";
|
|
1975
|
+
return `${spread}${p.name}${type}${def}`;
|
|
1976
|
+
}).join(", ");
|
|
1977
|
+
}
|
|
1978
|
+
emitComponentPropsStr(params) {
|
|
1979
|
+
if (params.length === 0)
|
|
1980
|
+
return "";
|
|
1981
|
+
return params.map((p) => {
|
|
1982
|
+
const type = this.opts.emitTypes ? `: ${this.emitTypeRef(p.type)}` : "";
|
|
1983
|
+
return `${p.name}${type}`;
|
|
1984
|
+
}).join(", ");
|
|
1985
|
+
}
|
|
1986
|
+
emitSuperTypesStr(superTypes) {
|
|
1987
|
+
if (superTypes.length === 0)
|
|
1988
|
+
return "";
|
|
1989
|
+
const parts = [];
|
|
1990
|
+
let first = true;
|
|
1991
|
+
for (const s of superTypes) {
|
|
1992
|
+
// TypeScript does NOT allow constructor arguments in the `extends` clause.
|
|
1993
|
+
// Delegation args are passed via `super(args)` inside the constructor.
|
|
1994
|
+
if (first) {
|
|
1995
|
+
parts.push(` extends ${this.emitTypeRef(s.type)}`);
|
|
1996
|
+
first = false;
|
|
1997
|
+
}
|
|
1998
|
+
else {
|
|
1999
|
+
parts.push(` implements ${this.emitTypeRef(s.type)}`);
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
return parts.join("");
|
|
2003
|
+
}
|
|
2004
|
+
exportPrefix(mods) {
|
|
2005
|
+
if (mods.visibility === "private" || mods.visibility === "internal")
|
|
2006
|
+
return "";
|
|
2007
|
+
return "export ";
|
|
2008
|
+
}
|
|
2009
|
+
visibilityPrefix(mods) {
|
|
2010
|
+
switch (mods.visibility) {
|
|
2011
|
+
case "private": return "private ";
|
|
2012
|
+
case "protected": return "protected ";
|
|
2013
|
+
case "internal": return "/* internal */ ";
|
|
2014
|
+
default: return "";
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
unaryOpStr(op) {
|
|
2018
|
+
if (op === "not")
|
|
2019
|
+
return "!";
|
|
2020
|
+
return op;
|
|
2021
|
+
}
|
|
2022
|
+
binaryOpStr(op) {
|
|
2023
|
+
switch (op) {
|
|
2024
|
+
case "and": return "&&";
|
|
2025
|
+
case "or": return "||";
|
|
2026
|
+
case "xor": return "^";
|
|
2027
|
+
case "shl": return "<<";
|
|
2028
|
+
case "shr": return ">>";
|
|
2029
|
+
case "ushr": return ">>>";
|
|
2030
|
+
// === / !== are JS reference equality (Jalvin's triple-equals)
|
|
2031
|
+
case "===": return "===";
|
|
2032
|
+
case "!==": return "!==";
|
|
2033
|
+
case "..": return "/* .. */ +"; // handled by range()
|
|
2034
|
+
case "..<": return "/* ..< */ +";
|
|
2035
|
+
default: return op;
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
// ── AST name walkers (for wildcard import resolution) ─────────────────────
|
|
2039
|
+
/**
|
|
2040
|
+
* Walk the entire program AST and collect all identifier names that are
|
|
2041
|
+
* REFERENCED (i.e., used) in the code: NameExpr values and the first name
|
|
2042
|
+
* component of SimpleTypeRef / GenericTypeRef nodes.
|
|
2043
|
+
*
|
|
2044
|
+
* Jalvin primitive type names (String, Boolean, …) that map to TS primitives
|
|
2045
|
+
* are excluded from TypeRef collection since they never need an import.
|
|
2046
|
+
*/
|
|
1748
2047
|
gatherReferencedNames(program) {
|
|
1749
2048
|
const names = new Set();
|
|
1750
2049
|
const visited = new WeakSet();
|
|
1751
2050
|
const walk = (val) => {
|
|
1752
|
-
if (!val || typeof val !== "object"
|
|
2051
|
+
if (!val || typeof val !== "object")
|
|
1753
2052
|
return;
|
|
1754
|
-
visited.add(val);
|
|
1755
2053
|
if (Array.isArray(val)) {
|
|
1756
|
-
val
|
|
2054
|
+
for (const item of val)
|
|
2055
|
+
walk(item);
|
|
1757
2056
|
return;
|
|
1758
2057
|
}
|
|
1759
2058
|
const obj = val;
|
|
1760
|
-
if (obj
|
|
1761
|
-
if (typeof obj.name === "string")
|
|
1762
|
-
names.add(obj.name);
|
|
2059
|
+
if (visited.has(obj))
|
|
1763
2060
|
return;
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
2061
|
+
visited.add(obj);
|
|
2062
|
+
const kind = obj["kind"];
|
|
2063
|
+
if (kind === "NameExpr") {
|
|
2064
|
+
const name = obj["name"];
|
|
2065
|
+
if (typeof name === "string")
|
|
2066
|
+
names.add(name);
|
|
2067
|
+
return; // leaf node
|
|
2068
|
+
}
|
|
2069
|
+
if (kind === "SimpleTypeRef") {
|
|
2070
|
+
const nameArr = obj["name"];
|
|
2071
|
+
if (Array.isArray(nameArr) && nameArr.length > 0) {
|
|
2072
|
+
const first = nameArr[0];
|
|
2073
|
+
// Skip Jalvin primitive type names — they're erased to TS primitives.
|
|
2074
|
+
if (!(first in PRIMITIVE_TYPE_MAP))
|
|
2075
|
+
names.add(first);
|
|
2076
|
+
}
|
|
1768
2077
|
return;
|
|
1769
2078
|
}
|
|
1770
|
-
if (
|
|
1771
|
-
|
|
1772
|
-
|
|
2079
|
+
if (kind === "GenericTypeRef") {
|
|
2080
|
+
const base = obj["base"];
|
|
2081
|
+
const nameArr = base?.["name"];
|
|
2082
|
+
if (Array.isArray(nameArr) && nameArr.length > 0) {
|
|
2083
|
+
const first = nameArr[0];
|
|
2084
|
+
if (!(first in PRIMITIVE_TYPE_MAP))
|
|
2085
|
+
names.add(first);
|
|
2086
|
+
}
|
|
2087
|
+
// Fall through to recurse into type arguments
|
|
2088
|
+
}
|
|
2089
|
+
for (const propVal of Object.values(obj)) {
|
|
2090
|
+
walk(propVal);
|
|
1773
2091
|
}
|
|
1774
|
-
Object.values(obj).forEach(walk);
|
|
1775
2092
|
};
|
|
1776
|
-
program.declarations
|
|
2093
|
+
for (const decl of program.declarations)
|
|
2094
|
+
walk(decl);
|
|
1777
2095
|
return names;
|
|
1778
2096
|
}
|
|
2097
|
+
/**
|
|
2098
|
+
* Walk the entire program AST and collect all names that are LOCALLY DEFINED:
|
|
2099
|
+
* top-level declaration names, non-star import aliases, function/lambda
|
|
2100
|
+
* parameters, local val/var bindings, for-loop variables, catch-clause names,
|
|
2101
|
+
* when-subject bindings, and type parameter names.
|
|
2102
|
+
*/
|
|
1779
2103
|
gatherAllLocalBindings(program) {
|
|
1780
2104
|
const names = new Set();
|
|
1781
2105
|
const visited = new WeakSet();
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
2106
|
+
// Top-level declaration names
|
|
2107
|
+
for (const decl of program.declarations) {
|
|
2108
|
+
const d = decl;
|
|
2109
|
+
if (typeof d.name === "string" && d.name)
|
|
2110
|
+
names.add(d.name);
|
|
2111
|
+
}
|
|
2112
|
+
// Non-star import aliases
|
|
2113
|
+
for (const imp of program.imports) {
|
|
2114
|
+
if (!imp.star) {
|
|
2115
|
+
const name = imp.alias ?? imp.path[imp.path.length - 1];
|
|
2116
|
+
if (name)
|
|
2117
|
+
names.add(name);
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
1786
2120
|
const walk = (val) => {
|
|
1787
|
-
if (!val || typeof val !== "object"
|
|
2121
|
+
if (!val || typeof val !== "object")
|
|
1788
2122
|
return;
|
|
1789
|
-
visited.add(val);
|
|
1790
2123
|
if (Array.isArray(val)) {
|
|
1791
|
-
val
|
|
2124
|
+
for (const item of val)
|
|
2125
|
+
walk(item);
|
|
1792
2126
|
return;
|
|
1793
2127
|
}
|
|
1794
2128
|
const obj = val;
|
|
1795
|
-
if (
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
2129
|
+
if (visited.has(obj))
|
|
2130
|
+
return;
|
|
2131
|
+
visited.add(obj);
|
|
2132
|
+
const kind = obj["kind"];
|
|
2133
|
+
// Collect bound names at their declaration sites
|
|
2134
|
+
if (kind === "PropertyDecl" || kind === "DestructuringDecl") {
|
|
2135
|
+
if (typeof obj["name"] === "string")
|
|
2136
|
+
names.add(obj["name"]);
|
|
2137
|
+
const nms = obj["names"];
|
|
2138
|
+
if (Array.isArray(nms)) {
|
|
2139
|
+
for (const n of nms) {
|
|
2140
|
+
if (typeof n === "string")
|
|
2141
|
+
names.add(n);
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
if (kind === "FunDecl" || kind === "ExtensionFunDecl" ||
|
|
2146
|
+
kind === "ComponentDecl" || kind === "ClassDecl" ||
|
|
2147
|
+
kind === "DataClassDecl" || kind === "SealedClassDecl" ||
|
|
2148
|
+
kind === "EnumClassDecl" || kind === "InterfaceDecl" ||
|
|
2149
|
+
kind === "ObjectDecl" || kind === "TypeAliasDecl") {
|
|
2150
|
+
if (typeof obj["name"] === "string" && obj["name"])
|
|
2151
|
+
names.add(obj["name"]);
|
|
2152
|
+
// Type parameters
|
|
2153
|
+
const typeParams = obj["typeParams"];
|
|
2154
|
+
if (Array.isArray(typeParams)) {
|
|
2155
|
+
for (const tp of typeParams) {
|
|
2156
|
+
if (tp.name)
|
|
2157
|
+
names.add(tp.name);
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
// Function/method parameters
|
|
2161
|
+
const params = obj["params"];
|
|
2162
|
+
if (Array.isArray(params)) {
|
|
2163
|
+
for (const p of params) {
|
|
2164
|
+
if (p.name)
|
|
2165
|
+
names.add(p.name);
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
if (kind === "LambdaExpr") {
|
|
2170
|
+
const params = obj["params"];
|
|
2171
|
+
if (Array.isArray(params)) {
|
|
2172
|
+
for (const p of params) {
|
|
2173
|
+
if (p.name)
|
|
2174
|
+
names.add(p.name);
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
if (kind === "ForStmt") {
|
|
2179
|
+
const binding = obj["binding"];
|
|
2180
|
+
if (typeof binding === "string") {
|
|
2181
|
+
names.add(binding);
|
|
2182
|
+
}
|
|
2183
|
+
else if (binding && typeof binding === "object") {
|
|
2184
|
+
const b = binding;
|
|
2185
|
+
// TupleDestructure or MapDestructure
|
|
2186
|
+
if (Array.isArray(b["names"])) {
|
|
2187
|
+
for (const n of b["names"]) {
|
|
2188
|
+
if (typeof n === "string")
|
|
2189
|
+
names.add(n);
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
if (typeof b["key"] === "string")
|
|
2193
|
+
names.add(b["key"]);
|
|
2194
|
+
if (typeof b["value"] === "string")
|
|
2195
|
+
names.add(b["value"]);
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
if (kind === "TryCatchStmt") {
|
|
2199
|
+
const catches = obj["catches"];
|
|
2200
|
+
if (Array.isArray(catches)) {
|
|
2201
|
+
for (const c of catches) {
|
|
2202
|
+
if (c.name)
|
|
2203
|
+
names.add(c.name);
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
if (kind === "WhenStmt" || kind === "WhenExpr") {
|
|
2208
|
+
const subject = obj["subject"];
|
|
2209
|
+
if (typeof subject?.["binding"] === "string")
|
|
2210
|
+
names.add(subject["binding"]);
|
|
2211
|
+
}
|
|
2212
|
+
if (kind === "SecondaryConstructor") {
|
|
2213
|
+
const params = obj["params"];
|
|
2214
|
+
if (Array.isArray(params)) {
|
|
2215
|
+
for (const p of params) {
|
|
2216
|
+
if (p.name)
|
|
2217
|
+
names.add(p.name);
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
for (const propVal of Object.values(obj)) {
|
|
2222
|
+
walk(propVal);
|
|
2223
|
+
}
|
|
1806
2224
|
};
|
|
1807
|
-
program.declarations
|
|
2225
|
+
for (const decl of program.declarations)
|
|
2226
|
+
walk(decl);
|
|
1808
2227
|
return names;
|
|
1809
2228
|
}
|
|
1810
2229
|
}
|
|
1811
2230
|
exports.CodeGenerator = CodeGenerator;
|
|
1812
|
-
|
|
1813
|
-
|
|
2231
|
+
// ---------------------------------------------------------------------------
|
|
2232
|
+
// Type name mappings (Jalvin → TypeScript)
|
|
2233
|
+
// ---------------------------------------------------------------------------
|
|
2234
|
+
const PRIMITIVE_TYPE_MAP = {
|
|
2235
|
+
Int: "number",
|
|
2236
|
+
Long: "bigint",
|
|
2237
|
+
Float: "number",
|
|
2238
|
+
Double: "number",
|
|
2239
|
+
Boolean: "boolean",
|
|
2240
|
+
String: "string",
|
|
2241
|
+
Char: "string",
|
|
2242
|
+
Byte: "number",
|
|
2243
|
+
Short: "number",
|
|
2244
|
+
Unit: "void",
|
|
2245
|
+
Any: "unknown",
|
|
2246
|
+
Nothing: "never",
|
|
2247
|
+
};
|
|
2248
|
+
const GENERIC_TYPE_MAP = {
|
|
2249
|
+
List: "ReadonlyArray",
|
|
2250
|
+
MutableList: "Array",
|
|
2251
|
+
Set: "ReadonlySet",
|
|
2252
|
+
MutableSet: "Set",
|
|
2253
|
+
Map: "ReadonlyMap",
|
|
2254
|
+
MutableMap: "Map",
|
|
2255
|
+
Array: "Array",
|
|
2256
|
+
Pair: "[", // handled specially
|
|
2257
|
+
Triple: "[",
|
|
2258
|
+
Deferred: "Promise",
|
|
2259
|
+
StateFlow: "StateFlow",
|
|
2260
|
+
MutableStateFlow: "MutableStateFlow",
|
|
2261
|
+
Flow: "AsyncIterable",
|
|
2262
|
+
};
|
|
1814
2263
|
const PRIMITIVE_TYPES = new Set(Object.keys(PRIMITIVE_TYPE_MAP));
|
|
1815
|
-
|
|
1816
|
-
|
|
2264
|
+
// ---------------------------------------------------------------------------
|
|
2265
|
+
// Well-known JavaScript / TypeScript global names that must never be emitted
|
|
2266
|
+
// as named imports from a wildcard-imported package.
|
|
2267
|
+
// ---------------------------------------------------------------------------
|
|
2268
|
+
const JS_GLOBAL_NAMES = new Set([
|
|
2269
|
+
// JS built-in constructors and objects
|
|
2270
|
+
"Array", "Map", "Set", "Object", "String", "Number", "Boolean",
|
|
2271
|
+
"Promise", "Error", "TypeError", "RangeError", "SyntaxError", "URIError",
|
|
2272
|
+
"EvalError", "ReferenceError",
|
|
2273
|
+
"console", "Math", "Date", "JSON", "RegExp", "Symbol", "BigInt",
|
|
2274
|
+
"Proxy", "Reflect", "globalThis", "Atomics", "SharedArrayBuffer",
|
|
2275
|
+
"ArrayBuffer", "DataView", "Int8Array", "Uint8Array", "Uint8ClampedArray",
|
|
2276
|
+
"Int16Array", "Uint16Array", "Int32Array", "Uint32Array",
|
|
2277
|
+
"Float32Array", "Float64Array", "BigInt64Array", "BigUint64Array",
|
|
2278
|
+
// Global functions
|
|
2279
|
+
"isNaN", "isFinite", "parseInt", "parseFloat", "eval",
|
|
2280
|
+
"encodeURI", "decodeURI", "encodeURIComponent", "decodeURIComponent",
|
|
2281
|
+
// Browser/Node globals
|
|
2282
|
+
"setTimeout", "clearTimeout", "setInterval", "clearInterval",
|
|
2283
|
+
"queueMicrotask", "requestAnimationFrame", "cancelAnimationFrame",
|
|
2284
|
+
"fetch", "URL", "URLSearchParams", "AbortController", "AbortSignal",
|
|
2285
|
+
"EventTarget", "Event", "CustomEvent", "FormData", "Headers",
|
|
2286
|
+
"Request", "Response", "Blob", "File", "FileReader",
|
|
2287
|
+
"Worker", "SharedWorker", "WebSocket",
|
|
2288
|
+
"ReadableStream", "WritableStream", "TransformStream",
|
|
2289
|
+
"document", "window", "navigator", "location", "history", "screen",
|
|
2290
|
+
"performance", "crypto", "indexedDB", "localStorage", "sessionStorage",
|
|
2291
|
+
"confirm", "alert", "prompt",
|
|
2292
|
+
"process", "Buffer", "global", "require", "module", "exports", "__dirname", "__filename",
|
|
2293
|
+
// Special identifiers
|
|
2294
|
+
"undefined", "null", "NaN", "Infinity",
|
|
2295
|
+
"it", "this", "super", "arguments", "new", "class", "function",
|
|
2296
|
+
// TypeScript primitive type names
|
|
2297
|
+
"any", "unknown", "never", "void", "string", "number", "boolean", "bigint",
|
|
2298
|
+
"object", "symbol",
|
|
2299
|
+
// Jalvin type names that map to TS primitives (never need import)
|
|
2300
|
+
"String", "Boolean", "Any", "Nothing", "Unit", "Char", "Byte", "Short",
|
|
2301
|
+
"Float", "Double",
|
|
2302
|
+
]);
|
|
2303
|
+
// ---------------------------------------------------------------------------
|
|
2304
|
+
// Public helper
|
|
2305
|
+
// ---------------------------------------------------------------------------
|
|
2306
|
+
function generate(program, opts, operatorOverloads, typeMap) {
|
|
2307
|
+
return new CodeGenerator(opts).generate(program, operatorOverloads, typeMap);
|
|
2308
|
+
}
|
|
1817
2309
|
//# sourceMappingURL=codegen.js.map
|