@jalvin/compiler 2.0.32 → 2.0.34
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/index.d.ts +179 -0
- package/dist/codegen/index.d.ts.map +1 -0
- package/dist/codegen/index.js +2207 -0
- package/dist/codegen/index.js.map +1 -0
- package/dist/codegen.d.ts +1 -1
- package/dist/codegen.d.ts.map +1 -1
- package/dist/codegen.js +64 -23
- package/dist/codegen.js.map +1 -1
- package/dist/codegen_freestanding_c.d.ts +9 -0
- package/dist/codegen_freestanding_c.d.ts.map +1 -0
- package/dist/codegen_freestanding_c.js +321 -0
- package/dist/codegen_freestanding_c.js.map +1 -0
- package/dist/diagnostics.d.ts +2 -0
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js +3 -1
- package/dist/diagnostics.js.map +1 -1
- package/dist/lexer/chars.d.ts +5 -0
- package/dist/lexer/chars.d.ts.map +1 -0
- package/dist/lexer/chars.js +20 -0
- package/dist/lexer/chars.js.map +1 -0
- package/dist/lexer/index.d.ts +37 -0
- package/dist/lexer/index.d.ts.map +1 -0
- package/dist/lexer/index.js +630 -0
- package/dist/lexer/index.js.map +1 -0
- package/dist/lexer/tokens.d.ts +135 -0
- package/dist/lexer/tokens.d.ts.map +1 -0
- package/dist/lexer/tokens.js +89 -0
- package/dist/lexer/tokens.js.map +1 -0
- package/dist/parser/constants.d.ts +3 -0
- package/dist/parser/constants.d.ts.map +1 -0
- package/dist/parser/constants.js +7 -0
- package/dist/parser/constants.js.map +1 -0
- package/dist/parser/helpers.d.ts +2 -0
- package/dist/parser/helpers.d.ts.map +1 -0
- package/dist/parser/helpers.js +9 -0
- package/dist/parser/helpers.js.map +1 -0
- package/dist/parser/index.d.ts +118 -0
- package/dist/parser/index.d.ts.map +1 -0
- package/dist/parser/index.js +2387 -0
- package/dist/parser/index.js.map +1 -0
- package/dist/typechecker/globals.d.ts +2 -0
- package/dist/typechecker/globals.d.ts.map +1 -0
- package/dist/typechecker/globals.js +15 -0
- package/dist/typechecker/globals.js.map +1 -0
- package/dist/typechecker/index.d.ts +146 -0
- package/dist/typechecker/index.d.ts.map +1 -0
- package/dist/typechecker/index.js +2288 -0
- package/dist/typechecker/index.js.map +1 -0
- package/dist/typechecker/types.d.ts +66 -0
- package/dist/typechecker/types.d.ts.map +1 -0
- package/dist/typechecker/types.js +39 -0
- package/dist/typechecker/types.js.map +1 -0
- package/dist/typechecker.d.ts +12 -0
- package/dist/typechecker.d.ts.map +1 -1
- package/dist/typechecker.js +243 -149
- package/dist/typechecker.js.map +1 -1
- package/package.json +10 -15
|
@@ -0,0 +1,2207 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
3
|
+
// Jalvin Code Generator — AST → TypeScript/TSX
|
|
4
|
+
//
|
|
5
|
+
// Design principles:
|
|
6
|
+
// • component declarations → React functional components (TSX)
|
|
7
|
+
// • data class → class with auto-generated copy(), equals(), toString()
|
|
8
|
+
// • sealed class → TypeScript discriminated union + base class
|
|
9
|
+
// • extension functions → module-level functions with receiver as first param,
|
|
10
|
+
// augmented onto the prototype via declaration merging
|
|
11
|
+
// • launch {} → void-returning async IIFE (fire-and-forget)
|
|
12
|
+
// • async {} → Promise<T>-returning async IIFE
|
|
13
|
+
// • suspend fun → async function
|
|
14
|
+
// • Bibi(...) → runtime Bibi() helper (HTTP client)
|
|
15
|
+
// • StateFlow / ViewModel → @jalvin/runtime types
|
|
16
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
17
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
18
|
+
if (k2 === undefined) k2 = k;
|
|
19
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
20
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
21
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
22
|
+
}
|
|
23
|
+
Object.defineProperty(o, k2, desc);
|
|
24
|
+
}) : (function(o, m, k, k2) {
|
|
25
|
+
if (k2 === undefined) k2 = k;
|
|
26
|
+
o[k2] = m[k];
|
|
27
|
+
}));
|
|
28
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
29
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
30
|
+
}) : function(o, v) {
|
|
31
|
+
o["default"] = v;
|
|
32
|
+
});
|
|
33
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
34
|
+
var ownKeys = function(o) {
|
|
35
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
36
|
+
var ar = [];
|
|
37
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
38
|
+
return ar;
|
|
39
|
+
};
|
|
40
|
+
return ownKeys(o);
|
|
41
|
+
};
|
|
42
|
+
return function (mod) {
|
|
43
|
+
if (mod && mod.__esModule) return mod;
|
|
44
|
+
var result = {};
|
|
45
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
46
|
+
__setModuleDefault(result, mod);
|
|
47
|
+
return result;
|
|
48
|
+
};
|
|
49
|
+
})();
|
|
50
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
51
|
+
exports.CodeGenerator = exports.DEFAULT_CODEGEN_OPTIONS = void 0;
|
|
52
|
+
exports.generate = generate;
|
|
53
|
+
const fs = __importStar(require("node:fs"));
|
|
54
|
+
const nodePath = __importStar(require("node:path"));
|
|
55
|
+
const AST = __importStar(require("../ast.js"));
|
|
56
|
+
const diagnostics_js_1 = require("../diagnostics.js");
|
|
57
|
+
const lexer_js_1 = require("../lexer.js");
|
|
58
|
+
const parser_js_1 = require("../parser.js");
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// Code writer — thin abstraction over string concatenation that tracks
|
|
61
|
+
// indentation and emits source-map line hints.
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
class Writer {
|
|
64
|
+
buf = "";
|
|
65
|
+
indent = 0;
|
|
66
|
+
INDENT = " ";
|
|
67
|
+
/** line mapping: output line → input line (1-based) */
|
|
68
|
+
lineMap = [];
|
|
69
|
+
currentLine = 1;
|
|
70
|
+
write(text) {
|
|
71
|
+
this.buf += text;
|
|
72
|
+
}
|
|
73
|
+
writeLine(text = "") {
|
|
74
|
+
this.buf += text + "\n";
|
|
75
|
+
this.lineMap.push(this.currentLine);
|
|
76
|
+
this.currentLine++;
|
|
77
|
+
}
|
|
78
|
+
writeIndented(text) {
|
|
79
|
+
this.buf += this.INDENT.repeat(this.indent) + text;
|
|
80
|
+
}
|
|
81
|
+
writeIndentedLine(text = "") {
|
|
82
|
+
this.buf += this.INDENT.repeat(this.indent) + text + "\n";
|
|
83
|
+
this.lineMap.push(this.currentLine);
|
|
84
|
+
this.currentLine++;
|
|
85
|
+
}
|
|
86
|
+
pushIndent() { this.indent++; }
|
|
87
|
+
popIndent() { this.indent = Math.max(0, this.indent - 1); }
|
|
88
|
+
get output() { return this.buf; }
|
|
89
|
+
setSourceLine(line) { this.currentLine = line + 1; }
|
|
90
|
+
}
|
|
91
|
+
exports.DEFAULT_CODEGEN_OPTIONS = {
|
|
92
|
+
jsx: false,
|
|
93
|
+
module: "esm",
|
|
94
|
+
emitTypes: false,
|
|
95
|
+
runtimeImport: "@jalvin/runtime",
|
|
96
|
+
};
|
|
97
|
+
class CodeGenerator {
|
|
98
|
+
w = new Writer();
|
|
99
|
+
opts;
|
|
100
|
+
hasComponents = false;
|
|
101
|
+
runtimeSymbolsNeeded = new Set();
|
|
102
|
+
/** Names of component functions — used to detect Compose-style calls and emit them as JSX */
|
|
103
|
+
componentNames = new Set();
|
|
104
|
+
/**
|
|
105
|
+
* Param names for components resolved from imported local .jalvin files.
|
|
106
|
+
* Maps component name → ordered list of param names.
|
|
107
|
+
*/
|
|
108
|
+
componentParamNames = new Map();
|
|
109
|
+
/** True when the program contains `import @jalvin/ui.*` — used to detect UI primitive calls */
|
|
110
|
+
hasUiStarImport = false;
|
|
111
|
+
/** Operator overload resolutions from the type checker */
|
|
112
|
+
operatorOverloadMap = new Map();
|
|
113
|
+
/** Type map from the type checker */
|
|
114
|
+
typeMap = new Map();
|
|
115
|
+
/**
|
|
116
|
+
* Registry of extension functions declared on primitive receiver types.
|
|
117
|
+
* Maps receiverTypeName → Map<methodName, generatedFnName>
|
|
118
|
+
* Populated during top-level emit; used at call sites for rewriting.
|
|
119
|
+
*/
|
|
120
|
+
primitiveExtensions = new Map();
|
|
121
|
+
/**
|
|
122
|
+
* When a scoped star import exists (e.g. import @jalvin/runtime.*), contains the
|
|
123
|
+
* set of external symbol names to emit as named imports from that package instead
|
|
124
|
+
* of a namespace import. Empty if namespace-import behavior is used.
|
|
125
|
+
*/
|
|
126
|
+
externalStarCandidates = new Set();
|
|
127
|
+
/** Module specifiers whose symbols are already covered by a named star-import expansion. */
|
|
128
|
+
handledStarImportModules = new Set();
|
|
129
|
+
constructor(opts = {}) {
|
|
130
|
+
this.opts = { ...exports.DEFAULT_CODEGEN_OPTIONS, ...opts };
|
|
131
|
+
}
|
|
132
|
+
generate(program, operatorOverloads, typeMap) {
|
|
133
|
+
if (operatorOverloads)
|
|
134
|
+
this.operatorOverloadMap = operatorOverloads;
|
|
135
|
+
if (typeMap)
|
|
136
|
+
this.typeMap = typeMap;
|
|
137
|
+
// Pre-scan for components (local declarations OR @jalvin/ui imports)
|
|
138
|
+
this.hasComponents = program.declarations.some((d) => d.kind === "ComponentDecl" ||
|
|
139
|
+
(d.kind === "ClassDecl" && d.body?.members.some((m) => m.kind === "ComponentDecl"))) || program.imports.some((imp) => imp.path[0] === "@jalvin" && imp.path[1] === "ui");
|
|
140
|
+
// Collect component names for Compose-style call detection
|
|
141
|
+
this.componentNames = new Set();
|
|
142
|
+
this.componentParamNames = new Map();
|
|
143
|
+
for (const decl of program.declarations) {
|
|
144
|
+
if (decl.kind === "ComponentDecl")
|
|
145
|
+
this.componentNames.add(decl.name);
|
|
146
|
+
if (decl.kind === "ClassDecl" && decl.body) {
|
|
147
|
+
for (const m of decl.body.members) {
|
|
148
|
+
if (m.kind === "ComponentDecl")
|
|
149
|
+
this.componentNames.add(m.name);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
this.hasUiStarImport = false;
|
|
154
|
+
for (const imp of program.imports) {
|
|
155
|
+
// Named imports from @jalvin/ui are component functions
|
|
156
|
+
if (imp.path[0] === "@jalvin" && imp.path[1] === "ui" && !imp.star) {
|
|
157
|
+
this.componentNames.add(imp.path[imp.path.length - 1]);
|
|
158
|
+
}
|
|
159
|
+
// Star import from @jalvin/ui — UI primitives won't be in componentNames
|
|
160
|
+
if (imp.path[0] === "@jalvin" && imp.path[1] === "ui" && imp.star) {
|
|
161
|
+
this.hasUiStarImport = true;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// Resolve component fun symbols from locally imported .jalvin files
|
|
165
|
+
const sourceRoot = this.opts.sourceRoot ?? process.cwd();
|
|
166
|
+
this.resolveLocalComponentImports(program, sourceRoot);
|
|
167
|
+
// Pre-compute external star-import candidates (before emitting anything)
|
|
168
|
+
this.externalStarCandidates = new Set();
|
|
169
|
+
this.handledStarImportModules = new Set();
|
|
170
|
+
const scopedStarImports = program.imports.filter((imp) => imp.star && imp.path[0].startsWith("@"));
|
|
171
|
+
if (scopedStarImports.length === 1) {
|
|
172
|
+
// Only resolve when there's exactly one scoped star import — with multiple
|
|
173
|
+
// star imports we can't safely split external names across packages.
|
|
174
|
+
const referencedNames = this.gatherReferencedNames(program);
|
|
175
|
+
const localBindings = this.gatherAllLocalBindings(program);
|
|
176
|
+
for (const name of referencedNames) {
|
|
177
|
+
if (!localBindings.has(name) && !JS_GLOBAL_NAMES.has(name)) {
|
|
178
|
+
this.externalStarCandidates.add(name);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
// Emit header
|
|
183
|
+
this.emitHeader(program);
|
|
184
|
+
// Declarations
|
|
185
|
+
for (const decl of program.declarations) {
|
|
186
|
+
this.emitTopLevelDecl(decl);
|
|
187
|
+
this.w.writeLine();
|
|
188
|
+
}
|
|
189
|
+
// Patch in runtime import if needed
|
|
190
|
+
const preamble = this.buildPreamble();
|
|
191
|
+
const code = preamble + this.w.output;
|
|
192
|
+
return {
|
|
193
|
+
code,
|
|
194
|
+
lineMap: this.w.lineMap,
|
|
195
|
+
isJsx: false, // DOM-based emit — no JSX
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
// ── Preamble & imports ─────────────────────────────────────────────────────
|
|
199
|
+
buildPreamble() {
|
|
200
|
+
const lines = [];
|
|
201
|
+
// No React import — @jalvin/ui is DOM-based.
|
|
202
|
+
// Emit compiler-injected runtime symbols, but only those NOT already covered
|
|
203
|
+
// by a star-import's named-import expansion (to avoid duplicate imports).
|
|
204
|
+
const alreadyCovered = this.handledStarImportModules.has(this.opts.runtimeImport)
|
|
205
|
+
? this.externalStarCandidates
|
|
206
|
+
: new Set();
|
|
207
|
+
const needed = [...this.runtimeSymbolsNeeded].filter((s) => !alreadyCovered.has(s));
|
|
208
|
+
if (needed.length > 0) {
|
|
209
|
+
lines.push(`import { ${needed.sort().join(", ")} } from "${this.opts.runtimeImport}";`);
|
|
210
|
+
}
|
|
211
|
+
return lines.length > 0 ? lines.join("\n") + "\n\n" : "";
|
|
212
|
+
}
|
|
213
|
+
emitHeader(program) {
|
|
214
|
+
const sourceRoot = this.opts.sourceRoot ?? process.cwd();
|
|
215
|
+
// Re-emit user imports as ES imports
|
|
216
|
+
for (const imp of program.imports) {
|
|
217
|
+
// Build module specifier.
|
|
218
|
+
//
|
|
219
|
+
// Scoped packages (@org/pkg.Symbol): symbol is an export from the package,
|
|
220
|
+
// strip the last segment.
|
|
221
|
+
// import @jalvin/ui.Column → import { Column } from "@jalvin/ui"
|
|
222
|
+
// import @jalvin/runtime.* → import * as runtime from "@jalvin/runtime"
|
|
223
|
+
//
|
|
224
|
+
// Local imports (a.b.C): check whether the preceding segments already
|
|
225
|
+
// resolve to a file on disk.
|
|
226
|
+
// import src.models.Rotation → src/models/Rotation.ts does NOT exist
|
|
227
|
+
// → import { Rotation } from "src/models/Rotation"
|
|
228
|
+
// import src.models.css.Css → src/models/css.ts DOES exist
|
|
229
|
+
// → import { Css } from "src/models/css"
|
|
230
|
+
const isScoped = imp.path[0].startsWith("@");
|
|
231
|
+
let moduleSpecifier;
|
|
232
|
+
if (imp.star) {
|
|
233
|
+
// Star import — the entire path is the module.
|
|
234
|
+
moduleSpecifier = isScoped
|
|
235
|
+
? imp.path[0] + "/" + imp.path.slice(1).join("/")
|
|
236
|
+
: imp.path.join("/");
|
|
237
|
+
}
|
|
238
|
+
else if (isScoped) {
|
|
239
|
+
// @org/pkg.Symbol → strip symbol from path.
|
|
240
|
+
const moduleParts = imp.path.slice(0, -1);
|
|
241
|
+
moduleSpecifier = moduleParts[0] + "/" + moduleParts.slice(1).join("/");
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
// Non-scoped import: prefer local-file convention unless this looks like
|
|
245
|
+
// an installed npm package path (e.g. cubing/twisty.TwistyPlayer).
|
|
246
|
+
const precedingParts = imp.path.slice(0, -1);
|
|
247
|
+
const precedingRelPath = precedingParts.join("/");
|
|
248
|
+
const fileExts = [".ts", ".tsx", ".jalvin"];
|
|
249
|
+
const precedingIsFile = precedingParts.length > 0 && fileExts.some((ext) => fs.existsSync(nodePath.join(sourceRoot, precedingRelPath + ext)));
|
|
250
|
+
const looksLikeNpmPackage = this.isLikelyNodeModuleImport(imp.path, sourceRoot);
|
|
251
|
+
moduleSpecifier = precedingIsFile
|
|
252
|
+
? precedingRelPath // src.models.css.Css → "src/models/css"
|
|
253
|
+
: looksLikeNpmPackage
|
|
254
|
+
? precedingRelPath // cubing.twisty.TwistyPlayer → "cubing/twisty"
|
|
255
|
+
: imp.path.join("/"); // src.models.Rotation → "src/models/Rotation"
|
|
256
|
+
}
|
|
257
|
+
if (imp.star && isScoped && this.externalStarCandidates.size > 0) {
|
|
258
|
+
// Named-import expansion of a scoped star import.
|
|
259
|
+
// Emit explicit named imports for each external symbol used in the file so
|
|
260
|
+
// that symbols like `ViewModel`, `mutableStateOf` are directly in scope.
|
|
261
|
+
const symbols = [...this.externalStarCandidates].sort();
|
|
262
|
+
this.w.writeIndentedLine(`import { ${symbols.join(", ")} } from "${moduleSpecifier}";`);
|
|
263
|
+
this.handledStarImportModules.add(moduleSpecifier);
|
|
264
|
+
}
|
|
265
|
+
else if (imp.star) {
|
|
266
|
+
// Fallback: namespace import (multiple scoped star imports or no candidates).
|
|
267
|
+
this.w.writeIndentedLine(`import * as ${imp.path[imp.path.length - 1]} from "${moduleSpecifier}";`);
|
|
268
|
+
}
|
|
269
|
+
else if (imp.alias) {
|
|
270
|
+
const named = imp.path[imp.path.length - 1];
|
|
271
|
+
this.w.writeIndentedLine(`import { ${named} as ${imp.alias} } from "${moduleSpecifier}";`);
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
this.w.writeIndentedLine(`import { ${imp.path[imp.path.length - 1]} } from "${moduleSpecifier}";`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (program.imports.length > 0)
|
|
278
|
+
this.w.writeLine();
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Heuristic for non-scoped imports: if the first path segment resolves to an
|
|
282
|
+
* installed package in node_modules, treat the import as npm/ESM and strip
|
|
283
|
+
* the trailing symbol name from the module specifier.
|
|
284
|
+
*/
|
|
285
|
+
isLikelyNodeModuleImport(pathParts, sourceRoot) {
|
|
286
|
+
if (pathParts.length < 2)
|
|
287
|
+
return false;
|
|
288
|
+
const packageName = pathParts[0];
|
|
289
|
+
let dir = sourceRoot;
|
|
290
|
+
while (true) {
|
|
291
|
+
const packageJson = nodePath.join(dir, "node_modules", packageName, "package.json");
|
|
292
|
+
if (fs.existsSync(packageJson))
|
|
293
|
+
return true;
|
|
294
|
+
const parent = nodePath.dirname(dir);
|
|
295
|
+
if (parent === dir)
|
|
296
|
+
break;
|
|
297
|
+
dir = parent;
|
|
298
|
+
}
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
// ── Top-level declarations ─────────────────────────────────────────────────
|
|
302
|
+
/** Emit a @deprecated JSDoc block if the declaration has @Nuked. */
|
|
303
|
+
emitAnnotations(mods) {
|
|
304
|
+
for (const ann of mods.annotations) {
|
|
305
|
+
if (ann.name === "Nuked") {
|
|
306
|
+
const reason = ann.args ? ` ${ann.args.replace(/^["']|["']$/g, "")}` : "";
|
|
307
|
+
this.w.writeIndentedLine(`/** @deprecated${reason} */`);
|
|
308
|
+
}
|
|
309
|
+
// Other annotations are silently ignored for now (pass-through model)
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
emitTopLevelDecl(decl) {
|
|
313
|
+
switch (decl.kind) {
|
|
314
|
+
case "FunDecl":
|
|
315
|
+
this.emitFunDecl(decl, false);
|
|
316
|
+
break;
|
|
317
|
+
case "ComponentDecl":
|
|
318
|
+
this.emitComponentDecl(decl);
|
|
319
|
+
break;
|
|
320
|
+
case "ClassDecl":
|
|
321
|
+
this.emitClassDecl(decl);
|
|
322
|
+
break;
|
|
323
|
+
case "DataClassDecl":
|
|
324
|
+
this.emitDataClassDecl(decl);
|
|
325
|
+
break;
|
|
326
|
+
case "SealedClassDecl":
|
|
327
|
+
this.emitSealedClassDecl(decl);
|
|
328
|
+
break;
|
|
329
|
+
case "EnumClassDecl":
|
|
330
|
+
this.emitEnumClassDecl(decl);
|
|
331
|
+
break;
|
|
332
|
+
case "InterfaceDecl":
|
|
333
|
+
this.emitInterfaceDecl(decl);
|
|
334
|
+
break;
|
|
335
|
+
case "ObjectDecl":
|
|
336
|
+
this.emitObjectDecl(decl);
|
|
337
|
+
break;
|
|
338
|
+
case "TypeAliasDecl":
|
|
339
|
+
this.emitTypeAliasDecl(decl);
|
|
340
|
+
break;
|
|
341
|
+
case "PropertyDecl":
|
|
342
|
+
this.emitPropertyDecl(decl, false);
|
|
343
|
+
break;
|
|
344
|
+
case "ExtensionFunDecl":
|
|
345
|
+
this.emitExtensionFunDecl(decl);
|
|
346
|
+
break;
|
|
347
|
+
case "DestructuringDecl":
|
|
348
|
+
this.emitDestructuringDecl(decl, false);
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
// ── function declarations ──────────────────────────────────────────────────
|
|
353
|
+
emitFunDecl(decl, memberOf) {
|
|
354
|
+
this.emitAnnotations(decl.modifiers);
|
|
355
|
+
const isSuspend = AST.isSuspend(decl.modifiers);
|
|
356
|
+
const vis = memberOf ? this.visibilityPrefix(decl.modifiers) : this.exportPrefix(decl.modifiers);
|
|
357
|
+
const asyncKw = isSuspend ? "async " : "";
|
|
358
|
+
const typeParams = this.emitTypeParamsStr(decl.typeParams);
|
|
359
|
+
const params = this.emitParamsStr(decl.params);
|
|
360
|
+
const retType = decl.returnType && this.opts.emitTypes
|
|
361
|
+
? `: ${this.emitTypeRef(decl.returnType)}`
|
|
362
|
+
: "";
|
|
363
|
+
if (!decl.body) {
|
|
364
|
+
// Abstract / interface method
|
|
365
|
+
this.w.writeIndentedLine(`${vis}${asyncKw}${decl.name}${typeParams}(${params})${retType};`);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
if (decl.body.kind === "Block") {
|
|
369
|
+
if (memberOf) {
|
|
370
|
+
this.w.writeIndentedLine(`${vis}${asyncKw}${decl.name}${typeParams}(${params})${retType} {`);
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
this.w.writeIndentedLine(`${vis}${asyncKw}function ${decl.name}${typeParams}(${params})${retType} {`);
|
|
374
|
+
}
|
|
375
|
+
this.w.pushIndent();
|
|
376
|
+
this.emitBlock(decl.body);
|
|
377
|
+
this.w.popIndent();
|
|
378
|
+
this.w.writeIndentedLine(`}`);
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
// Expression body
|
|
382
|
+
if (memberOf) {
|
|
383
|
+
this.w.writeIndentedLine(`${vis}${asyncKw}${decl.name}${typeParams}(${params})${retType} {`);
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
this.w.writeIndentedLine(`${vis}${asyncKw}function ${decl.name}${typeParams}(${params})${retType} {`);
|
|
387
|
+
}
|
|
388
|
+
this.w.pushIndent();
|
|
389
|
+
this.w.writeIndentedLine(`return ${this.emitExpr(decl.body)};`);
|
|
390
|
+
this.w.popIndent();
|
|
391
|
+
this.w.writeIndentedLine(`}`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
emitMemberFunDecl(decl) {
|
|
395
|
+
const isSuspend = AST.isSuspend(decl.modifiers);
|
|
396
|
+
const vis = this.visibilityPrefix(decl.modifiers);
|
|
397
|
+
const asyncKw = isSuspend ? "async " : "";
|
|
398
|
+
const typeParams = this.emitTypeParamsStr(decl.typeParams);
|
|
399
|
+
const params = this.emitParamsStr(decl.params);
|
|
400
|
+
const retType = decl.returnType && this.opts.emitTypes
|
|
401
|
+
? `: ${this.emitTypeRef(decl.returnType)}`
|
|
402
|
+
: "";
|
|
403
|
+
if (!decl.body) {
|
|
404
|
+
this.w.writeIndentedLine(`${vis}${asyncKw}${decl.name}${typeParams}(${params})${retType};`);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
if (decl.body.kind === "Block") {
|
|
408
|
+
this.w.writeIndentedLine(`${vis}${asyncKw}${decl.name}${typeParams}(${params})${retType} {`);
|
|
409
|
+
this.w.pushIndent();
|
|
410
|
+
this.emitBlock(decl.body);
|
|
411
|
+
this.w.popIndent();
|
|
412
|
+
this.w.writeIndentedLine(`}`);
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
this.w.writeIndentedLine(`${vis}${asyncKw}${decl.name}${typeParams}(${params})${retType} {`);
|
|
416
|
+
this.w.pushIndent();
|
|
417
|
+
this.w.writeIndentedLine(`return ${this.emitExpr(decl.body)};`);
|
|
418
|
+
this.w.popIndent();
|
|
419
|
+
this.w.writeIndentedLine(`}`);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
// ── component declarations → React function components ────────────────────
|
|
423
|
+
emitComponentDecl(decl) {
|
|
424
|
+
this.emitAnnotations(decl.modifiers);
|
|
425
|
+
this.hasComponents = true;
|
|
426
|
+
const vis = this.exportPrefix(decl.modifiers);
|
|
427
|
+
const params = this.emitComponentPropsStr(decl.params);
|
|
428
|
+
// Props interface
|
|
429
|
+
if (decl.params.length > 0) {
|
|
430
|
+
this.w.writeIndentedLine(`interface ${decl.name}Props {`);
|
|
431
|
+
this.w.pushIndent();
|
|
432
|
+
for (const p of decl.params) {
|
|
433
|
+
const optional = p.defaultValue ? "?" : "";
|
|
434
|
+
const typeStr = this.opts.emitTypes ? this.emitTypeRef(p.type) : "any";
|
|
435
|
+
this.w.writeIndentedLine(`readonly ${p.name}${optional}: ${typeStr};`);
|
|
436
|
+
}
|
|
437
|
+
this.w.popIndent();
|
|
438
|
+
this.w.writeIndentedLine(`}`);
|
|
439
|
+
this.w.writeLine();
|
|
440
|
+
}
|
|
441
|
+
const propsParam = decl.params.length > 0
|
|
442
|
+
? `{ ${decl.params.map((p) => p.name + (p.defaultValue ? ` = ${this.emitExpr(p.defaultValue)}` : "")).join(", ")} }: ${decl.name}Props`
|
|
443
|
+
: "";
|
|
444
|
+
this.w.writeIndentedLine(`${vis}function ${decl.name}(${propsParam}) {`);
|
|
445
|
+
this.w.pushIndent();
|
|
446
|
+
this.emitComponentBlock(decl.body);
|
|
447
|
+
this.w.popIndent();
|
|
448
|
+
this.w.writeIndentedLine(`}`);
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Emit the body of a `component fun` block.
|
|
452
|
+
* The last statement is emitted in tail position — if it is an expression,
|
|
453
|
+
* an if/else chain, or a when block, the innermost UI call is implicitly
|
|
454
|
+
* returned (Kotlin-style implicit return of the last expression).
|
|
455
|
+
*/
|
|
456
|
+
emitComponentBlock(block) {
|
|
457
|
+
const stmts = block.statements;
|
|
458
|
+
for (let i = 0; i < stmts.length; i++) {
|
|
459
|
+
const stmt = stmts[i];
|
|
460
|
+
if (i === stmts.length - 1) {
|
|
461
|
+
this.emitTailStmt(stmt);
|
|
462
|
+
}
|
|
463
|
+
else {
|
|
464
|
+
this.emitStmt(stmt);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
/** Emit a statement in tail position, adding implicit return where applicable. */
|
|
469
|
+
emitTailStmt(stmt) {
|
|
470
|
+
if (stmt.kind === "ExprStmt") {
|
|
471
|
+
// Implicit return: last expression in a component block
|
|
472
|
+
this.w.writeIndentedLine(`return ${this.emitExpr(stmt.expr)};`);
|
|
473
|
+
}
|
|
474
|
+
else if (stmt.kind === "IfStmt") {
|
|
475
|
+
this.emitIfStmtWithTailReturn(stmt);
|
|
476
|
+
}
|
|
477
|
+
else if (stmt.kind === "WhenStmt") {
|
|
478
|
+
this.emitWhenStmtWithTailReturn(stmt);
|
|
479
|
+
}
|
|
480
|
+
else {
|
|
481
|
+
this.emitStmt(stmt);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
/** Emit a block in tail position, treating its last statement as a tail expression. */
|
|
485
|
+
emitTailBlock(block) {
|
|
486
|
+
const stmts = block.statements;
|
|
487
|
+
for (let i = 0; i < stmts.length; i++) {
|
|
488
|
+
const stmt = stmts[i];
|
|
489
|
+
if (i === stmts.length - 1) {
|
|
490
|
+
this.emitTailStmt(stmt);
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
this.emitStmt(stmt);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
/** Like emitIfStmt but with tail-return applied recursively to each branch body. */
|
|
498
|
+
emitIfStmtWithTailReturn(stmt) {
|
|
499
|
+
this.w.writeIndentedLine(`if (${this.emitExpr(stmt.condition)}) {`);
|
|
500
|
+
this.w.pushIndent();
|
|
501
|
+
this.emitTailBlock(stmt.then);
|
|
502
|
+
this.w.popIndent();
|
|
503
|
+
if (stmt.else) {
|
|
504
|
+
if (stmt.else.kind === "IfStmt") {
|
|
505
|
+
this.w.writeIndented(`} else `);
|
|
506
|
+
this.emitIfStmtWithTailReturn(stmt.else);
|
|
507
|
+
}
|
|
508
|
+
else {
|
|
509
|
+
this.w.writeIndentedLine(`} else {`);
|
|
510
|
+
this.w.pushIndent();
|
|
511
|
+
this.emitTailBlock(stmt.else);
|
|
512
|
+
this.w.popIndent();
|
|
513
|
+
this.w.writeIndentedLine(`}`);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
else {
|
|
517
|
+
this.w.writeIndentedLine(`}`);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
/** Like emitWhenStmt but with tail-return applied recursively to each branch body. */
|
|
521
|
+
emitWhenStmtWithTailReturn(stmt) {
|
|
522
|
+
const subjectVar = stmt.subject ? "__when_subject__" : null;
|
|
523
|
+
if (stmt.subject) {
|
|
524
|
+
const binding = stmt.subject.binding ?? subjectVar;
|
|
525
|
+
this.w.writeIndentedLine(`const ${binding} = ${this.emitExpr(stmt.subject.expr)};`);
|
|
526
|
+
}
|
|
527
|
+
let first = true;
|
|
528
|
+
for (const branch of stmt.branches) {
|
|
529
|
+
if (branch.isElse) {
|
|
530
|
+
this.w.writeIndentedLine(first ? `{` : `} else {`);
|
|
531
|
+
}
|
|
532
|
+
else {
|
|
533
|
+
const cond = branch.conditions.map((c) => this.emitWhenCondition(c, subjectVar ?? "")).join(" || ");
|
|
534
|
+
this.w.writeIndentedLine(first ? `if (${cond}) {` : `} else if (${cond}) {`);
|
|
535
|
+
}
|
|
536
|
+
first = false;
|
|
537
|
+
this.w.pushIndent();
|
|
538
|
+
if (branch.body.kind === "Block") {
|
|
539
|
+
this.emitTailBlock(branch.body);
|
|
540
|
+
}
|
|
541
|
+
else {
|
|
542
|
+
this.w.writeIndentedLine(`return ${this.emitExpr(branch.body)};`);
|
|
543
|
+
}
|
|
544
|
+
this.w.popIndent();
|
|
545
|
+
}
|
|
546
|
+
this.w.writeIndentedLine(`}`);
|
|
547
|
+
}
|
|
548
|
+
// ── regular class ──────────────────────────────────────────────────────────
|
|
549
|
+
emitClassDecl(decl) {
|
|
550
|
+
this.emitAnnotations(decl.modifiers);
|
|
551
|
+
const vis = this.exportPrefix(decl.modifiers);
|
|
552
|
+
const abstract = decl.modifiers.modifiers.includes("abstract") ? "abstract " : "";
|
|
553
|
+
const typeParams = this.emitTypeParamsStr(decl.typeParams);
|
|
554
|
+
const superTypes = this.emitSuperTypesStr(decl.superTypes);
|
|
555
|
+
this.w.writeIndentedLine(`${vis}${abstract}class ${decl.name}${typeParams}${superTypes} {`);
|
|
556
|
+
this.w.pushIndent();
|
|
557
|
+
// The first supertype's delegateArgs become the `super(args)` call inside the constructor.
|
|
558
|
+
const superDelegateArgs = decl.superTypes[0]?.delegateArgs ?? null;
|
|
559
|
+
if (decl.primaryConstructor) {
|
|
560
|
+
const initBlocks = decl.body?.members.filter((m) => m.kind === "InitBlock") ?? [];
|
|
561
|
+
this.emitPrimaryConstructorProps(decl.primaryConstructor.params, initBlocks, superDelegateArgs);
|
|
562
|
+
}
|
|
563
|
+
else if (superDelegateArgs !== null) {
|
|
564
|
+
// No primary constructor but the supertype has explicit delegation args —
|
|
565
|
+
// emit a minimal constructor that forwards them to super().
|
|
566
|
+
const superArgsStr = superDelegateArgs.map((a) => this.emitExpr(a.value)).join(", ");
|
|
567
|
+
this.w.writeIndentedLine(`constructor() {`);
|
|
568
|
+
this.w.pushIndent();
|
|
569
|
+
this.w.writeIndentedLine(`super(${superArgsStr});`);
|
|
570
|
+
this.w.popIndent();
|
|
571
|
+
this.w.writeIndentedLine(`}`);
|
|
572
|
+
}
|
|
573
|
+
if (decl.body) {
|
|
574
|
+
this.emitClassBody(decl.body);
|
|
575
|
+
}
|
|
576
|
+
this.w.popIndent();
|
|
577
|
+
this.w.writeIndentedLine(`}`);
|
|
578
|
+
}
|
|
579
|
+
// ── data class → class with copy/equals/toString ──────────────────────────
|
|
580
|
+
emitDataClassDecl(decl, kindName) {
|
|
581
|
+
this.emitAnnotations(decl.modifiers);
|
|
582
|
+
const vis = this.exportPrefix(decl.modifiers);
|
|
583
|
+
const typeParams = this.emitTypeParamsStr(decl.typeParams);
|
|
584
|
+
const superTypes = this.emitSuperTypesStr(decl.superTypes);
|
|
585
|
+
const props = decl.primaryConstructor.params;
|
|
586
|
+
this.w.writeIndentedLine(`${vis}class ${decl.name}${typeParams}${superTypes} {`);
|
|
587
|
+
this.w.pushIndent();
|
|
588
|
+
// Discriminant for sealed class sub-types
|
|
589
|
+
if (kindName) {
|
|
590
|
+
this.w.writeIndentedLine(`readonly __kind = "${kindName}" as const;`);
|
|
591
|
+
}
|
|
592
|
+
// Constructor params → readonly properties
|
|
593
|
+
for (const p of props) {
|
|
594
|
+
const t = this.opts.emitTypes ? `: ${this.emitTypeRef(p.type)}` : "";
|
|
595
|
+
this.w.writeIndentedLine(`readonly ${p.name}${t};`);
|
|
596
|
+
}
|
|
597
|
+
this.w.writeLine();
|
|
598
|
+
// Constructor
|
|
599
|
+
const ctorParams = props.map((p) => {
|
|
600
|
+
const t = this.opts.emitTypes ? `: ${this.emitTypeRef(p.type)}` : "";
|
|
601
|
+
return `${p.name}${t}`;
|
|
602
|
+
}).join(", ");
|
|
603
|
+
this.w.writeIndentedLine(`constructor(${ctorParams}) {`);
|
|
604
|
+
this.w.pushIndent();
|
|
605
|
+
// Emit super() with actual delegation args (or empty call if delegateArgs is []).
|
|
606
|
+
const dataSuperDelegateArgs = decl.superTypes[0]?.delegateArgs ?? null;
|
|
607
|
+
if (dataSuperDelegateArgs !== null) {
|
|
608
|
+
const superArgsStr = dataSuperDelegateArgs.map((a) => this.emitExpr(a.value)).join(", ");
|
|
609
|
+
this.w.writeIndentedLine(`super(${superArgsStr});`);
|
|
610
|
+
}
|
|
611
|
+
for (const p of props) {
|
|
612
|
+
this.w.writeIndentedLine(`this.${p.name} = ${p.name};`);
|
|
613
|
+
}
|
|
614
|
+
this.w.popIndent();
|
|
615
|
+
this.w.writeIndentedLine(`}`);
|
|
616
|
+
this.w.writeLine();
|
|
617
|
+
// copy()
|
|
618
|
+
const copyParams = props.map((p) => {
|
|
619
|
+
const t = this.opts.emitTypes ? `: ${this.emitTypeRef(p.type)}` : "";
|
|
620
|
+
return `${p.name}${t} = this.${p.name}`;
|
|
621
|
+
}).join(", ");
|
|
622
|
+
const copyArgs = props.map((p) => p.name).join(", ");
|
|
623
|
+
this.w.writeIndentedLine(`copy(${copyParams}): ${decl.name} {`);
|
|
624
|
+
this.w.pushIndent();
|
|
625
|
+
this.w.writeIndentedLine(`return new ${decl.name}(${copyArgs});`);
|
|
626
|
+
this.w.popIndent();
|
|
627
|
+
this.w.writeIndentedLine(`}`);
|
|
628
|
+
this.w.writeLine();
|
|
629
|
+
// equals()
|
|
630
|
+
this.runtimeSymbolsNeeded.add("jalvinEquals");
|
|
631
|
+
const eqChecks = props.map((p) => `jalvinEquals(this.${p.name}, other.${p.name})`).join(" && ") || "true";
|
|
632
|
+
this.w.writeIndentedLine(`equals(other: unknown): boolean {`);
|
|
633
|
+
this.w.pushIndent();
|
|
634
|
+
this.w.writeIndentedLine(`if (!(other instanceof ${decl.name})) return false;`);
|
|
635
|
+
this.w.writeIndentedLine(`return ${eqChecks};`);
|
|
636
|
+
this.w.popIndent();
|
|
637
|
+
this.w.writeIndentedLine(`}`);
|
|
638
|
+
this.w.writeLine();
|
|
639
|
+
// toString()
|
|
640
|
+
const toStringParts = props.map((p) => `${p.name}=\${this.${p.name}}`).join(", ");
|
|
641
|
+
this.w.writeIndentedLine(`toString(): string {`);
|
|
642
|
+
this.w.pushIndent();
|
|
643
|
+
this.w.writeIndentedLine(`return \`${decl.name}(${toStringParts})\`;`);
|
|
644
|
+
this.w.popIndent();
|
|
645
|
+
this.w.writeIndentedLine(`}`);
|
|
646
|
+
this.w.writeLine();
|
|
647
|
+
// hashCode() — djb2
|
|
648
|
+
this.w.writeIndentedLine(`hashCode(): number {`);
|
|
649
|
+
this.w.pushIndent();
|
|
650
|
+
this.w.writeIndentedLine(`let h = 17;`);
|
|
651
|
+
for (const p of props) {
|
|
652
|
+
this.w.writeIndentedLine(`h = h * 31 + String(this.${p.name}).split("").reduce((a, c) => (a * 31 + c.charCodeAt(0)) >>> 0, 0);`);
|
|
653
|
+
}
|
|
654
|
+
this.w.writeIndentedLine(`return h >>> 0;`);
|
|
655
|
+
this.w.popIndent();
|
|
656
|
+
this.w.writeIndentedLine(`}`);
|
|
657
|
+
if (decl.body) {
|
|
658
|
+
this.w.writeLine();
|
|
659
|
+
this.emitClassBody(decl.body);
|
|
660
|
+
}
|
|
661
|
+
this.w.popIndent();
|
|
662
|
+
this.w.writeIndentedLine(`}`);
|
|
663
|
+
}
|
|
664
|
+
// ── sealed class → TS abstract class + namespace merging ──────────────────
|
|
665
|
+
emitSealedClassDecl(decl) {
|
|
666
|
+
this.emitAnnotations(decl.modifiers);
|
|
667
|
+
const vis = this.exportPrefix(decl.modifiers);
|
|
668
|
+
const typeParams = this.emitTypeParamsStr(decl.typeParams);
|
|
669
|
+
const subDecls = [];
|
|
670
|
+
const otherMembers = [];
|
|
671
|
+
if (decl.body) {
|
|
672
|
+
for (const member of decl.body.members) {
|
|
673
|
+
if (member.kind === "ClassDecl" ||
|
|
674
|
+
member.kind === "DataClassDecl" ||
|
|
675
|
+
member.kind === "ObjectDecl") {
|
|
676
|
+
subDecls.push(member);
|
|
677
|
+
}
|
|
678
|
+
else if (member.kind !== "InitBlock") {
|
|
679
|
+
otherMembers.push(member);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
// Emit the abstract base class (only non-subtype members in the body)
|
|
684
|
+
this.w.writeIndentedLine(`${vis}abstract class ${decl.name}${typeParams} {`);
|
|
685
|
+
this.w.pushIndent();
|
|
686
|
+
this.w.writeIndentedLine(`abstract readonly __kind: string;`);
|
|
687
|
+
for (const member of otherMembers) {
|
|
688
|
+
this.emitClassMember(member);
|
|
689
|
+
this.w.writeLine();
|
|
690
|
+
}
|
|
691
|
+
this.w.popIndent();
|
|
692
|
+
this.w.writeIndentedLine(`}`);
|
|
693
|
+
this.w.writeLine();
|
|
694
|
+
if (subDecls.length === 0)
|
|
695
|
+
return;
|
|
696
|
+
// Emit non-object sub-declarations at module level (before namespace)
|
|
697
|
+
for (const sub of subDecls) {
|
|
698
|
+
if (sub.kind === "DataClassDecl") {
|
|
699
|
+
this.emitDataClassDecl(sub, sub.name);
|
|
700
|
+
this.w.writeLine();
|
|
701
|
+
}
|
|
702
|
+
else if (sub.kind === "ClassDecl") {
|
|
703
|
+
this.emitClassDecl(sub);
|
|
704
|
+
this.w.writeLine();
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
// Emit namespace merging so SealedClass.SubType is accessible
|
|
708
|
+
this.w.writeIndentedLine(`${vis}namespace ${decl.name} {`);
|
|
709
|
+
this.w.pushIndent();
|
|
710
|
+
for (const sub of subDecls) {
|
|
711
|
+
if (sub.kind === "ObjectDecl") {
|
|
712
|
+
// Singleton object — emit inline in namespace with __kind discriminant
|
|
713
|
+
const superStr = sub.superTypes.length > 0
|
|
714
|
+
? this.emitSuperTypesStr(sub.superTypes)
|
|
715
|
+
: ` extends ${decl.name}`;
|
|
716
|
+
const membersArr = [];
|
|
717
|
+
membersArr.push(`readonly __kind = "${sub.name}" as const;`);
|
|
718
|
+
if (sub.body) {
|
|
719
|
+
for (const m of sub.body.members) {
|
|
720
|
+
if (m.kind !== "InitBlock")
|
|
721
|
+
membersArr.push(this.captureClassMember(m));
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
const membersStr = membersArr.join(" ");
|
|
725
|
+
this.w.writeIndentedLine(`export const ${sub.name} = new (class${superStr} { ${membersStr} })();`);
|
|
726
|
+
}
|
|
727
|
+
else {
|
|
728
|
+
// DataClassDecl / ClassDecl — already emitted at module level, re-export
|
|
729
|
+
this.w.writeIndentedLine(`export { ${sub.name} };`);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
this.w.popIndent();
|
|
733
|
+
this.w.writeIndentedLine(`}`);
|
|
734
|
+
}
|
|
735
|
+
// ── enum class → TypeScript const enum + class ────────────────────────────
|
|
736
|
+
emitEnumClassDecl(decl) {
|
|
737
|
+
const vis = this.exportPrefix(decl.modifiers);
|
|
738
|
+
const typeParams = this.emitTypeParamsStr(decl.typeParams);
|
|
739
|
+
// Emit the enum as a TypeScript class with static singleton instances.
|
|
740
|
+
// This preserves the `EnumClass.ENTRY` access pattern and allows
|
|
741
|
+
// methods/properties to be attached to entries.
|
|
742
|
+
this.w.writeIndentedLine(`${vis}class ${decl.name}${typeParams} {`);
|
|
743
|
+
this.w.pushIndent();
|
|
744
|
+
// Private constructor so entries are the only instances
|
|
745
|
+
if (decl.primaryConstructor && decl.primaryConstructor.params.length > 0) {
|
|
746
|
+
const ctorParams = decl.primaryConstructor.params
|
|
747
|
+
.map((p) => {
|
|
748
|
+
const ro = p.propertyKind === "val" ? "readonly " : "";
|
|
749
|
+
const type = this.opts.emitTypes ? `: ${this.emitTypeRef(p.type)}` : "";
|
|
750
|
+
return `${ro}${p.name}${type}`;
|
|
751
|
+
})
|
|
752
|
+
.join(", ");
|
|
753
|
+
this.w.writeIndentedLine(`private constructor(${ctorParams}) {}`);
|
|
754
|
+
}
|
|
755
|
+
else {
|
|
756
|
+
this.w.writeIndentedLine(`private constructor(readonly name: string, readonly ordinal: number) {}`);
|
|
757
|
+
}
|
|
758
|
+
this.w.writeLine();
|
|
759
|
+
// Static entry instances
|
|
760
|
+
for (let i = 0; i < decl.entries.length; i++) {
|
|
761
|
+
const entry = decl.entries[i];
|
|
762
|
+
const args = entry.args.length > 0
|
|
763
|
+
? entry.args.map((a) => this.emitExpr(a.value)).join(", ")
|
|
764
|
+
: `"${entry.name}", ${i}`;
|
|
765
|
+
this.w.writeIndentedLine(`static readonly ${entry.name} = new ${decl.name}(${args});`);
|
|
766
|
+
}
|
|
767
|
+
if (decl.entries.length > 0) {
|
|
768
|
+
this.w.writeLine();
|
|
769
|
+
// values() helper
|
|
770
|
+
const entryList = decl.entries.map((e) => `${decl.name}.${e.name}`).join(", ");
|
|
771
|
+
this.w.writeIndentedLine(`static values(): ${decl.name}[] { return [${entryList}]; }`);
|
|
772
|
+
this.w.writeIndentedLine(`static valueOf(name: string): ${decl.name} {`);
|
|
773
|
+
this.w.pushIndent();
|
|
774
|
+
this.w.writeIndentedLine(`const v = ${decl.name}.values().find(e => e.name === name);`);
|
|
775
|
+
this.w.writeIndentedLine(`if (!v) throw new Error(\`No enum constant ${decl.name}.\${name}\`);`);
|
|
776
|
+
this.w.writeIndentedLine(`return v;`);
|
|
777
|
+
this.w.popIndent();
|
|
778
|
+
this.w.writeIndentedLine(`}`);
|
|
779
|
+
}
|
|
780
|
+
if (decl.body) {
|
|
781
|
+
this.w.writeLine();
|
|
782
|
+
this.emitClassBody(decl.body);
|
|
783
|
+
}
|
|
784
|
+
this.w.popIndent();
|
|
785
|
+
this.w.writeIndentedLine(`}`);
|
|
786
|
+
}
|
|
787
|
+
// ── destructuring declaration — val (a, b) = expr ─────────────────────────
|
|
788
|
+
emitDestructuringDecl(decl, _memberOf) {
|
|
789
|
+
const kw = decl.mutable ? "let" : "const";
|
|
790
|
+
const names = decl.names.map((n) => n ?? "_").join(", ");
|
|
791
|
+
const init = this.emitExpr(decl.initializer);
|
|
792
|
+
this.w.writeIndentedLine(`${kw} [${names}] = ${init};`);
|
|
793
|
+
}
|
|
794
|
+
// ── interface ──────────────────────────────────────────────────────────────
|
|
795
|
+
emitInterfaceDecl(decl) {
|
|
796
|
+
this.emitAnnotations(decl.modifiers);
|
|
797
|
+
const vis = this.exportPrefix(decl.modifiers);
|
|
798
|
+
const typeParams = this.emitTypeParamsStr(decl.typeParams);
|
|
799
|
+
const superTypes = decl.superTypes.length > 0
|
|
800
|
+
? ` extends ${decl.superTypes.map((s) => this.emitTypeRef(s.type)).join(", ")}`
|
|
801
|
+
: "";
|
|
802
|
+
this.w.writeIndentedLine(`${vis}interface ${decl.name}${typeParams}${superTypes} {`);
|
|
803
|
+
this.w.pushIndent();
|
|
804
|
+
if (decl.body) {
|
|
805
|
+
for (const member of decl.body.members) {
|
|
806
|
+
this.emitClassMember(member);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
this.w.popIndent();
|
|
810
|
+
this.w.writeIndentedLine(`}`);
|
|
811
|
+
}
|
|
812
|
+
// ── object declaration → singleton ────────────────────────────────────────
|
|
813
|
+
emitObjectDecl(decl) {
|
|
814
|
+
if (!decl.name) {
|
|
815
|
+
// Anonymous object — emitted inline
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
this.emitAnnotations(decl.modifiers);
|
|
819
|
+
const vis = this.exportPrefix(decl.modifiers);
|
|
820
|
+
const superTypes = this.emitSuperTypesStr(decl.superTypes);
|
|
821
|
+
this.w.writeIndentedLine(`${vis}const ${decl.name} = new (class${superTypes} {`);
|
|
822
|
+
this.w.pushIndent();
|
|
823
|
+
this.emitClassBody(decl.body);
|
|
824
|
+
this.w.popIndent();
|
|
825
|
+
this.w.writeIndentedLine(`})();`);
|
|
826
|
+
}
|
|
827
|
+
// ── type alias ─────────────────────────────────────────────────────────────
|
|
828
|
+
emitTypeAliasDecl(decl) {
|
|
829
|
+
const vis = this.exportPrefix(decl.modifiers);
|
|
830
|
+
const typeParams = this.emitTypeParamsStr(decl.typeParams);
|
|
831
|
+
this.w.writeIndentedLine(`${vis}type ${decl.name}${typeParams} = ${this.emitTypeRef(decl.type)};`);
|
|
832
|
+
}
|
|
833
|
+
// ── extension functions ────────────────────────────────────────────────────
|
|
834
|
+
// Emitted as standalone free functions with receiver as explicit first param.
|
|
835
|
+
// For class types, also patched onto the prototype for dot-call syntax.
|
|
836
|
+
// For primitive types, call sites are rewritten via `primitiveExtensions`.
|
|
837
|
+
emitExtensionFunDecl(decl) {
|
|
838
|
+
const isSuspend = AST.isSuspend(decl.modifiers);
|
|
839
|
+
const asyncKw = isSuspend ? "async " : "";
|
|
840
|
+
const receiverType = this.emitTypeRef(decl.receiver);
|
|
841
|
+
const typeParams = this.emitTypeParamsStr(decl.typeParams);
|
|
842
|
+
const params = this.emitParamsStr(decl.params);
|
|
843
|
+
const retType = decl.returnType && this.opts.emitTypes
|
|
844
|
+
? `: ${this.emitTypeRef(decl.returnType)}`
|
|
845
|
+
: "";
|
|
846
|
+
const fnName = `__ext_${receiverType.replace(/[^a-zA-Z0-9_]/g, "_")}_${decl.name}`;
|
|
847
|
+
// Emit as a free function: receiver is the first explicit parameter `$receiver`.
|
|
848
|
+
// Inside the function body, `this` is rebound to `$receiver` so Jalvin's
|
|
849
|
+
// `this.prop` references work correctly.
|
|
850
|
+
this.w.writeIndentedLine(`${asyncKw}function ${fnName}${typeParams}($receiver: ${receiverType}${params ? ", " + params : ""})${retType} {`);
|
|
851
|
+
this.w.pushIndent();
|
|
852
|
+
if (decl.body.kind === "Block") {
|
|
853
|
+
this.w.writeIndentedLine(`return (function(this: ${receiverType})${retType} {`);
|
|
854
|
+
this.w.pushIndent();
|
|
855
|
+
this.emitBlock(decl.body);
|
|
856
|
+
this.w.popIndent();
|
|
857
|
+
this.w.writeIndentedLine(`}).call($receiver);`);
|
|
858
|
+
}
|
|
859
|
+
else {
|
|
860
|
+
this.w.writeIndentedLine(`return (function(this: ${receiverType})${retType} { return ${this.emitExpr(decl.body)}; }).call($receiver);`);
|
|
861
|
+
}
|
|
862
|
+
this.w.popIndent();
|
|
863
|
+
this.w.writeIndentedLine(`}`);
|
|
864
|
+
this.w.writeLine();
|
|
865
|
+
const simpleReceiverName = this.receiverClassName(decl.receiver);
|
|
866
|
+
if (simpleReceiverName) {
|
|
867
|
+
if (PRIMITIVE_TYPES.has(simpleReceiverName)) {
|
|
868
|
+
// Register for call-site rewriting — cannot patch primitive prototypes
|
|
869
|
+
if (!this.primitiveExtensions.has(simpleReceiverName)) {
|
|
870
|
+
this.primitiveExtensions.set(simpleReceiverName, new Map());
|
|
871
|
+
}
|
|
872
|
+
this.primitiveExtensions.get(simpleReceiverName).set(decl.name, fnName);
|
|
873
|
+
}
|
|
874
|
+
else {
|
|
875
|
+
// Class types — prototype monkey-patch for dot-call syntax
|
|
876
|
+
this.w.writeIndentedLine(`// Extension: ${receiverType}.${decl.name}`);
|
|
877
|
+
this.w.writeIndentedLine(`(${simpleReceiverName}.prototype as any).${decl.name} = function(this: ${receiverType}, ...args: unknown[]) { return (${fnName} as Function)(this, ...args); };`);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
receiverClassName(ref) {
|
|
882
|
+
if (ref.kind === "SimpleTypeRef")
|
|
883
|
+
return ref.name[ref.name.length - 1] ?? null;
|
|
884
|
+
if (ref.kind === "GenericTypeRef")
|
|
885
|
+
return ref.base.name[ref.base.name.length - 1] ?? null;
|
|
886
|
+
return null;
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* Maps a JType to the primitive receiver type name used as a key
|
|
890
|
+
* in `primitiveExtensions` (e.g. JType `{ tag: "string" }` → `"String"`).
|
|
891
|
+
* Returns null for non-primitive / unknown types.
|
|
892
|
+
*/
|
|
893
|
+
jTypeToReceiverName(type) {
|
|
894
|
+
const tagToReceiverName = {
|
|
895
|
+
string: "String",
|
|
896
|
+
int: "Int",
|
|
897
|
+
long: "Long",
|
|
898
|
+
float: "Float",
|
|
899
|
+
double: "Double",
|
|
900
|
+
boolean: "Boolean",
|
|
901
|
+
char: "Char",
|
|
902
|
+
};
|
|
903
|
+
return tagToReceiverName[type.tag] ?? null;
|
|
904
|
+
}
|
|
905
|
+
classHasInvokeOperator(decl) {
|
|
906
|
+
if (!decl.body)
|
|
907
|
+
return false;
|
|
908
|
+
return decl.body.members.some((m) => m.kind === "FunDecl" &&
|
|
909
|
+
m.name === "invoke" &&
|
|
910
|
+
m.modifiers.modifiers.includes("operator"));
|
|
911
|
+
}
|
|
912
|
+
// ── property declaration ───────────────────────────────────────────────────
|
|
913
|
+
emitPropertyDecl(decl, member, isLocal = false) {
|
|
914
|
+
this.emitAnnotations(decl.modifiers);
|
|
915
|
+
const vis = member ? this.visibilityPrefix(decl.modifiers) : isLocal ? "" : this.exportPrefix(decl.modifiers);
|
|
916
|
+
const isConst = decl.modifiers.modifiers.includes("const");
|
|
917
|
+
const isLateinit = decl.modifiers.modifiers.includes("lateinit");
|
|
918
|
+
// `const val` → `const` at module level; inside classes treated as `static readonly`
|
|
919
|
+
const kw = member
|
|
920
|
+
? (decl.mutable ? "" : "readonly ")
|
|
921
|
+
: (isConst ? "const " : decl.mutable ? "let " : "const ");
|
|
922
|
+
const type = decl.type && this.opts.emitTypes ? `: ${this.emitTypeRef(decl.type)}` : "";
|
|
923
|
+
if (decl.delegate) {
|
|
924
|
+
// Delegated property — wrap in a getter/setter pair using the delegate
|
|
925
|
+
this.runtimeSymbolsNeeded.add("delegate");
|
|
926
|
+
const delegateExpr = this.emitExpr(decl.delegate);
|
|
927
|
+
if (member) {
|
|
928
|
+
this.w.writeIndentedLine(`${vis}get ${decl.name}()${type} { return delegate(${delegateExpr}, "${decl.name}", this).getValue(); }`);
|
|
929
|
+
if (decl.mutable) {
|
|
930
|
+
this.w.writeIndentedLine(`${vis}set ${decl.name}(v: any) { delegate(${delegateExpr}, "${decl.name}", this).setValue(v); }`);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
else {
|
|
934
|
+
this.w.writeIndentedLine(`${kw}${decl.name}${type} = ${delegateExpr};`);
|
|
935
|
+
}
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
const init = decl.initializer ? ` = ${this.emitExpr(decl.initializer)}` : (isLateinit ? "" : "");
|
|
939
|
+
if (member) {
|
|
940
|
+
const modStr = isConst ? "static readonly " : decl.mutable ? "" : "readonly ";
|
|
941
|
+
this.w.writeIndentedLine(`${vis}${modStr}${decl.name}${type}${init};`);
|
|
942
|
+
}
|
|
943
|
+
else {
|
|
944
|
+
this.w.writeIndentedLine(`${vis}${kw}${decl.name}${type}${init};`);
|
|
945
|
+
}
|
|
946
|
+
if (decl.getter) {
|
|
947
|
+
this.emitPropertyAccessor("get", decl.name, decl.getter, member, type);
|
|
948
|
+
}
|
|
949
|
+
if (decl.setter) {
|
|
950
|
+
this.emitPropertyAccessor("set", decl.name, decl.setter, member, type);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
emitPropertyAccessor(kind, name, acc, member, type) {
|
|
954
|
+
const retOrParam = kind === "get" ? type : `(value: any)`;
|
|
955
|
+
const vis = this.visibilityPrefix(acc.modifiers);
|
|
956
|
+
this.w.writeIndentedLine(`${vis}${kind} ${name}()${retOrParam} {`);
|
|
957
|
+
this.w.pushIndent();
|
|
958
|
+
if (acc.body) {
|
|
959
|
+
if (acc.body.kind === "Block")
|
|
960
|
+
this.emitBlock(acc.body);
|
|
961
|
+
else
|
|
962
|
+
this.w.writeIndentedLine(`return ${this.emitExpr(acc.body)};`);
|
|
963
|
+
}
|
|
964
|
+
this.w.popIndent();
|
|
965
|
+
this.w.writeIndentedLine(`}`);
|
|
966
|
+
}
|
|
967
|
+
// ── class body ─────────────────────────────────────────────────────────────
|
|
968
|
+
emitClassBody(body) {
|
|
969
|
+
for (const member of body.members) {
|
|
970
|
+
// InitBlocks are emitted inside the constructor by emitPrimaryConstructorProps
|
|
971
|
+
if (member.kind === "InitBlock")
|
|
972
|
+
continue;
|
|
973
|
+
this.emitClassMember(member);
|
|
974
|
+
this.w.writeLine();
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
emitClassMember(member) {
|
|
978
|
+
switch (member.kind) {
|
|
979
|
+
case "FunDecl":
|
|
980
|
+
this.emitMemberFunDecl(member);
|
|
981
|
+
break;
|
|
982
|
+
case "ComponentDecl":
|
|
983
|
+
this.emitComponentDecl(member);
|
|
984
|
+
break;
|
|
985
|
+
case "PropertyDecl":
|
|
986
|
+
this.emitPropertyDecl(member, true);
|
|
987
|
+
break;
|
|
988
|
+
case "ClassDecl":
|
|
989
|
+
this.emitClassDecl(member);
|
|
990
|
+
break;
|
|
991
|
+
case "DataClassDecl":
|
|
992
|
+
this.emitDataClassDecl(member);
|
|
993
|
+
break;
|
|
994
|
+
case "SealedClassDecl":
|
|
995
|
+
this.emitSealedClassDecl(member);
|
|
996
|
+
break;
|
|
997
|
+
case "EnumClassDecl":
|
|
998
|
+
this.emitEnumClassDecl(member);
|
|
999
|
+
break;
|
|
1000
|
+
case "ObjectDecl":
|
|
1001
|
+
this.emitObjectDecl(member);
|
|
1002
|
+
break;
|
|
1003
|
+
case "CompanionObject":
|
|
1004
|
+
this.emitCompanionObject(member);
|
|
1005
|
+
break;
|
|
1006
|
+
case "InitBlock": /* handled in constructor */ break;
|
|
1007
|
+
case "SecondaryConstructor":
|
|
1008
|
+
this.emitSecondaryConstructor(member);
|
|
1009
|
+
break;
|
|
1010
|
+
case "ExtensionFunDecl":
|
|
1011
|
+
this.emitExtensionFunDecl(member);
|
|
1012
|
+
break;
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
emitPrimaryConstructorProps(params, initBlocks, superDelegateArgs) {
|
|
1016
|
+
for (const p of params) {
|
|
1017
|
+
if (p.propertyKind) {
|
|
1018
|
+
const ro = p.propertyKind === "val" ? "readonly " : "";
|
|
1019
|
+
const type = this.opts.emitTypes ? `: ${this.emitTypeRef(p.type)}` : "";
|
|
1020
|
+
this.w.writeIndentedLine(`${ro}${p.name}${type};`);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
// Constructor
|
|
1024
|
+
const ctorParams = params.map((p) => {
|
|
1025
|
+
const ro = p.propertyKind === "val" ? "readonly " : p.propertyKind === "var" ? "" : "";
|
|
1026
|
+
const type = this.opts.emitTypes ? `: ${this.emitTypeRef(p.type)}` : "";
|
|
1027
|
+
const def = p.defaultValue ? ` = ${this.emitExpr(p.defaultValue)}` : "";
|
|
1028
|
+
return `${p.name}${type}${def}`;
|
|
1029
|
+
});
|
|
1030
|
+
this.w.writeIndentedLine(`constructor(${ctorParams.join(", ")}) {`);
|
|
1031
|
+
this.w.pushIndent();
|
|
1032
|
+
// super() call must be the first statement if there is a supertype.
|
|
1033
|
+
if (superDelegateArgs !== null && superDelegateArgs !== undefined) {
|
|
1034
|
+
const superArgsStr = superDelegateArgs.map((a) => this.emitExpr(a.value)).join(", ");
|
|
1035
|
+
this.w.writeIndentedLine(`super(${superArgsStr});`);
|
|
1036
|
+
}
|
|
1037
|
+
for (const p of params) {
|
|
1038
|
+
if (p.propertyKind) {
|
|
1039
|
+
this.w.writeIndentedLine(`this.${p.name} = ${p.name};`);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
// Emit init{} blocks inside the constructor, in declaration order
|
|
1043
|
+
for (const ib of (initBlocks ?? [])) {
|
|
1044
|
+
this.emitBlock(ib.body);
|
|
1045
|
+
}
|
|
1046
|
+
this.w.popIndent();
|
|
1047
|
+
this.w.writeIndentedLine(`}`);
|
|
1048
|
+
this.w.writeLine();
|
|
1049
|
+
}
|
|
1050
|
+
emitCompanionObject(co) {
|
|
1051
|
+
// Emit companion object members as `static` members of the outer class so that
|
|
1052
|
+
// `MyClass.factoryMethod()` works directly (standard semantics).
|
|
1053
|
+
for (const member of co.body.members) {
|
|
1054
|
+
if (member.kind === "FunDecl") {
|
|
1055
|
+
const vis = this.visibilityPrefix(member.modifiers);
|
|
1056
|
+
const params = this.emitParamsStr(member.params);
|
|
1057
|
+
const retType = member.returnType && this.opts.emitTypes
|
|
1058
|
+
? `: ${this.emitTypeRef(member.returnType)}` : "";
|
|
1059
|
+
const asyncKw = AST.isSuspend(member.modifiers) ? "async " : "";
|
|
1060
|
+
if (member.body) {
|
|
1061
|
+
if (member.body.kind === "Block") {
|
|
1062
|
+
this.w.writeIndentedLine(`${vis}static ${asyncKw}${member.name}(${params})${retType} {`);
|
|
1063
|
+
this.w.pushIndent();
|
|
1064
|
+
this.emitBlock(member.body);
|
|
1065
|
+
this.w.popIndent();
|
|
1066
|
+
this.w.writeIndentedLine(`}`);
|
|
1067
|
+
}
|
|
1068
|
+
else {
|
|
1069
|
+
this.w.writeIndentedLine(`${vis}static ${asyncKw}${member.name}(${params})${retType} { return ${this.emitExpr(member.body)}; }`);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
else {
|
|
1073
|
+
this.w.writeIndentedLine(`${vis}static abstract ${member.name}(${params})${retType};`);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
else if (member.kind === "PropertyDecl") {
|
|
1077
|
+
const vis = this.visibilityPrefix(member.modifiers);
|
|
1078
|
+
const kw = member.mutable ? "" : "readonly ";
|
|
1079
|
+
const typeAnn = member.type && this.opts.emitTypes ? `: ${this.emitTypeRef(member.type)}` : "";
|
|
1080
|
+
const init = member.initializer ? ` = ${this.emitExpr(member.initializer)}` : "";
|
|
1081
|
+
this.w.writeIndentedLine(`${vis}static ${kw}${member.name}${typeAnn}${init};`);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
emitSecondaryConstructor(ctor) {
|
|
1086
|
+
const params = this.emitParamsStr(ctor.params);
|
|
1087
|
+
this.w.writeIndentedLine(`constructor(${params}) {`);
|
|
1088
|
+
this.w.pushIndent();
|
|
1089
|
+
if (ctor.delegation) {
|
|
1090
|
+
const args = ctor.delegateArgs.map((a) => this.emitExpr(a.value)).join(", ");
|
|
1091
|
+
this.w.writeIndentedLine(`${ctor.delegation}(${args});`);
|
|
1092
|
+
}
|
|
1093
|
+
this.emitBlock(ctor.body);
|
|
1094
|
+
this.w.popIndent();
|
|
1095
|
+
this.w.writeIndentedLine(`}`);
|
|
1096
|
+
}
|
|
1097
|
+
// ── Statements ─────────────────────────────────────────────────────────────
|
|
1098
|
+
emitBlock(block) {
|
|
1099
|
+
for (const stmt of block.statements) {
|
|
1100
|
+
this.emitStmt(stmt);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
emitStmt(stmt) {
|
|
1104
|
+
switch (stmt.kind) {
|
|
1105
|
+
case "Block":
|
|
1106
|
+
this.w.writeIndentedLine(`{`);
|
|
1107
|
+
this.w.pushIndent();
|
|
1108
|
+
this.emitBlock(stmt);
|
|
1109
|
+
this.w.popIndent();
|
|
1110
|
+
this.w.writeIndentedLine(`}`);
|
|
1111
|
+
break;
|
|
1112
|
+
case "PropertyDecl":
|
|
1113
|
+
this.emitPropertyDecl(stmt, false, true);
|
|
1114
|
+
break;
|
|
1115
|
+
case "DestructuringDecl":
|
|
1116
|
+
this.emitDestructuringDecl(stmt, false);
|
|
1117
|
+
break;
|
|
1118
|
+
case "ExprStmt":
|
|
1119
|
+
this.w.writeIndentedLine(`${this.emitExpr(stmt.expr)};`);
|
|
1120
|
+
break;
|
|
1121
|
+
case "ReturnStmt": {
|
|
1122
|
+
const val = stmt.value ? ` ${this.emitExpr(stmt.value)}` : "";
|
|
1123
|
+
this.w.writeIndentedLine(`return${val};`);
|
|
1124
|
+
break;
|
|
1125
|
+
}
|
|
1126
|
+
case "ThrowStmt":
|
|
1127
|
+
this.w.writeIndentedLine(`throw ${this.emitExpr(stmt.value)};`);
|
|
1128
|
+
break;
|
|
1129
|
+
case "BreakStmt":
|
|
1130
|
+
this.w.writeIndentedLine(stmt.label ? `break ${stmt.label};` : `break;`);
|
|
1131
|
+
break;
|
|
1132
|
+
case "ContinueStmt":
|
|
1133
|
+
this.w.writeIndentedLine(stmt.label ? `continue ${stmt.label};` : `continue;`);
|
|
1134
|
+
break;
|
|
1135
|
+
case "IfStmt":
|
|
1136
|
+
this.emitIfStmt(stmt);
|
|
1137
|
+
break;
|
|
1138
|
+
case "WhenStmt":
|
|
1139
|
+
this.emitWhenStmt(stmt);
|
|
1140
|
+
break;
|
|
1141
|
+
case "ForStmt":
|
|
1142
|
+
this.emitForStmt(stmt);
|
|
1143
|
+
break;
|
|
1144
|
+
case "WhileStmt":
|
|
1145
|
+
this.w.writeIndentedLine(`while (${this.emitExpr(stmt.condition)}) {`);
|
|
1146
|
+
this.w.pushIndent();
|
|
1147
|
+
this.emitBlock(stmt.body);
|
|
1148
|
+
this.w.popIndent();
|
|
1149
|
+
this.w.writeIndentedLine(`}`);
|
|
1150
|
+
break;
|
|
1151
|
+
case "DoWhileStmt":
|
|
1152
|
+
this.w.writeIndentedLine(`do {`);
|
|
1153
|
+
this.w.pushIndent();
|
|
1154
|
+
this.emitBlock(stmt.body);
|
|
1155
|
+
this.w.popIndent();
|
|
1156
|
+
this.w.writeIndentedLine(`} while (${this.emitExpr(stmt.condition)});`);
|
|
1157
|
+
break;
|
|
1158
|
+
case "TryCatchStmt":
|
|
1159
|
+
this.emitTryCatch(stmt);
|
|
1160
|
+
break;
|
|
1161
|
+
case "LabeledStmt":
|
|
1162
|
+
this.w.writeIndentedLine(`${stmt.label}:`);
|
|
1163
|
+
this.emitStmt(stmt.body);
|
|
1164
|
+
break;
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
emitIfStmt(stmt) {
|
|
1168
|
+
this.w.writeIndentedLine(`if (${this.emitExpr(stmt.condition)}) {`);
|
|
1169
|
+
this.w.pushIndent();
|
|
1170
|
+
this.emitBlock(stmt.then);
|
|
1171
|
+
this.w.popIndent();
|
|
1172
|
+
if (stmt.else) {
|
|
1173
|
+
if (stmt.else.kind === "IfStmt") {
|
|
1174
|
+
this.w.writeIndented(`} else `);
|
|
1175
|
+
this.emitIfStmt(stmt.else);
|
|
1176
|
+
}
|
|
1177
|
+
else {
|
|
1178
|
+
this.w.writeIndentedLine(`} else {`);
|
|
1179
|
+
this.w.pushIndent();
|
|
1180
|
+
this.emitBlock(stmt.else);
|
|
1181
|
+
this.w.popIndent();
|
|
1182
|
+
this.w.writeIndentedLine(`}`);
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
else {
|
|
1186
|
+
this.w.writeIndentedLine(`}`);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
emitWhenStmt(stmt) {
|
|
1190
|
+
// when(subject) { is Foo -> ... else -> ... }
|
|
1191
|
+
// Compiles to a series of if/else-if chains
|
|
1192
|
+
const subjectVar = stmt.subject ? "__when_subject__" : null;
|
|
1193
|
+
if (stmt.subject) {
|
|
1194
|
+
const binding = stmt.subject.binding ?? subjectVar;
|
|
1195
|
+
this.w.writeIndentedLine(`const ${binding} = ${this.emitExpr(stmt.subject.expr)};`);
|
|
1196
|
+
}
|
|
1197
|
+
let first = true;
|
|
1198
|
+
for (const branch of stmt.branches) {
|
|
1199
|
+
if (branch.isElse) {
|
|
1200
|
+
this.w.writeIndentedLine(first ? `{` : `} else {`);
|
|
1201
|
+
}
|
|
1202
|
+
else {
|
|
1203
|
+
const cond = branch.conditions.map((c) => this.emitWhenCondition(c, subjectVar ?? "")).join(" || ");
|
|
1204
|
+
this.w.writeIndentedLine(first ? `if (${cond}) {` : `} else if (${cond}) {`);
|
|
1205
|
+
}
|
|
1206
|
+
first = false;
|
|
1207
|
+
this.w.pushIndent();
|
|
1208
|
+
if (branch.body.kind === "Block") {
|
|
1209
|
+
this.emitBlock(branch.body);
|
|
1210
|
+
}
|
|
1211
|
+
else {
|
|
1212
|
+
this.w.writeIndentedLine(`${this.emitExpr(branch.body)};`);
|
|
1213
|
+
}
|
|
1214
|
+
this.w.popIndent();
|
|
1215
|
+
}
|
|
1216
|
+
this.w.writeIndentedLine(`}`);
|
|
1217
|
+
}
|
|
1218
|
+
emitWhenCondition(cond, subject) {
|
|
1219
|
+
switch (cond.kind) {
|
|
1220
|
+
case "WhenIsCondition": {
|
|
1221
|
+
// Qualified name (e.g. UiState.Loading) → use __kind discriminant
|
|
1222
|
+
// Single name (e.g. BibiError) → use instanceof
|
|
1223
|
+
const isQualified = cond.type.kind === "SimpleTypeRef" && cond.type.name.length > 1;
|
|
1224
|
+
const check = isQualified
|
|
1225
|
+
? `(${subject} as any).__kind === "${cond.type.kind === "SimpleTypeRef" ? cond.type.name[cond.type.name.length - 1] : ""}"`
|
|
1226
|
+
: `${subject} instanceof ${this.emitTypeRef(cond.type)}`;
|
|
1227
|
+
return cond.negated ? `!(${check})` : check;
|
|
1228
|
+
}
|
|
1229
|
+
case "WhenInCondition": {
|
|
1230
|
+
const check = `(${this.emitExpr(cond.expr)}).includes(${subject})`;
|
|
1231
|
+
return cond.negated ? `!(${check})` : check;
|
|
1232
|
+
}
|
|
1233
|
+
case "WhenExprCondition":
|
|
1234
|
+
return subject
|
|
1235
|
+
? `${subject} === ${this.emitExpr(cond.expr)}`
|
|
1236
|
+
: this.emitExpr(cond.expr);
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
emitForStmt(stmt) {
|
|
1240
|
+
const iter = this.emitExpr(stmt.iterable);
|
|
1241
|
+
if (typeof stmt.binding === "string") {
|
|
1242
|
+
this.w.writeIndentedLine(`for (const ${stmt.binding} of ${iter}) {`);
|
|
1243
|
+
}
|
|
1244
|
+
else if (stmt.binding.kind === "TupleDestructure") {
|
|
1245
|
+
const names = stmt.binding.names.map((n) => n ?? "_").join(", ");
|
|
1246
|
+
this.w.writeIndentedLine(`for (const [${names}] of ${iter}) {`);
|
|
1247
|
+
}
|
|
1248
|
+
else {
|
|
1249
|
+
const { key, value } = stmt.binding;
|
|
1250
|
+
// Use .entries() for JS Map (Map<K,V>), Object.entries() for plain objects
|
|
1251
|
+
const iterableType = this.typeMap.get(stmt.iterable);
|
|
1252
|
+
const isMap = iterableType?.tag === "class" && iterableType.name === "Map";
|
|
1253
|
+
const entries = isMap ? `${iter}.entries()` : `Object.entries(${iter})`;
|
|
1254
|
+
this.w.writeIndentedLine(`for (const [${key}, ${value}] of ${entries}) {`);
|
|
1255
|
+
}
|
|
1256
|
+
this.w.pushIndent();
|
|
1257
|
+
this.emitBlock(stmt.body);
|
|
1258
|
+
this.w.popIndent();
|
|
1259
|
+
this.w.writeIndentedLine(`}`);
|
|
1260
|
+
}
|
|
1261
|
+
emitTryCatch(stmt) {
|
|
1262
|
+
this.w.writeIndentedLine(`try {`);
|
|
1263
|
+
this.w.pushIndent();
|
|
1264
|
+
this.emitBlock(stmt.body);
|
|
1265
|
+
this.w.popIndent();
|
|
1266
|
+
for (const c of stmt.catches) {
|
|
1267
|
+
this.w.writeIndentedLine(`} catch (${c.name}: unknown) {`);
|
|
1268
|
+
this.w.pushIndent();
|
|
1269
|
+
// Narrow type
|
|
1270
|
+
if (this.opts.emitTypes) {
|
|
1271
|
+
this.w.writeIndentedLine(`if (!(${c.name} instanceof ${this.emitTypeRef(c.type)})) throw ${c.name};`);
|
|
1272
|
+
}
|
|
1273
|
+
this.emitBlock(c.body);
|
|
1274
|
+
this.w.popIndent();
|
|
1275
|
+
}
|
|
1276
|
+
if (stmt.finally) {
|
|
1277
|
+
this.w.writeIndentedLine(`} finally {`);
|
|
1278
|
+
this.w.pushIndent();
|
|
1279
|
+
this.emitBlock(stmt.finally);
|
|
1280
|
+
this.w.popIndent();
|
|
1281
|
+
}
|
|
1282
|
+
this.w.writeIndentedLine(`}`);
|
|
1283
|
+
}
|
|
1284
|
+
// ── Expressions ────────────────────────────────────────────────────────────
|
|
1285
|
+
emitExpr(expr) {
|
|
1286
|
+
switch (expr.kind) {
|
|
1287
|
+
case "IntLiteralExpr": return String(expr.value);
|
|
1288
|
+
case "LongLiteralExpr": return `BigInt(${expr.value})`;
|
|
1289
|
+
case "FloatLiteralExpr":
|
|
1290
|
+
case "DoubleLiteralExpr": return String(expr.value);
|
|
1291
|
+
case "BooleanLiteralExpr": return String(expr.value);
|
|
1292
|
+
case "NullLiteralExpr": return "null";
|
|
1293
|
+
case "StringLiteralExpr":
|
|
1294
|
+
return expr.raw
|
|
1295
|
+
? `\`${expr.value}\``
|
|
1296
|
+
: JSON.stringify(expr.value);
|
|
1297
|
+
case "StringTemplateExpr":
|
|
1298
|
+
return this.emitStringTemplate(expr);
|
|
1299
|
+
case "NameExpr":
|
|
1300
|
+
// Ensure Int/Long companion objects are imported from the runtime
|
|
1301
|
+
if (expr.name === "Int" || expr.name === "Long") {
|
|
1302
|
+
this.runtimeSymbolsNeeded.add(expr.name);
|
|
1303
|
+
}
|
|
1304
|
+
// Unit singleton emits as undefined (matches Kotlin/JVM void semantics)
|
|
1305
|
+
if (expr.name === "Unit")
|
|
1306
|
+
return "undefined";
|
|
1307
|
+
return expr.name;
|
|
1308
|
+
case "ThisExpr": return "this";
|
|
1309
|
+
case "SuperExpr": return "super";
|
|
1310
|
+
case "ParenExpr": return `(${this.emitExpr(expr.expr)})`;
|
|
1311
|
+
case "UnaryExpr":
|
|
1312
|
+
return expr.prefix
|
|
1313
|
+
? `${this.unaryOpStr(expr.op)}${this.emitExpr(expr.operand)}`
|
|
1314
|
+
: `${this.emitExpr(expr.operand)}${this.unaryOpStr(expr.op)}`;
|
|
1315
|
+
case "BinaryExpr": {
|
|
1316
|
+
const opMethod = this.operatorOverloadMap.get(expr);
|
|
1317
|
+
if (opMethod) {
|
|
1318
|
+
const l = this.emitExpr(expr.left);
|
|
1319
|
+
const r = this.emitExpr(expr.right);
|
|
1320
|
+
// `compareTo` overloads: wrap result in a comparison against 0
|
|
1321
|
+
if (opMethod === "compareTo") {
|
|
1322
|
+
const cmpOp = expr.op;
|
|
1323
|
+
return `(${l}.compareTo(${r}) ${cmpOp} 0)`;
|
|
1324
|
+
}
|
|
1325
|
+
// `contains` overloads: `element in collection` → `collection.contains(element)`
|
|
1326
|
+
if (opMethod === "contains") {
|
|
1327
|
+
return `${r}.contains(${l})`;
|
|
1328
|
+
}
|
|
1329
|
+
if (opMethod === "!contains") {
|
|
1330
|
+
return `!${r}.contains(${l})`;
|
|
1331
|
+
}
|
|
1332
|
+
// Generic operator overload: emit as method call `left.method(right)`
|
|
1333
|
+
return `${l}.${opMethod}(${r})`;
|
|
1334
|
+
}
|
|
1335
|
+
// `==` → structural equality, `!=` → negation
|
|
1336
|
+
if (expr.op === "==" || expr.op === "!=") {
|
|
1337
|
+
this.runtimeSymbolsNeeded.add("jalvinEquals");
|
|
1338
|
+
const eq = `jalvinEquals(${this.emitExpr(expr.left)}, ${this.emitExpr(expr.right)})`;
|
|
1339
|
+
return expr.op === "==" ? eq : `!${eq}`;
|
|
1340
|
+
}
|
|
1341
|
+
// `in` / `!in` without a user-defined operator: use JS `in` / array includes
|
|
1342
|
+
if (expr.op === "in") {
|
|
1343
|
+
return `${this.emitExpr(expr.right)}.includes?.(${this.emitExpr(expr.left)}) ?? (${this.emitExpr(expr.left)} in ${this.emitExpr(expr.right)})`;
|
|
1344
|
+
}
|
|
1345
|
+
if (expr.op === "!in") {
|
|
1346
|
+
return `!(${this.emitExpr(expr.right)}.includes?.(${this.emitExpr(expr.left)}) ?? (${this.emitExpr(expr.left)} in ${this.emitExpr(expr.right)}))`;
|
|
1347
|
+
}
|
|
1348
|
+
return `(${this.emitExpr(expr.left)} ${this.binaryOpStr(expr.op)} ${this.emitExpr(expr.right)})`;
|
|
1349
|
+
}
|
|
1350
|
+
case "AssignExpr":
|
|
1351
|
+
return `${this.emitExpr(expr.target)} = ${this.emitExpr(expr.value)}`;
|
|
1352
|
+
case "CompoundAssignExpr":
|
|
1353
|
+
return `${this.emitExpr(expr.target)} ${expr.op} ${this.emitExpr(expr.value)}`;
|
|
1354
|
+
case "IncrDecrExpr":
|
|
1355
|
+
return expr.prefix
|
|
1356
|
+
? `${expr.op}${this.emitExpr(expr.target)}`
|
|
1357
|
+
: `${this.emitExpr(expr.target)}${expr.op}`;
|
|
1358
|
+
case "MemberExpr":
|
|
1359
|
+
return `${this.emitExpr(expr.target)}.${expr.member}`;
|
|
1360
|
+
case "SafeMemberExpr":
|
|
1361
|
+
return `${this.emitExpr(expr.target)}?.${expr.member}`;
|
|
1362
|
+
case "IndexExpr":
|
|
1363
|
+
return `${this.emitExpr(expr.target)}[${this.emitExpr(expr.index)}]`;
|
|
1364
|
+
case "NotNullExpr":
|
|
1365
|
+
// Runtime check
|
|
1366
|
+
this.runtimeSymbolsNeeded.add("notNull");
|
|
1367
|
+
return `notNull(${this.emitExpr(expr.expr)})`;
|
|
1368
|
+
case "SafeCallExpr": {
|
|
1369
|
+
// x?.() — safe invocation of a nullable function value
|
|
1370
|
+
const callee = this.emitExpr(expr.callee);
|
|
1371
|
+
const restArgs = [];
|
|
1372
|
+
for (const a of expr.args) {
|
|
1373
|
+
restArgs.push(a.spread ? `...${this.emitExpr(a.value)}` : this.emitExpr(a.value));
|
|
1374
|
+
}
|
|
1375
|
+
if (expr.trailingLambda)
|
|
1376
|
+
restArgs.push(this.emitLambdaExpr(expr.trailingLambda));
|
|
1377
|
+
return `${callee}?.(${restArgs.join(", ")})`;
|
|
1378
|
+
}
|
|
1379
|
+
case "ElvisExpr": {
|
|
1380
|
+
// left ?? right (null coalescing)
|
|
1381
|
+
return `(${this.emitExpr(expr.left)} ?? ${this.emitExpr(expr.right)})`;
|
|
1382
|
+
}
|
|
1383
|
+
case "CallExpr":
|
|
1384
|
+
return this.emitCallExpr(expr);
|
|
1385
|
+
case "LambdaExpr":
|
|
1386
|
+
return this.emitLambdaExpr(expr);
|
|
1387
|
+
case "IfExpr":
|
|
1388
|
+
return this.emitIfExpr(expr);
|
|
1389
|
+
case "WhenExpr":
|
|
1390
|
+
return this.emitWhenExpr(expr);
|
|
1391
|
+
case "TryCatchExpr":
|
|
1392
|
+
return this.emitTryCatchExpr(expr);
|
|
1393
|
+
case "TypeCheckExpr":
|
|
1394
|
+
return `${expr.negated ? "!(" : ""}${this.emitExpr(expr.expr)} instanceof ${this.emitTypeRef(expr.type)}${expr.negated ? ")" : ""}`;
|
|
1395
|
+
case "TypeCastExpr":
|
|
1396
|
+
// Unsafe cast — emit as `(expr as Type)` which is stripped at runtime
|
|
1397
|
+
return `(${this.emitExpr(expr.expr)} as unknown as ${this.emitTypeRef(expr.type)})`;
|
|
1398
|
+
case "SafeCastExpr": {
|
|
1399
|
+
this.runtimeSymbolsNeeded.add("safeCast");
|
|
1400
|
+
return `safeCast(${this.emitExpr(expr.expr)}, ${this.emitTypeRef(expr.type)})`;
|
|
1401
|
+
}
|
|
1402
|
+
case "RangeExpr": {
|
|
1403
|
+
this.runtimeSymbolsNeeded.add("range");
|
|
1404
|
+
return `range(${this.emitExpr(expr.from)}, ${this.emitExpr(expr.to)}, ${expr.inclusive})`;
|
|
1405
|
+
}
|
|
1406
|
+
case "LaunchExpr": {
|
|
1407
|
+
// fire-and-forget → void IIFE
|
|
1408
|
+
const stmts = this.captureBlock(expr.body);
|
|
1409
|
+
return `(async () => { ${stmts} })()`;
|
|
1410
|
+
}
|
|
1411
|
+
case "AsyncExpr": {
|
|
1412
|
+
// returns Promise<T>
|
|
1413
|
+
const stmts = this.captureBlock(expr.body);
|
|
1414
|
+
return `(async () => { ${stmts} })()`;
|
|
1415
|
+
}
|
|
1416
|
+
case "CollectionLiteralExpr":
|
|
1417
|
+
return this.emitCollectionLiteral(expr);
|
|
1418
|
+
case "ObjectExpr":
|
|
1419
|
+
return this.emitObjectExpr(expr);
|
|
1420
|
+
case "ReturnExpr": {
|
|
1421
|
+
const val = expr.value ? ` ${this.emitExpr(expr.value)}` : "";
|
|
1422
|
+
return `((() => { return${val}; })())`;
|
|
1423
|
+
}
|
|
1424
|
+
case "BreakExpr": return "undefined /* break */";
|
|
1425
|
+
case "ContinueExpr": return "undefined /* continue */";
|
|
1426
|
+
case "JsxElement":
|
|
1427
|
+
return this.emitJsxElement(expr);
|
|
1428
|
+
default:
|
|
1429
|
+
return "undefined";
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
// ── JSX ──────────────────────────────────────────────────────────────────
|
|
1433
|
+
emitJsxElement(expr) {
|
|
1434
|
+
const attrsStr = expr.attrs.length > 0
|
|
1435
|
+
? " " + expr.attrs.map((a) => this.emitJsxAttr(a)).join(" ")
|
|
1436
|
+
: "";
|
|
1437
|
+
if (expr.children.length === 0) {
|
|
1438
|
+
return `<${expr.tag}${attrsStr} />`;
|
|
1439
|
+
}
|
|
1440
|
+
const children = expr.children.map((c) => this.emitJsxChild(c)).join("");
|
|
1441
|
+
return `<${expr.tag}${attrsStr}>${children}</${expr.tag}>`;
|
|
1442
|
+
}
|
|
1443
|
+
emitJsxAttr(attr) {
|
|
1444
|
+
// Map HTML attribute names to React prop names
|
|
1445
|
+
const name = attr.name === "class" ? "className"
|
|
1446
|
+
: attr.name === "for" ? "htmlFor"
|
|
1447
|
+
: attr.name;
|
|
1448
|
+
if (attr.value === null)
|
|
1449
|
+
return name;
|
|
1450
|
+
if (typeof attr.value === "string")
|
|
1451
|
+
return `${name}="${attr.value}"`;
|
|
1452
|
+
return `${name}={${this.emitExpr(attr.value)}}`;
|
|
1453
|
+
}
|
|
1454
|
+
emitJsxChild(child) {
|
|
1455
|
+
switch (child.kind) {
|
|
1456
|
+
case "JsxElement": return this.emitJsxElement(child);
|
|
1457
|
+
case "JsxExprChild": return `{${this.emitExpr(child.expr)}}`;
|
|
1458
|
+
case "JsxTextChild": return child.text;
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
emitStringTemplate(expr) {
|
|
1462
|
+
const inner = expr.parts.map((p) => {
|
|
1463
|
+
if (p.kind === "LiteralPart") {
|
|
1464
|
+
return p.value.replace(/`/g, "\\`").replace(/\\/g, "\\\\");
|
|
1465
|
+
}
|
|
1466
|
+
return `\${${this.emitExpr(p.expr)}}`;
|
|
1467
|
+
}).join("");
|
|
1468
|
+
return `\`${inner}\``;
|
|
1469
|
+
}
|
|
1470
|
+
emitCallExpr(expr) {
|
|
1471
|
+
// Rewrite infix numeric extension calls that JS numbers don't natively have:
|
|
1472
|
+
// `a.downTo(b)` → `downTo(a, b)` `a.until(b)` → `range(a, b, false)`
|
|
1473
|
+
// `range.step(n)` → `step(range, n)`
|
|
1474
|
+
if (expr.callee.kind === "MemberExpr" &&
|
|
1475
|
+
(expr.callee.member === "downTo" || expr.callee.member === "until" || expr.callee.member === "step")) {
|
|
1476
|
+
const obj = this.emitExpr(expr.callee.target);
|
|
1477
|
+
const arg = expr.args.length > 0 ? this.emitExpr(expr.args[0].value) : "0";
|
|
1478
|
+
if (expr.callee.member === "downTo") {
|
|
1479
|
+
this.runtimeSymbolsNeeded.add("downTo");
|
|
1480
|
+
return `downTo(${obj}, ${arg})`;
|
|
1481
|
+
}
|
|
1482
|
+
if (expr.callee.member === "until") {
|
|
1483
|
+
this.runtimeSymbolsNeeded.add("range");
|
|
1484
|
+
return `range(${obj}, ${arg}, false)`;
|
|
1485
|
+
}
|
|
1486
|
+
if (expr.callee.member === "step") {
|
|
1487
|
+
this.runtimeSymbolsNeeded.add("step");
|
|
1488
|
+
return `step(${obj}, ${arg})`;
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
// Rewrite calls on primitive-receiver extension functions.
|
|
1492
|
+
// `str.truncate(30)` where `String.truncate` is a user extension → `__ext_string_truncate(str, 30)`
|
|
1493
|
+
if (expr.callee.kind === "MemberExpr") {
|
|
1494
|
+
const scopeMember = expr.callee.member;
|
|
1495
|
+
// Scope function call rewriting: `x.let { ... }` → `let_(x, ...)`
|
|
1496
|
+
// `x.apply { ... }` → `apply(x, function(this:any) { ... })`
|
|
1497
|
+
if (scopeMember === "let" || scopeMember === "also" || scopeMember === "apply" || scopeMember === "run" || scopeMember === "takeIf" || scopeMember === "takeUnless") {
|
|
1498
|
+
const receiver = this.emitExpr(expr.callee.target);
|
|
1499
|
+
const lambda = expr.trailingLambda ??
|
|
1500
|
+
(expr.args.length === 1 && expr.args[0].value.kind === "LambdaExpr"
|
|
1501
|
+
? expr.args[0].value
|
|
1502
|
+
: null);
|
|
1503
|
+
if (lambda) {
|
|
1504
|
+
if (scopeMember === "let") {
|
|
1505
|
+
this.runtimeSymbolsNeeded.add("let_");
|
|
1506
|
+
return `let_(${receiver}, ${this.emitLambdaExpr(lambda)})`;
|
|
1507
|
+
}
|
|
1508
|
+
if (scopeMember === "also") {
|
|
1509
|
+
this.runtimeSymbolsNeeded.add("also");
|
|
1510
|
+
return `also(${receiver}, ${this.emitLambdaExpr(lambda)})`;
|
|
1511
|
+
}
|
|
1512
|
+
if (scopeMember === "apply") {
|
|
1513
|
+
this.runtimeSymbolsNeeded.add("apply");
|
|
1514
|
+
const body = this.captureBlockStatements(lambda.body);
|
|
1515
|
+
return `apply(${receiver}, function(this: any) { ${body} })`;
|
|
1516
|
+
}
|
|
1517
|
+
if (scopeMember === "run") {
|
|
1518
|
+
this.runtimeSymbolsNeeded.add("run_");
|
|
1519
|
+
const body = this.captureBlockStatements(lambda.body);
|
|
1520
|
+
return `run_(${receiver}, function(this: any) { ${body} })`;
|
|
1521
|
+
}
|
|
1522
|
+
if (scopeMember === "takeIf") {
|
|
1523
|
+
this.runtimeSymbolsNeeded.add("takeIf");
|
|
1524
|
+
return `takeIf(${receiver}, ${this.emitLambdaExpr(lambda)})`;
|
|
1525
|
+
}
|
|
1526
|
+
if (scopeMember === "takeUnless") {
|
|
1527
|
+
this.runtimeSymbolsNeeded.add("takeUnless");
|
|
1528
|
+
return `takeUnless(${receiver}, ${this.emitLambdaExpr(lambda)})`;
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
// `with(x) { ... }` is a free function handled via seedBuiltins; but rewrite if emitted as member
|
|
1533
|
+
}
|
|
1534
|
+
// Rewrite calls on primitive-receiver extension functions.
|
|
1535
|
+
// `str.truncate(30)` where `String.truncate` is a user extension → `__ext_string_truncate(str, 30)`
|
|
1536
|
+
if (expr.callee.kind === "MemberExpr") {
|
|
1537
|
+
const receiverType = this.typeMap.get(expr.callee.target);
|
|
1538
|
+
const memberName = expr.callee.member;
|
|
1539
|
+
if (receiverType) {
|
|
1540
|
+
const tsTypeName = this.jTypeToReceiverName(receiverType);
|
|
1541
|
+
if (tsTypeName) {
|
|
1542
|
+
const extMap = this.primitiveExtensions.get(tsTypeName);
|
|
1543
|
+
if (extMap?.has(memberName)) {
|
|
1544
|
+
const fnName = extMap.get(memberName);
|
|
1545
|
+
const receiver = this.emitExpr(expr.callee.target);
|
|
1546
|
+
const restArgs = expr.args.map((a) => a.spread ? `...${this.emitExpr(a.value)}` : this.emitExpr(a.value));
|
|
1547
|
+
if (expr.trailingLambda)
|
|
1548
|
+
restArgs.push(this.emitLambdaExpr(expr.trailingLambda));
|
|
1549
|
+
return `${fnName}(${[receiver, ...restArgs].join(", ")})`;
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
const callee = this.emitExpr(expr.callee);
|
|
1555
|
+
const typeArgs = expr.typeArgs.length > 0
|
|
1556
|
+
? `<${expr.typeArgs.map((t) => t.star ? "*" : this.emitTypeRef(t.type)).join(", ")}>`
|
|
1557
|
+
: "";
|
|
1558
|
+
// Rewrite top-level `with(obj) { ... }` → `with_(obj, function(this: any) { ... })`
|
|
1559
|
+
if (expr.callee.kind === "NameExpr" && expr.callee.name === "with" &&
|
|
1560
|
+
(expr.trailingLambda || (expr.args.length === 2 && expr.args[1].value.kind === "LambdaExpr"))) {
|
|
1561
|
+
const obj = expr.args.length > 0 ? this.emitExpr(expr.args[0].value) : "undefined";
|
|
1562
|
+
const lambda = expr.trailingLambda ??
|
|
1563
|
+
expr.args[1].value;
|
|
1564
|
+
this.runtimeSymbolsNeeded.add("with_");
|
|
1565
|
+
const body = this.captureBlockStatements(lambda.body);
|
|
1566
|
+
return `with_(${obj}, function(this: any) { ${body} })`;
|
|
1567
|
+
}
|
|
1568
|
+
// Rewrite top-level `run { ... }` (no receiver) — just call the lambda
|
|
1569
|
+
if (expr.callee.kind === "NameExpr" && expr.callee.name === "run" &&
|
|
1570
|
+
expr.args.length === 0 && expr.trailingLambda) {
|
|
1571
|
+
return `(${this.emitLambdaExpr(expr.trailingLambda)})()`;
|
|
1572
|
+
}
|
|
1573
|
+
// Handle named arguments by reordering them to match positional parameters
|
|
1574
|
+
const calleeType = this.typeMap.get(expr.callee);
|
|
1575
|
+
// Compose-style component call: Column(modifier = ...) { ... } → Column({ modifier: ... }, [children])
|
|
1576
|
+
// Also catches star-imported @jalvin/ui primitives (Row, Button, etc.) whose type is T_UNKNOWN
|
|
1577
|
+
if (expr.callee.kind === "NameExpr" && (this.componentNames.has(expr.callee.name) ||
|
|
1578
|
+
(this.hasUiStarImport && (!calleeType || calleeType.tag === "unknown") && /^[A-Z]/.test(expr.callee.name)))) {
|
|
1579
|
+
return this.emitComposeCallAsDom(expr);
|
|
1580
|
+
}
|
|
1581
|
+
let finalArgs = [];
|
|
1582
|
+
if (calleeType && calleeType.tag === "func" && calleeType.paramNames) {
|
|
1583
|
+
const paramNames = calleeType.paramNames;
|
|
1584
|
+
const argsByName = new Map();
|
|
1585
|
+
const positionalArgs = [];
|
|
1586
|
+
for (const arg of expr.args) {
|
|
1587
|
+
if (arg.name) {
|
|
1588
|
+
argsByName.set(arg.name, arg.spread ? `...${this.emitExpr(arg.value)}` : this.emitExpr(arg.value));
|
|
1589
|
+
}
|
|
1590
|
+
else {
|
|
1591
|
+
positionalArgs.push(arg.spread ? `...${this.emitExpr(arg.value)}` : this.emitExpr(arg.value));
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
// Reconstruct args based on paramNames
|
|
1595
|
+
for (let i = 0; i < paramNames.length; i++) {
|
|
1596
|
+
const name = paramNames[i];
|
|
1597
|
+
if (argsByName.has(name)) {
|
|
1598
|
+
finalArgs.push(argsByName.get(name));
|
|
1599
|
+
}
|
|
1600
|
+
else if (i < positionalArgs.length) {
|
|
1601
|
+
finalArgs.push(positionalArgs[i]);
|
|
1602
|
+
}
|
|
1603
|
+
else {
|
|
1604
|
+
// Missing argument - JS default value logic will handle it if we pass undefined
|
|
1605
|
+
finalArgs.push("undefined");
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
// Handle trailing positional args if any
|
|
1609
|
+
if (positionalArgs.length > paramNames.length) {
|
|
1610
|
+
finalArgs.push(...positionalArgs.slice(paramNames.length));
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
else {
|
|
1614
|
+
// Fallback for types we don't know param names for (like constructors or any)
|
|
1615
|
+
finalArgs = expr.args.map((a) => {
|
|
1616
|
+
return a.spread ? `...${this.emitExpr(a.value)}` : this.emitExpr(a.value);
|
|
1617
|
+
});
|
|
1618
|
+
}
|
|
1619
|
+
if (expr.trailingLambda) {
|
|
1620
|
+
finalArgs.push(this.emitLambdaExpr(expr.trailingLambda));
|
|
1621
|
+
}
|
|
1622
|
+
// If the callee is a class instance (not a constructor / function), emit `.invoke(args)`
|
|
1623
|
+
if (calleeType &&
|
|
1624
|
+
calleeType.tag === "class" &&
|
|
1625
|
+
calleeType.decl &&
|
|
1626
|
+
this.classHasInvokeOperator(calleeType.decl)) {
|
|
1627
|
+
return `${callee}.invoke(${finalArgs.join(", ")})`;
|
|
1628
|
+
}
|
|
1629
|
+
// Constructor calls: class names start with uppercase by convention
|
|
1630
|
+
if (this.isConstructorCall(expr.callee)) {
|
|
1631
|
+
return `new ${callee}${typeArgs}(${finalArgs.join(", ")})`;
|
|
1632
|
+
}
|
|
1633
|
+
return `${callee}${typeArgs}(${finalArgs.join(", ")})`;
|
|
1634
|
+
}
|
|
1635
|
+
/**
|
|
1636
|
+
* For each non-scoped local import (e.g. `import src.views.MoveCounterView.MoveCounter`),
|
|
1637
|
+
* attempt to load and parse the corresponding .jalvin source file. If the named symbol is
|
|
1638
|
+
* a `component fun`, register it in `componentNames` and record its param names so that
|
|
1639
|
+
* call sites can emit the correct props-object invocation.
|
|
1640
|
+
*/
|
|
1641
|
+
resolveLocalComponentImports(program, sourceRoot) {
|
|
1642
|
+
for (const imp of program.imports) {
|
|
1643
|
+
// Skip scoped packages (@org/pkg) and star imports
|
|
1644
|
+
if (imp.star || imp.path[0]?.startsWith("@"))
|
|
1645
|
+
continue;
|
|
1646
|
+
// Need at least 2 path segments: the last is the symbol, the rest form the file path
|
|
1647
|
+
if (imp.path.length < 2)
|
|
1648
|
+
continue;
|
|
1649
|
+
const rawSymbolName = imp.path[imp.path.length - 1];
|
|
1650
|
+
const symbolName = imp.alias ?? rawSymbolName;
|
|
1651
|
+
const precedingParts = imp.path.slice(0, -1);
|
|
1652
|
+
const candidatePath = nodePath.join(sourceRoot, precedingParts.join("/") + ".jalvin");
|
|
1653
|
+
if (!fs.existsSync(candidatePath))
|
|
1654
|
+
continue;
|
|
1655
|
+
try {
|
|
1656
|
+
const source = fs.readFileSync(candidatePath, "utf8");
|
|
1657
|
+
const diag = new diagnostics_js_1.DiagnosticBag();
|
|
1658
|
+
const tokens = (0, lexer_js_1.lex)(source, candidatePath, diag);
|
|
1659
|
+
if (diag.hasErrors)
|
|
1660
|
+
continue;
|
|
1661
|
+
const ast = (0, parser_js_1.parse)(tokens, candidatePath, diag, source);
|
|
1662
|
+
if (diag.hasErrors)
|
|
1663
|
+
continue;
|
|
1664
|
+
// Look for a top-level ComponentDecl matching the imported symbol name
|
|
1665
|
+
const compDecl = ast.declarations.find((d) => d.kind === "ComponentDecl" && d.name === rawSymbolName);
|
|
1666
|
+
if (compDecl) {
|
|
1667
|
+
this.componentNames.add(symbolName);
|
|
1668
|
+
this.componentParamNames.set(symbolName, compDecl.params.map((p) => p.name));
|
|
1669
|
+
this.hasComponents = true;
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
catch {
|
|
1673
|
+
// Silently skip if the file cannot be read or parsed
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
/** Returns true if a call expression's callee looks like a class constructor.
|
|
1678
|
+
* In Jalvin, class names always start with an uppercase letter. */
|
|
1679
|
+
isConstructorCall(calleeExpr) {
|
|
1680
|
+
if (calleeExpr.kind === "NameExpr") {
|
|
1681
|
+
// Names in componentNames are factory functions (UI primitives, components), not constructors
|
|
1682
|
+
if (this.componentNames.has(calleeExpr.name))
|
|
1683
|
+
return false;
|
|
1684
|
+
// Class names start with uppercase; function names start with lowercase
|
|
1685
|
+
return /^[A-Z]/.test(calleeExpr.name);
|
|
1686
|
+
}
|
|
1687
|
+
if (calleeExpr.kind === "MemberExpr") {
|
|
1688
|
+
// e.g. UiState.Success(data), Discount.Percentage(0.1)
|
|
1689
|
+
return /^[A-Z]/.test(calleeExpr.member);
|
|
1690
|
+
}
|
|
1691
|
+
return false;
|
|
1692
|
+
}
|
|
1693
|
+
// ── Compose-style component call → DOM ──────────────────────────────────────
|
|
1694
|
+
/** Emit `Column(modifier = ...) { ... }` as `Column({ modifier: ... }, [children])` */
|
|
1695
|
+
emitComposeCallAsDom(expr) {
|
|
1696
|
+
const tag = expr.callee.name;
|
|
1697
|
+
const calleeType = this.typeMap.get(expr.callee);
|
|
1698
|
+
// Param names from type checker (for same-file components) or resolved from imported files
|
|
1699
|
+
const paramNames = (calleeType?.tag === "func" ? calleeType.paramNames : undefined) ??
|
|
1700
|
+
this.componentParamNames.get(tag);
|
|
1701
|
+
// Build props object from named/positional args
|
|
1702
|
+
const props = [];
|
|
1703
|
+
for (let i = 0; i < expr.args.length; i++) {
|
|
1704
|
+
const arg = expr.args[i];
|
|
1705
|
+
// Prop name: explicit named arg > known param name > NameExpr variable name (shorthand)
|
|
1706
|
+
const propName = arg.name ??
|
|
1707
|
+
paramNames?.[i] ??
|
|
1708
|
+
(arg.value.kind === "NameExpr" ? arg.value.name : undefined);
|
|
1709
|
+
if (!propName)
|
|
1710
|
+
continue;
|
|
1711
|
+
const val = this.emitExpr(arg.value);
|
|
1712
|
+
// Use JS shorthand `{ key }` when key and value are the same identifier
|
|
1713
|
+
props.push(propName === val ? propName : `${propName}: ${val}`);
|
|
1714
|
+
}
|
|
1715
|
+
const propsStr = props.length > 0 ? `{ ${props.join(", ")} }` : "{}";
|
|
1716
|
+
if (expr.trailingLambda) {
|
|
1717
|
+
const children = this.emitLambdaBodyAsDomChildren(expr.trailingLambda);
|
|
1718
|
+
return children ? `${tag}(${propsStr}, [${children}])` : `${tag}(${propsStr})`;
|
|
1719
|
+
}
|
|
1720
|
+
return `${tag}(${propsStr})`;
|
|
1721
|
+
}
|
|
1722
|
+
/** Collect each expression statement in a trailing-lambda body as DOM children. */
|
|
1723
|
+
emitLambdaBodyAsDomChildren(lambda) {
|
|
1724
|
+
return lambda.body
|
|
1725
|
+
.filter((stmt) => stmt.kind === "ExprStmt")
|
|
1726
|
+
.map((stmt) => this.emitExpr(stmt.expr))
|
|
1727
|
+
.join(", ");
|
|
1728
|
+
}
|
|
1729
|
+
emitLambdaExpr(expr) {
|
|
1730
|
+
// If no explicit params, emit `it` as the single implicit parameter
|
|
1731
|
+
// (Convention for single-argument lambdas)
|
|
1732
|
+
const params = expr.params.length === 0
|
|
1733
|
+
? "it"
|
|
1734
|
+
: expr.params.map((p) => p.name ?? "_").join(", ");
|
|
1735
|
+
const body = expr.body;
|
|
1736
|
+
if (body.length === 1 && body[0].kind === "ExprStmt") {
|
|
1737
|
+
return `(${params}) => ${this.emitExpr(body[0].expr)}`;
|
|
1738
|
+
}
|
|
1739
|
+
return `(${params}) => { ${body.map((s) => this.captureStmt(s)).join(" ")} }`;
|
|
1740
|
+
}
|
|
1741
|
+
emitIfExpr(expr) {
|
|
1742
|
+
const cond = this.emitExpr(expr.condition);
|
|
1743
|
+
const thenStr = expr.then.kind === "Block"
|
|
1744
|
+
? `(() => { ${this.captureBlockStatements(expr.then.statements)} })()`
|
|
1745
|
+
: this.emitExpr(expr.then);
|
|
1746
|
+
const elseExpr = expr.else;
|
|
1747
|
+
const elseStr = elseExpr.kind === "Block"
|
|
1748
|
+
? `(() => { ${this.captureBlockStatements(elseExpr.statements)} })()`
|
|
1749
|
+
: elseExpr.kind === "IfExpr"
|
|
1750
|
+
? this.emitIfExpr(elseExpr)
|
|
1751
|
+
: this.emitExpr(elseExpr);
|
|
1752
|
+
return `(${cond} ? ${thenStr} : ${elseStr})`;
|
|
1753
|
+
}
|
|
1754
|
+
emitWhenExpr(expr) {
|
|
1755
|
+
// Compile as an IIFE with if/else chain
|
|
1756
|
+
const parts = [];
|
|
1757
|
+
const subject = expr.subject ? `const __s = ${this.emitExpr(expr.subject.expr)};` : "";
|
|
1758
|
+
const subjectRef = expr.subject ? (expr.subject.binding ?? "__s") : "";
|
|
1759
|
+
for (const branch of expr.branches) {
|
|
1760
|
+
if (branch.isElse) {
|
|
1761
|
+
const body = branch.body.kind === "Block"
|
|
1762
|
+
? this.captureBlock(branch.body)
|
|
1763
|
+
: `return ${this.emitExpr(branch.body)};`;
|
|
1764
|
+
parts.push(`{ ${body} }`);
|
|
1765
|
+
}
|
|
1766
|
+
else {
|
|
1767
|
+
const cond = branch.conditions.map((c) => this.emitWhenCondition(c, subjectRef)).join(" || ");
|
|
1768
|
+
const body = branch.body.kind === "Block"
|
|
1769
|
+
? this.captureBlock(branch.body)
|
|
1770
|
+
: `return ${this.emitExpr(branch.body)};`;
|
|
1771
|
+
parts.push(`if (${cond}) { ${body} }`);
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
return `(() => { ${subject} ${parts.join(" else ")} })()`;
|
|
1775
|
+
}
|
|
1776
|
+
emitTryCatchExpr(expr) {
|
|
1777
|
+
const body = this.captureBlock(expr.body);
|
|
1778
|
+
const catches = expr.catches.map((c) => {
|
|
1779
|
+
const cb = this.captureBlock(c.body);
|
|
1780
|
+
return `catch (${c.name}) { ${cb} }`;
|
|
1781
|
+
}).join(" ");
|
|
1782
|
+
const fin = expr.finally ? `finally { ${this.captureBlock(expr.finally)} }` : "";
|
|
1783
|
+
return `(() => { try { ${body} } ${catches} ${fin} })()`;
|
|
1784
|
+
}
|
|
1785
|
+
emitCollectionLiteral(expr) {
|
|
1786
|
+
if (expr.collectionKind === "map") {
|
|
1787
|
+
const entries = expr.elements.map((e) => {
|
|
1788
|
+
if ("kind" in e && e.kind === "MapEntry") {
|
|
1789
|
+
return `[${this.emitExpr(e.key)}, ${this.emitExpr(e.value)}]`;
|
|
1790
|
+
}
|
|
1791
|
+
return "null";
|
|
1792
|
+
});
|
|
1793
|
+
return `new Map([${entries.join(", ")}])`;
|
|
1794
|
+
}
|
|
1795
|
+
if (expr.collectionKind === "set") {
|
|
1796
|
+
const items = expr.elements.map((e) => this.emitExpr(e));
|
|
1797
|
+
return `new Set([${items.join(", ")}])`;
|
|
1798
|
+
}
|
|
1799
|
+
const items = expr.elements.map((e) => this.emitExpr(e));
|
|
1800
|
+
return `[${items.join(", ")}]`;
|
|
1801
|
+
}
|
|
1802
|
+
emitObjectExpr(expr) {
|
|
1803
|
+
const superType = expr.superTypes[0];
|
|
1804
|
+
const ext = superType ? ` extends ${this.emitTypeRef(superType.type)}` : "";
|
|
1805
|
+
const members = expr.body.members.map((m) => this.captureClassMember(m)).join(" ");
|
|
1806
|
+
return `(new (class${ext} { ${members} })())`;
|
|
1807
|
+
}
|
|
1808
|
+
// ── capture helpers (emit to temp string) ─────────────────────────────────
|
|
1809
|
+
captureBlock(block) {
|
|
1810
|
+
return block.statements.map((s) => this.captureStmt(s)).join(" ");
|
|
1811
|
+
}
|
|
1812
|
+
captureBlockStatements(stmts) {
|
|
1813
|
+
return stmts.map((s) => this.captureStmt(s)).join(" ");
|
|
1814
|
+
}
|
|
1815
|
+
captureStmt(stmt) {
|
|
1816
|
+
const saved = this.w;
|
|
1817
|
+
const tmp = new Writer();
|
|
1818
|
+
this.w = tmp;
|
|
1819
|
+
this.emitStmt(stmt);
|
|
1820
|
+
this.w = saved;
|
|
1821
|
+
return tmp.output.trim();
|
|
1822
|
+
}
|
|
1823
|
+
captureClassMember(member) {
|
|
1824
|
+
const saved = this.w;
|
|
1825
|
+
const tmp = new Writer();
|
|
1826
|
+
this.w = tmp;
|
|
1827
|
+
this.emitClassMember(member);
|
|
1828
|
+
this.w = saved;
|
|
1829
|
+
return tmp.output.trim();
|
|
1830
|
+
}
|
|
1831
|
+
// ── Type reference emission ────────────────────────────────────────────────
|
|
1832
|
+
emitTypeRef(ref) {
|
|
1833
|
+
switch (ref.kind) {
|
|
1834
|
+
case "SimpleTypeRef": {
|
|
1835
|
+
const name = ref.name.join(".");
|
|
1836
|
+
return PRIMITIVE_TYPE_MAP[name] ?? name;
|
|
1837
|
+
}
|
|
1838
|
+
case "NullableTypeRef":
|
|
1839
|
+
return `${this.emitTypeRef(ref.base)} | null | undefined`;
|
|
1840
|
+
case "GenericTypeRef": {
|
|
1841
|
+
const base = ref.base.name.join(".");
|
|
1842
|
+
const mapped = GENERIC_TYPE_MAP[base] ?? base;
|
|
1843
|
+
const args = ref.args.map((a) => a.star ? "any" : a.type ? this.emitTypeRef(a.type) : "unknown");
|
|
1844
|
+
return `${mapped}<${args.join(", ")}>`;
|
|
1845
|
+
}
|
|
1846
|
+
case "FunctionTypeRef": {
|
|
1847
|
+
const params = ref.params.map((p, i) => `p${i}: ${this.emitTypeRef(p)}`).join(", ");
|
|
1848
|
+
const ret = this.emitTypeRef(ref.returnType);
|
|
1849
|
+
return `(${params}) => ${ret}`;
|
|
1850
|
+
}
|
|
1851
|
+
case "StarProjection":
|
|
1852
|
+
return "any";
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
// ── Utility helpers ────────────────────────────────────────────────────────
|
|
1856
|
+
emitTypeParamsStr(params) {
|
|
1857
|
+
if (params.length === 0)
|
|
1858
|
+
return "";
|
|
1859
|
+
const ps = params.map((p) => {
|
|
1860
|
+
const bound = p.upperBound ? ` extends ${this.emitTypeRef(p.upperBound)}` : "";
|
|
1861
|
+
return `${p.name}${bound}`;
|
|
1862
|
+
});
|
|
1863
|
+
return `<${ps.join(", ")}>`;
|
|
1864
|
+
}
|
|
1865
|
+
emitParamsStr(params) {
|
|
1866
|
+
return params.map((p) => {
|
|
1867
|
+
const spread = p.vararg ? "..." : "";
|
|
1868
|
+
// vararg params are rest params in TS: `...name: T[]`
|
|
1869
|
+
const type = this.opts.emitTypes
|
|
1870
|
+
? `: ${this.emitTypeRef(p.type)}${p.vararg ? "[]" : ""}`
|
|
1871
|
+
: "";
|
|
1872
|
+
const def = p.defaultValue ? ` = ${this.emitExpr(p.defaultValue)}` : "";
|
|
1873
|
+
return `${spread}${p.name}${type}${def}`;
|
|
1874
|
+
}).join(", ");
|
|
1875
|
+
}
|
|
1876
|
+
emitComponentPropsStr(params) {
|
|
1877
|
+
if (params.length === 0)
|
|
1878
|
+
return "";
|
|
1879
|
+
return params.map((p) => {
|
|
1880
|
+
const type = this.opts.emitTypes ? `: ${this.emitTypeRef(p.type)}` : "";
|
|
1881
|
+
return `${p.name}${type}`;
|
|
1882
|
+
}).join(", ");
|
|
1883
|
+
}
|
|
1884
|
+
emitSuperTypesStr(superTypes) {
|
|
1885
|
+
if (superTypes.length === 0)
|
|
1886
|
+
return "";
|
|
1887
|
+
const parts = [];
|
|
1888
|
+
let first = true;
|
|
1889
|
+
for (const s of superTypes) {
|
|
1890
|
+
// TypeScript does NOT allow constructor arguments in the `extends` clause.
|
|
1891
|
+
// Delegation args are passed via `super(args)` inside the constructor.
|
|
1892
|
+
if (first) {
|
|
1893
|
+
parts.push(` extends ${this.emitTypeRef(s.type)}`);
|
|
1894
|
+
first = false;
|
|
1895
|
+
}
|
|
1896
|
+
else {
|
|
1897
|
+
parts.push(` implements ${this.emitTypeRef(s.type)}`);
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
return parts.join("");
|
|
1901
|
+
}
|
|
1902
|
+
exportPrefix(mods) {
|
|
1903
|
+
if (mods.visibility === "private" || mods.visibility === "internal")
|
|
1904
|
+
return "";
|
|
1905
|
+
return "export ";
|
|
1906
|
+
}
|
|
1907
|
+
visibilityPrefix(mods) {
|
|
1908
|
+
switch (mods.visibility) {
|
|
1909
|
+
case "private": return "private ";
|
|
1910
|
+
case "protected": return "protected ";
|
|
1911
|
+
case "internal": return "/* internal */ ";
|
|
1912
|
+
default: return "";
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
unaryOpStr(op) {
|
|
1916
|
+
if (op === "not")
|
|
1917
|
+
return "!";
|
|
1918
|
+
return op;
|
|
1919
|
+
}
|
|
1920
|
+
binaryOpStr(op) {
|
|
1921
|
+
switch (op) {
|
|
1922
|
+
case "and": return "&&";
|
|
1923
|
+
case "or": return "||";
|
|
1924
|
+
case "xor": return "^";
|
|
1925
|
+
case "shl": return "<<";
|
|
1926
|
+
case "shr": return ">>";
|
|
1927
|
+
case "ushr": return ">>>";
|
|
1928
|
+
// === / !== are JS reference equality (Jalvin's triple-equals)
|
|
1929
|
+
case "===": return "===";
|
|
1930
|
+
case "!==": return "!==";
|
|
1931
|
+
case "..": return "/* .. */ +"; // handled by range()
|
|
1932
|
+
case "..<": return "/* ..< */ +";
|
|
1933
|
+
default: return op;
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
// ── AST name walkers (for wildcard import resolution) ─────────────────────
|
|
1937
|
+
/**
|
|
1938
|
+
* Walk the entire program AST and collect all identifier names that are
|
|
1939
|
+
* REFERENCED (i.e., used) in the code: NameExpr values and the first name
|
|
1940
|
+
* component of SimpleTypeRef / GenericTypeRef nodes.
|
|
1941
|
+
*
|
|
1942
|
+
* Jalvin primitive type names (String, Boolean, …) that map to TS primitives
|
|
1943
|
+
* are excluded from TypeRef collection since they never need an import.
|
|
1944
|
+
*/
|
|
1945
|
+
gatherReferencedNames(program) {
|
|
1946
|
+
const names = new Set();
|
|
1947
|
+
const visited = new WeakSet();
|
|
1948
|
+
const walk = (val) => {
|
|
1949
|
+
if (!val || typeof val !== "object")
|
|
1950
|
+
return;
|
|
1951
|
+
if (Array.isArray(val)) {
|
|
1952
|
+
for (const item of val)
|
|
1953
|
+
walk(item);
|
|
1954
|
+
return;
|
|
1955
|
+
}
|
|
1956
|
+
const obj = val;
|
|
1957
|
+
if (visited.has(obj))
|
|
1958
|
+
return;
|
|
1959
|
+
visited.add(obj);
|
|
1960
|
+
const kind = obj["kind"];
|
|
1961
|
+
if (kind === "NameExpr") {
|
|
1962
|
+
const name = obj["name"];
|
|
1963
|
+
if (typeof name === "string")
|
|
1964
|
+
names.add(name);
|
|
1965
|
+
return; // leaf node
|
|
1966
|
+
}
|
|
1967
|
+
if (kind === "SimpleTypeRef") {
|
|
1968
|
+
const nameArr = obj["name"];
|
|
1969
|
+
if (Array.isArray(nameArr) && nameArr.length > 0) {
|
|
1970
|
+
const first = nameArr[0];
|
|
1971
|
+
// Skip Jalvin primitive type names — they're erased to TS primitives.
|
|
1972
|
+
if (!(first in PRIMITIVE_TYPE_MAP))
|
|
1973
|
+
names.add(first);
|
|
1974
|
+
}
|
|
1975
|
+
return;
|
|
1976
|
+
}
|
|
1977
|
+
if (kind === "GenericTypeRef") {
|
|
1978
|
+
const base = obj["base"];
|
|
1979
|
+
const nameArr = base?.["name"];
|
|
1980
|
+
if (Array.isArray(nameArr) && nameArr.length > 0) {
|
|
1981
|
+
const first = nameArr[0];
|
|
1982
|
+
if (!(first in PRIMITIVE_TYPE_MAP))
|
|
1983
|
+
names.add(first);
|
|
1984
|
+
}
|
|
1985
|
+
// Fall through to recurse into type arguments
|
|
1986
|
+
}
|
|
1987
|
+
for (const propVal of Object.values(obj)) {
|
|
1988
|
+
walk(propVal);
|
|
1989
|
+
}
|
|
1990
|
+
};
|
|
1991
|
+
for (const decl of program.declarations)
|
|
1992
|
+
walk(decl);
|
|
1993
|
+
return names;
|
|
1994
|
+
}
|
|
1995
|
+
/**
|
|
1996
|
+
* Walk the entire program AST and collect all names that are LOCALLY DEFINED:
|
|
1997
|
+
* top-level declaration names, non-star import aliases, function/lambda
|
|
1998
|
+
* parameters, local val/var bindings, for-loop variables, catch-clause names,
|
|
1999
|
+
* when-subject bindings, and type parameter names.
|
|
2000
|
+
*/
|
|
2001
|
+
gatherAllLocalBindings(program) {
|
|
2002
|
+
const names = new Set();
|
|
2003
|
+
const visited = new WeakSet();
|
|
2004
|
+
// Top-level declaration names
|
|
2005
|
+
for (const decl of program.declarations) {
|
|
2006
|
+
const d = decl;
|
|
2007
|
+
if (typeof d.name === "string" && d.name)
|
|
2008
|
+
names.add(d.name);
|
|
2009
|
+
}
|
|
2010
|
+
// Non-star import aliases
|
|
2011
|
+
for (const imp of program.imports) {
|
|
2012
|
+
if (!imp.star) {
|
|
2013
|
+
const name = imp.alias ?? imp.path[imp.path.length - 1];
|
|
2014
|
+
if (name)
|
|
2015
|
+
names.add(name);
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
const walk = (val) => {
|
|
2019
|
+
if (!val || typeof val !== "object")
|
|
2020
|
+
return;
|
|
2021
|
+
if (Array.isArray(val)) {
|
|
2022
|
+
for (const item of val)
|
|
2023
|
+
walk(item);
|
|
2024
|
+
return;
|
|
2025
|
+
}
|
|
2026
|
+
const obj = val;
|
|
2027
|
+
if (visited.has(obj))
|
|
2028
|
+
return;
|
|
2029
|
+
visited.add(obj);
|
|
2030
|
+
const kind = obj["kind"];
|
|
2031
|
+
// Collect bound names at their declaration sites
|
|
2032
|
+
if (kind === "PropertyDecl" || kind === "DestructuringDecl") {
|
|
2033
|
+
if (typeof obj["name"] === "string")
|
|
2034
|
+
names.add(obj["name"]);
|
|
2035
|
+
const nms = obj["names"];
|
|
2036
|
+
if (Array.isArray(nms)) {
|
|
2037
|
+
for (const n of nms) {
|
|
2038
|
+
if (typeof n === "string")
|
|
2039
|
+
names.add(n);
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
if (kind === "FunDecl" || kind === "ExtensionFunDecl" ||
|
|
2044
|
+
kind === "ComponentDecl" || kind === "ClassDecl" ||
|
|
2045
|
+
kind === "DataClassDecl" || kind === "SealedClassDecl" ||
|
|
2046
|
+
kind === "EnumClassDecl" || kind === "InterfaceDecl" ||
|
|
2047
|
+
kind === "ObjectDecl" || kind === "TypeAliasDecl") {
|
|
2048
|
+
if (typeof obj["name"] === "string" && obj["name"])
|
|
2049
|
+
names.add(obj["name"]);
|
|
2050
|
+
// Type parameters
|
|
2051
|
+
const typeParams = obj["typeParams"];
|
|
2052
|
+
if (Array.isArray(typeParams)) {
|
|
2053
|
+
for (const tp of typeParams) {
|
|
2054
|
+
if (tp.name)
|
|
2055
|
+
names.add(tp.name);
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
// Function/method parameters
|
|
2059
|
+
const params = obj["params"];
|
|
2060
|
+
if (Array.isArray(params)) {
|
|
2061
|
+
for (const p of params) {
|
|
2062
|
+
if (p.name)
|
|
2063
|
+
names.add(p.name);
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
if (kind === "LambdaExpr") {
|
|
2068
|
+
const params = obj["params"];
|
|
2069
|
+
if (Array.isArray(params)) {
|
|
2070
|
+
for (const p of params) {
|
|
2071
|
+
if (p.name)
|
|
2072
|
+
names.add(p.name);
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
if (kind === "ForStmt") {
|
|
2077
|
+
const binding = obj["binding"];
|
|
2078
|
+
if (typeof binding === "string") {
|
|
2079
|
+
names.add(binding);
|
|
2080
|
+
}
|
|
2081
|
+
else if (binding && typeof binding === "object") {
|
|
2082
|
+
const b = binding;
|
|
2083
|
+
// TupleDestructure or MapDestructure
|
|
2084
|
+
if (Array.isArray(b["names"])) {
|
|
2085
|
+
for (const n of b["names"]) {
|
|
2086
|
+
if (typeof n === "string")
|
|
2087
|
+
names.add(n);
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
if (typeof b["key"] === "string")
|
|
2091
|
+
names.add(b["key"]);
|
|
2092
|
+
if (typeof b["value"] === "string")
|
|
2093
|
+
names.add(b["value"]);
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
if (kind === "TryCatchStmt") {
|
|
2097
|
+
const catches = obj["catches"];
|
|
2098
|
+
if (Array.isArray(catches)) {
|
|
2099
|
+
for (const c of catches) {
|
|
2100
|
+
if (c.name)
|
|
2101
|
+
names.add(c.name);
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
if (kind === "WhenStmt" || kind === "WhenExpr") {
|
|
2106
|
+
const subject = obj["subject"];
|
|
2107
|
+
if (typeof subject?.["binding"] === "string")
|
|
2108
|
+
names.add(subject["binding"]);
|
|
2109
|
+
}
|
|
2110
|
+
if (kind === "SecondaryConstructor") {
|
|
2111
|
+
const params = obj["params"];
|
|
2112
|
+
if (Array.isArray(params)) {
|
|
2113
|
+
for (const p of params) {
|
|
2114
|
+
if (p.name)
|
|
2115
|
+
names.add(p.name);
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
for (const propVal of Object.values(obj)) {
|
|
2120
|
+
walk(propVal);
|
|
2121
|
+
}
|
|
2122
|
+
};
|
|
2123
|
+
for (const decl of program.declarations)
|
|
2124
|
+
walk(decl);
|
|
2125
|
+
return names;
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
exports.CodeGenerator = CodeGenerator;
|
|
2129
|
+
// ---------------------------------------------------------------------------
|
|
2130
|
+
// Type name mappings (Jalvin → TypeScript)
|
|
2131
|
+
// ---------------------------------------------------------------------------
|
|
2132
|
+
const PRIMITIVE_TYPE_MAP = {
|
|
2133
|
+
Int: "number",
|
|
2134
|
+
Long: "bigint",
|
|
2135
|
+
Float: "number",
|
|
2136
|
+
Double: "number",
|
|
2137
|
+
Boolean: "boolean",
|
|
2138
|
+
String: "string",
|
|
2139
|
+
Char: "string",
|
|
2140
|
+
Byte: "number",
|
|
2141
|
+
Short: "number",
|
|
2142
|
+
Unit: "void",
|
|
2143
|
+
Any: "unknown",
|
|
2144
|
+
Nothing: "never",
|
|
2145
|
+
};
|
|
2146
|
+
const GENERIC_TYPE_MAP = {
|
|
2147
|
+
List: "ReadonlyArray",
|
|
2148
|
+
MutableList: "Array",
|
|
2149
|
+
Set: "ReadonlySet",
|
|
2150
|
+
MutableSet: "Set",
|
|
2151
|
+
Map: "ReadonlyMap",
|
|
2152
|
+
MutableMap: "Map",
|
|
2153
|
+
Array: "Array",
|
|
2154
|
+
Pair: "[", // handled specially
|
|
2155
|
+
Triple: "[",
|
|
2156
|
+
Deferred: "Promise",
|
|
2157
|
+
StateFlow: "StateFlow",
|
|
2158
|
+
MutableStateFlow: "MutableStateFlow",
|
|
2159
|
+
Flow: "AsyncIterable",
|
|
2160
|
+
};
|
|
2161
|
+
const PRIMITIVE_TYPES = new Set(Object.keys(PRIMITIVE_TYPE_MAP));
|
|
2162
|
+
// ---------------------------------------------------------------------------
|
|
2163
|
+
// Well-known JavaScript / TypeScript global names that must never be emitted
|
|
2164
|
+
// as named imports from a wildcard-imported package.
|
|
2165
|
+
// ---------------------------------------------------------------------------
|
|
2166
|
+
const JS_GLOBAL_NAMES = new Set([
|
|
2167
|
+
// JS built-in constructors and objects
|
|
2168
|
+
"Array", "Map", "Set", "Object", "String", "Number", "Boolean",
|
|
2169
|
+
"Promise", "Error", "TypeError", "RangeError", "SyntaxError", "URIError",
|
|
2170
|
+
"EvalError", "ReferenceError",
|
|
2171
|
+
"console", "Math", "Date", "JSON", "RegExp", "Symbol", "BigInt",
|
|
2172
|
+
"Proxy", "Reflect", "globalThis", "Atomics", "SharedArrayBuffer",
|
|
2173
|
+
"ArrayBuffer", "DataView", "Int8Array", "Uint8Array", "Uint8ClampedArray",
|
|
2174
|
+
"Int16Array", "Uint16Array", "Int32Array", "Uint32Array",
|
|
2175
|
+
"Float32Array", "Float64Array", "BigInt64Array", "BigUint64Array",
|
|
2176
|
+
// Global functions
|
|
2177
|
+
"isNaN", "isFinite", "parseInt", "parseFloat", "eval",
|
|
2178
|
+
"encodeURI", "decodeURI", "encodeURIComponent", "decodeURIComponent",
|
|
2179
|
+
// Browser/Node globals
|
|
2180
|
+
"setTimeout", "clearTimeout", "setInterval", "clearInterval",
|
|
2181
|
+
"queueMicrotask", "requestAnimationFrame", "cancelAnimationFrame",
|
|
2182
|
+
"fetch", "URL", "URLSearchParams", "AbortController", "AbortSignal",
|
|
2183
|
+
"EventTarget", "Event", "CustomEvent", "FormData", "Headers",
|
|
2184
|
+
"Request", "Response", "Blob", "File", "FileReader",
|
|
2185
|
+
"Worker", "SharedWorker", "WebSocket",
|
|
2186
|
+
"ReadableStream", "WritableStream", "TransformStream",
|
|
2187
|
+
"document", "window", "navigator", "location", "history", "screen",
|
|
2188
|
+
"performance", "crypto", "indexedDB", "localStorage", "sessionStorage",
|
|
2189
|
+
"confirm", "alert", "prompt",
|
|
2190
|
+
"process", "Buffer", "global", "require", "module", "exports", "__dirname", "__filename",
|
|
2191
|
+
// Special identifiers
|
|
2192
|
+
"undefined", "null", "NaN", "Infinity",
|
|
2193
|
+
"it", "this", "super", "arguments", "new", "class", "function",
|
|
2194
|
+
// TypeScript primitive type names
|
|
2195
|
+
"any", "unknown", "never", "void", "string", "number", "boolean", "bigint",
|
|
2196
|
+
"object", "symbol",
|
|
2197
|
+
// Jalvin type names that map to TS primitives (never need import)
|
|
2198
|
+
"String", "Boolean", "Any", "Nothing", "Unit", "Char", "Byte", "Short",
|
|
2199
|
+
"Float", "Double",
|
|
2200
|
+
]);
|
|
2201
|
+
// ---------------------------------------------------------------------------
|
|
2202
|
+
// Public helper
|
|
2203
|
+
// ---------------------------------------------------------------------------
|
|
2204
|
+
function generate(program, opts, operatorOverloads, typeMap) {
|
|
2205
|
+
return new CodeGenerator(opts).generate(program, operatorOverloads, typeMap);
|
|
2206
|
+
}
|
|
2207
|
+
//# sourceMappingURL=index.js.map
|