gofront 0.0.1
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/LICENSE +9 -0
- package/README.md +202 -0
- package/package.json +41 -0
- package/src/codegen.js +1192 -0
- package/src/compiler.js +156 -0
- package/src/dts-parser.js +762 -0
- package/src/index.js +338 -0
- package/src/lexer.js +465 -0
- package/src/parser.js +1120 -0
- package/src/resolver.js +114 -0
- package/src/typechecker.js +1252 -0
package/src/codegen.js
ADDED
|
@@ -0,0 +1,1192 @@
|
|
|
1
|
+
// Code generator: walks the typed AST and emits clean JavaScript.
|
|
2
|
+
//
|
|
3
|
+
// Key design choices:
|
|
4
|
+
// - Structs → ES6 classes with a single destructured-object constructor
|
|
5
|
+
// - Methods → class instance methods
|
|
6
|
+
// - Multiple returns → JS arrays e.g. return [a, b]
|
|
7
|
+
// - Destructuring → let [a, b] = f()
|
|
8
|
+
// - nil → null
|
|
9
|
+
// - Slices → JS arrays (append → spread, len → .length)
|
|
10
|
+
// - Maps → plain JS objects (map[string]T)
|
|
11
|
+
// - make([]T, n) → new Array(n).fill(zeroOf(T))
|
|
12
|
+
// - make(map[K]V) → {}
|
|
13
|
+
// - for range → for...of with .entries()
|
|
14
|
+
|
|
15
|
+
// ── Minimal VLQ / source-map helpers ─────────────────────────
|
|
16
|
+
const B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
17
|
+
|
|
18
|
+
function vlqEncode(value) {
|
|
19
|
+
// Signed VLQ: sign bit in LSB of first group, then 5-bit continuation chunks
|
|
20
|
+
let vlq = value < 0 ? (-value << 1) | 1 : value << 1;
|
|
21
|
+
let result = "";
|
|
22
|
+
do {
|
|
23
|
+
let digit = vlq & 0x1f;
|
|
24
|
+
vlq >>>= 5;
|
|
25
|
+
if (vlq > 0) digit |= 0x20; // continuation bit
|
|
26
|
+
result += B64[digit];
|
|
27
|
+
} while (vlq > 0);
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function buildSourceMap(sourceName, mappings) {
|
|
32
|
+
// mappings: Array<{ genLine: number, srcLine: number }> (0-based)
|
|
33
|
+
// Emits one segment per generated line, column 0 → source line (delta-encoded).
|
|
34
|
+
const lines = [];
|
|
35
|
+
let prevSrcLine = 0;
|
|
36
|
+
const maxGen = mappings.reduce((m, e) => Math.max(m, e.genLine), -1);
|
|
37
|
+
for (let g = 0; g <= maxGen; g++) {
|
|
38
|
+
const entry = mappings.find((e) => e.genLine === g);
|
|
39
|
+
if (entry) {
|
|
40
|
+
const srcDelta = entry.srcLine - prevSrcLine;
|
|
41
|
+
// Segment: [genCol=0, srcFileIdx=0, srcLineDelta, srcCol=0]
|
|
42
|
+
lines.push(
|
|
43
|
+
vlqEncode(0) + vlqEncode(0) + vlqEncode(srcDelta) + vlqEncode(0),
|
|
44
|
+
);
|
|
45
|
+
prevSrcLine = entry.srcLine;
|
|
46
|
+
} else {
|
|
47
|
+
lines.push(""); // no mapping for this generated line
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return JSON.stringify({
|
|
51
|
+
version: 3,
|
|
52
|
+
sources: [sourceName],
|
|
53
|
+
names: [],
|
|
54
|
+
mappings: lines.join(";"),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export class CodeGen {
|
|
59
|
+
// jsImports: Map<importPath, string[]> — npm package imports to emit at top of file
|
|
60
|
+
// bundledPackages: Set<string> — GoFront package names bundled inline;
|
|
61
|
+
// SelectorExpr `pkg.Foo` → just `Foo`
|
|
62
|
+
constructor(
|
|
63
|
+
checker = null,
|
|
64
|
+
jsImports = new Map(),
|
|
65
|
+
bundledPackages = new Set(),
|
|
66
|
+
) {
|
|
67
|
+
this.checker = checker;
|
|
68
|
+
this.out = [];
|
|
69
|
+
this.indent = 0;
|
|
70
|
+
this.structNames = new Set();
|
|
71
|
+
this.jsImports = jsImports;
|
|
72
|
+
this.bundledPackages = bundledPackages;
|
|
73
|
+
this.namedReturnVars = null; // names of current function's named return vars
|
|
74
|
+
this._srcMappings = []; // { genLine, srcLine } for source map
|
|
75
|
+
// Runtime helper usage tracking — only emit helpers that are actually used
|
|
76
|
+
this._usesLen = false;
|
|
77
|
+
this._usesAppend = false;
|
|
78
|
+
this._usesSliceGuard = false;
|
|
79
|
+
this._usesSprintf = false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── Output helpers ───────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
emit(s) {
|
|
85
|
+
this.out.push(s);
|
|
86
|
+
}
|
|
87
|
+
line(s = "", srcLine = null) {
|
|
88
|
+
if (srcLine != null) {
|
|
89
|
+
this._srcMappings.push({
|
|
90
|
+
genLine: this.out.length,
|
|
91
|
+
srcLine: srcLine - 1,
|
|
92
|
+
}); // 0-based
|
|
93
|
+
}
|
|
94
|
+
this.out.push(" ".repeat(this.indent) + s);
|
|
95
|
+
}
|
|
96
|
+
blank() {
|
|
97
|
+
this.out.push("");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
indented(fn) {
|
|
101
|
+
this.indent++;
|
|
102
|
+
fn();
|
|
103
|
+
this.indent--;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
generate(program) {
|
|
107
|
+
// Collect struct names first (needed by zero-value helpers and genCompositeLit)
|
|
108
|
+
for (const d of program.decls) {
|
|
109
|
+
if (d.kind === "TypeDecl" && d.type.kind === "StructType") {
|
|
110
|
+
this.structNames.add(d.name);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Build a map: structName → [MethodDecl, ...]
|
|
115
|
+
const methods = new Map();
|
|
116
|
+
for (const d of program.decls) {
|
|
117
|
+
if (d.kind === "MethodDecl") {
|
|
118
|
+
const name = d.recvType.name;
|
|
119
|
+
if (!methods.has(name)) methods.set(name, []);
|
|
120
|
+
methods.get(name).push(d);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Emit ESM import statements for npm packages
|
|
125
|
+
for (const [importPath, names] of this.jsImports) {
|
|
126
|
+
if (names.length === 0) continue;
|
|
127
|
+
this.line(`import { ${names.join(", ")} } from '${importPath}';`);
|
|
128
|
+
}
|
|
129
|
+
if (this.jsImports.size > 0) this.blank();
|
|
130
|
+
|
|
131
|
+
for (const d of program.decls) {
|
|
132
|
+
if (d.kind === "TypeDecl") {
|
|
133
|
+
this.genTypeDeclWithMethods(d, methods.get(d.name) ?? []);
|
|
134
|
+
this.blank();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
for (const d of program.decls) {
|
|
139
|
+
if (d.kind === "VarDecl") {
|
|
140
|
+
this.genVarDecl(d);
|
|
141
|
+
this.blank();
|
|
142
|
+
}
|
|
143
|
+
if (d.kind === "ConstDecl") {
|
|
144
|
+
this.genConstDecl(d);
|
|
145
|
+
this.blank();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Emit functions — rename duplicate init() to init$0, init$1, etc.
|
|
150
|
+
let initCount = 0;
|
|
151
|
+
const initNames = [];
|
|
152
|
+
for (const d of program.decls) {
|
|
153
|
+
if (d.kind === "FuncDecl") {
|
|
154
|
+
if (d.name === "init") {
|
|
155
|
+
const renamed = initCount === 0 ? "init" : `init$${initCount}`;
|
|
156
|
+
initNames.push(renamed);
|
|
157
|
+
const saved = d.name;
|
|
158
|
+
d.name = renamed;
|
|
159
|
+
this.genFuncDecl(d);
|
|
160
|
+
d.name = saved;
|
|
161
|
+
initCount++;
|
|
162
|
+
} else {
|
|
163
|
+
this.genFuncDecl(d);
|
|
164
|
+
}
|
|
165
|
+
this.blank();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Auto-call init() functions, then main()
|
|
170
|
+
for (const name of initNames) this.line(`${name}();`);
|
|
171
|
+
const hasMain = program.decls.some(
|
|
172
|
+
(d) => d.kind === "FuncDecl" && d.name === "main",
|
|
173
|
+
);
|
|
174
|
+
if (hasMain) this.line("main();");
|
|
175
|
+
|
|
176
|
+
// Prepend runtime helpers that were actually used
|
|
177
|
+
const helpers = [];
|
|
178
|
+
if (this._usesLen)
|
|
179
|
+
helpers.push("function __len(a) { return a?.length ?? 0; }");
|
|
180
|
+
if (this._usesAppend)
|
|
181
|
+
helpers.push(
|
|
182
|
+
"function __append(a, ...b) { return a ? [...a, ...b] : b; }",
|
|
183
|
+
);
|
|
184
|
+
if (this._usesSliceGuard)
|
|
185
|
+
helpers.push("function __s(a) { return a || []; }");
|
|
186
|
+
if (this._usesSprintf)
|
|
187
|
+
helpers.push(
|
|
188
|
+
"function __sprintf(f,...a){let i=0;return f.replace(/%[sdvf%]/g,m=>{if(m==='%%')return'%';const v=a[i++];return m==='%f'?Number(v).toString():String(v==null?'<nil>':v);});}",
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
if (helpers.length > 0) this.out.unshift(...helpers, "");
|
|
192
|
+
|
|
193
|
+
// Strip leading blank line
|
|
194
|
+
while (this.out[0] === "") this.out.shift();
|
|
195
|
+
return this.out.join("\n");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Generate a single bundle from multiple programs (same-package multi-file).
|
|
199
|
+
// Merges all decls and generates as if it were one program.
|
|
200
|
+
generateAll(programs) {
|
|
201
|
+
const merged = { decls: programs.flatMap((p) => p.decls) };
|
|
202
|
+
return this.generate(merged);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Returns a source map JSON string for the last generate() call.
|
|
206
|
+
// sourceName: the .go filename to reference in the map.
|
|
207
|
+
getSourceMap(sourceName) {
|
|
208
|
+
return buildSourceMap(sourceName, this._srcMappings);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ── Type declarations ────────────────────────────────────────
|
|
212
|
+
|
|
213
|
+
genTypeDecl(decl) {
|
|
214
|
+
this.genTypeDeclWithMethods(decl, []);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
genTypeDeclWithMethods(decl, methodDecls) {
|
|
218
|
+
if (decl.type.kind === "StructType") {
|
|
219
|
+
this.genStruct(decl.name, decl.type, methodDecls);
|
|
220
|
+
} else if (decl.type.kind === "InterfaceType") {
|
|
221
|
+
// Interfaces are compile-time only — no JS output needed.
|
|
222
|
+
this.line(`// interface ${decl.name} (compile-time only)`);
|
|
223
|
+
} else {
|
|
224
|
+
// Type alias — no JS equivalent needed unless it's used as a constructor.
|
|
225
|
+
// e.g. type MyInt int → just a comment
|
|
226
|
+
this.line(`// type ${decl.name} = ${this.typeComment(decl.type)}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
genStruct(name, structTypeAst, methodDecls) {
|
|
231
|
+
const fields = [];
|
|
232
|
+
if (this.checker) {
|
|
233
|
+
const resolved = this.checker.types.get(name)?.underlying;
|
|
234
|
+
if (resolved?.kind === "struct") {
|
|
235
|
+
for (const [fName, fType] of resolved.fields.entries()) {
|
|
236
|
+
fields.push({ name: fName, zero: this.zeroValueForType(fType) });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
} else {
|
|
240
|
+
// Fallback (e.g. tests without typechecker)
|
|
241
|
+
for (const f of structTypeAst.fields) {
|
|
242
|
+
if (f.embedded) continue; // Can't resolve here
|
|
243
|
+
const zero = this.zeroValueForTypeNode(f.type);
|
|
244
|
+
for (const n of f.names) fields.push({ name: n, zero });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
this.line(`class ${name} {`);
|
|
249
|
+
this.indented(() => {
|
|
250
|
+
if (fields.length === 0) {
|
|
251
|
+
this.line("constructor() {}");
|
|
252
|
+
} else {
|
|
253
|
+
const params = fields.map((f) => `${f.name} = ${f.zero}`).join(", ");
|
|
254
|
+
this.line(`constructor({ ${params} } = {}) {`);
|
|
255
|
+
this.indented(() => {
|
|
256
|
+
for (const f of fields) this.line(`this.${f.name} = ${f.name};`);
|
|
257
|
+
});
|
|
258
|
+
this.line("}");
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Methods
|
|
262
|
+
for (const m of methodDecls) {
|
|
263
|
+
this.blank();
|
|
264
|
+
this.genMethod(m);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Delegation stubs for promoted embedded methods
|
|
268
|
+
if (this.checker) {
|
|
269
|
+
const resolvedType = this.checker.types.get(name)?.underlying;
|
|
270
|
+
if (resolvedType?.kind === "struct" && resolvedType._embeds) {
|
|
271
|
+
const declared = new Set(methodDecls.map((m) => m.name));
|
|
272
|
+
for (const embed of resolvedType._embeds) {
|
|
273
|
+
const embedName = embed.kind === "named" ? embed.name : null;
|
|
274
|
+
if (!embedName) continue;
|
|
275
|
+
const embedBase = embed.kind === "named" ? embed.underlying : embed;
|
|
276
|
+
if (embedBase?.kind !== "struct" || !embedBase.methods) continue;
|
|
277
|
+
for (const [mName] of embedBase.methods.entries()) {
|
|
278
|
+
if (!declared.has(mName)) {
|
|
279
|
+
this.blank();
|
|
280
|
+
this.line(
|
|
281
|
+
`${mName}(...__a) { return ${embedName}.prototype.${mName}.call(this, ...__a); }`,
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
this.line("}");
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
genMethod(decl) {
|
|
293
|
+
const params = decl.params.map((p) => p.name).join(", ");
|
|
294
|
+
const asyncPrefix = decl.async ? "async " : "";
|
|
295
|
+
this.line(`${asyncPrefix}${decl.name}(${params}) {`);
|
|
296
|
+
this.indented(() => {
|
|
297
|
+
if (decl.recvName && decl.recvName !== "_") {
|
|
298
|
+
this.line(`const ${decl.recvName} = this;`);
|
|
299
|
+
}
|
|
300
|
+
this._withNamedReturns(decl, () => this._genBody(decl.body));
|
|
301
|
+
});
|
|
302
|
+
this.line("}");
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ── Function declarations ────────────────────────────────────
|
|
306
|
+
|
|
307
|
+
genFuncDecl(decl) {
|
|
308
|
+
const params = decl.params
|
|
309
|
+
.map((p, i) =>
|
|
310
|
+
p.variadic && i === decl.params.length - 1 ? `...${p.name}` : p.name,
|
|
311
|
+
)
|
|
312
|
+
.join(", ");
|
|
313
|
+
const asyncPrefix = decl.async ? "async " : "";
|
|
314
|
+
const srcLine = decl._line ?? null;
|
|
315
|
+
this.line(
|
|
316
|
+
`${asyncPrefix}function ${decl.name}(${params}) {`,
|
|
317
|
+
srcLine ? srcLine - 1 : null,
|
|
318
|
+
);
|
|
319
|
+
this.indented(() =>
|
|
320
|
+
this._withNamedReturns(decl, () => this._genBody(decl.body)),
|
|
321
|
+
);
|
|
322
|
+
this.line("}");
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
_withNamedReturns(decl, fn) {
|
|
326
|
+
const named = decl.returnType?._namedReturns;
|
|
327
|
+
const prev = this.namedReturnVars;
|
|
328
|
+
if (named) {
|
|
329
|
+
// Emit zero-value declarations for named return vars
|
|
330
|
+
for (const { name, type } of named) {
|
|
331
|
+
if (name)
|
|
332
|
+
this.line(`let ${name} = ${this.zeroValueForTypeNode(type)};`);
|
|
333
|
+
}
|
|
334
|
+
this.namedReturnVars = named.map((r) => r.name).filter(Boolean);
|
|
335
|
+
} else {
|
|
336
|
+
this.namedReturnVars = null;
|
|
337
|
+
}
|
|
338
|
+
fn();
|
|
339
|
+
this.namedReturnVars = prev;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Emit a function body, wrapping in try/finally for defer if needed.
|
|
343
|
+
_genBody(body) {
|
|
344
|
+
if (!this._hasDefer(body)) {
|
|
345
|
+
this.genBlock(body, true);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
this.line("const __defers = [];");
|
|
349
|
+
this.line("try {");
|
|
350
|
+
this.indented(() => this.genBlock(body, true));
|
|
351
|
+
this.line("} finally {");
|
|
352
|
+
this.indented(() => {
|
|
353
|
+
this.line(
|
|
354
|
+
"for (let __i = __defers.length - 1; __i >= 0; __i--) __defers[__i]();",
|
|
355
|
+
);
|
|
356
|
+
});
|
|
357
|
+
this.line("}");
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Recursively check if a block (or nested blocks) contains any DeferStmt.
|
|
361
|
+
_hasDefer(block) {
|
|
362
|
+
if (!block?.stmts) return false;
|
|
363
|
+
for (const stmt of block.stmts) {
|
|
364
|
+
if (stmt.kind === "DeferStmt") return true;
|
|
365
|
+
if (stmt.kind === "Block" && this._hasDefer(stmt)) return true;
|
|
366
|
+
if (
|
|
367
|
+
stmt.kind === "IfStmt" &&
|
|
368
|
+
(this._hasDefer(stmt.body) || this._hasDefer(stmt.elseBody))
|
|
369
|
+
)
|
|
370
|
+
return true;
|
|
371
|
+
if (stmt.kind === "ForStmt" && this._hasDefer(stmt.body)) return true;
|
|
372
|
+
if (
|
|
373
|
+
stmt.kind === "SwitchStmt" &&
|
|
374
|
+
stmt.cases.some((c) => c.stmts.some((s) => s.kind === "DeferStmt"))
|
|
375
|
+
)
|
|
376
|
+
return true;
|
|
377
|
+
}
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// ── Variable / const declarations ────────────────────────────
|
|
382
|
+
|
|
383
|
+
genVarDecl(decl) {
|
|
384
|
+
for (const spec of decl.decls) {
|
|
385
|
+
if (spec.value) {
|
|
386
|
+
const vals = spec.value.map((v) => this.genExpr(v));
|
|
387
|
+
if (spec.names.length === 1) {
|
|
388
|
+
this.line(`let ${spec.names[0]} = ${vals[0]};`);
|
|
389
|
+
} else {
|
|
390
|
+
// let [a, b] = [v1, v2]
|
|
391
|
+
this.line(`let [${spec.names.join(", ")}] = [${vals.join(", ")}];`);
|
|
392
|
+
}
|
|
393
|
+
} else {
|
|
394
|
+
const zero = spec.type ? this.zeroValueForTypeNode(spec.type) : "null";
|
|
395
|
+
for (const name of spec.names) this.line(`let ${name} = ${zero};`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
genConstDecl(decl) {
|
|
401
|
+
for (const spec of decl.decls) {
|
|
402
|
+
const vals = spec.value.map((v) => this.genExpr(v));
|
|
403
|
+
if (spec.names.length === 1) {
|
|
404
|
+
this.line(`const ${spec.names[0]} = ${vals[0]};`);
|
|
405
|
+
} else {
|
|
406
|
+
this.line(`const [${spec.names.join(", ")}] = [${vals.join(", ")}];`);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// ── Statements ───────────────────────────────────────────────
|
|
412
|
+
|
|
413
|
+
genBlock(block, _skipBraces = false) {
|
|
414
|
+
for (const stmt of block.stmts) this.genStmt(stmt);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
genStmt(stmt) {
|
|
418
|
+
// Record source mapping for the first line this statement produces
|
|
419
|
+
const srcLine = stmt._line ?? null;
|
|
420
|
+
const _line0 = this.out.length;
|
|
421
|
+
switch (stmt.kind) {
|
|
422
|
+
case "VarDecl":
|
|
423
|
+
this.genVarDecl(stmt);
|
|
424
|
+
break;
|
|
425
|
+
case "ConstDecl":
|
|
426
|
+
this.genConstDecl(stmt);
|
|
427
|
+
break;
|
|
428
|
+
case "TypeDecl":
|
|
429
|
+
this.genTypeDecl(stmt);
|
|
430
|
+
break;
|
|
431
|
+
|
|
432
|
+
case "DefineStmt": {
|
|
433
|
+
// Map comma-ok: v, ok := m[key] → let v = m[key]; let ok = key in m;
|
|
434
|
+
const rhsNode = stmt.rhs[0];
|
|
435
|
+
if (
|
|
436
|
+
stmt.rhs.length === 1 &&
|
|
437
|
+
stmt.lhs.length === 2 &&
|
|
438
|
+
rhsNode?.kind === "IndexExpr" &&
|
|
439
|
+
rhsNode.expr?._type?.kind === "map"
|
|
440
|
+
) {
|
|
441
|
+
const mapExpr = this.genExpr(rhsNode.expr);
|
|
442
|
+
const keyExpr = this.genExpr(rhsNode.index);
|
|
443
|
+
const [vName, okName] = stmt.lhs.map(
|
|
444
|
+
(e) => e.name ?? this.genExpr(e),
|
|
445
|
+
);
|
|
446
|
+
if (vName !== "_")
|
|
447
|
+
this.line(`let ${vName} = ${mapExpr}[${keyExpr}];`);
|
|
448
|
+
if (okName !== "_")
|
|
449
|
+
this.line(`let ${okName} = (${keyExpr}) in ${mapExpr};`);
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
const rhs = stmt.rhs.map((e) => this.genExpr(e));
|
|
453
|
+
const lhs = stmt.lhs.map((e) => e.name ?? this.genExpr(e));
|
|
454
|
+
const redecls = stmt.lhs.map((e) => !!e._redecl);
|
|
455
|
+
const anyRedecl = redecls.some((r) => r);
|
|
456
|
+
if (lhs.length === 1) {
|
|
457
|
+
if (redecls[0]) {
|
|
458
|
+
this.line(`${lhs[0]} = ${rhs[0]};`);
|
|
459
|
+
} else {
|
|
460
|
+
this.line(`let ${lhs[0]} = ${rhs[0]};`);
|
|
461
|
+
}
|
|
462
|
+
} else if (!anyRedecl) {
|
|
463
|
+
// All new: let [a, b] = ...
|
|
464
|
+
const rhsStr = rhs.length === 1 ? rhs[0] : `[${rhs.join(", ")}]`;
|
|
465
|
+
this.line(`let [${lhs.join(", ")}] = ${rhsStr};`);
|
|
466
|
+
} else {
|
|
467
|
+
// Mixed new/existing: emit separate statements
|
|
468
|
+
// First compute the tuple into a temp if needed
|
|
469
|
+
if (rhs.length === 1 && lhs.length > 1) {
|
|
470
|
+
const tmp = "__t";
|
|
471
|
+
this.line(`let ${tmp} = ${rhs[0]};`);
|
|
472
|
+
for (let i = 0; i < lhs.length; i++) {
|
|
473
|
+
const prefix = redecls[i] ? "" : "let ";
|
|
474
|
+
this.line(`${prefix}${lhs[i]} = ${tmp}[${i}];`);
|
|
475
|
+
}
|
|
476
|
+
} else {
|
|
477
|
+
for (let i = 0; i < lhs.length; i++) {
|
|
478
|
+
const prefix = redecls[i] ? "" : "let ";
|
|
479
|
+
this.line(`${prefix}${lhs[i]} = ${rhs[i]};`);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
break;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
case "AssignStmt": {
|
|
487
|
+
const rhs = stmt.rhs.map((e) => this.genExpr(e));
|
|
488
|
+
for (const e of stmt.lhs) if (e.kind === "IndexExpr") e._lvalue = true;
|
|
489
|
+
const lhs = stmt.lhs.map((e) => this.genExpr(e));
|
|
490
|
+
if (lhs.length === 1) {
|
|
491
|
+
this.line(`${lhs[0]} ${stmt.op} ${rhs[0]};`);
|
|
492
|
+
} else {
|
|
493
|
+
const rhsStr = rhs.length === 1 ? rhs[0] : `[${rhs.join(", ")}]`;
|
|
494
|
+
this.line(`[${lhs.join(", ")}] = ${rhsStr};`);
|
|
495
|
+
}
|
|
496
|
+
break;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
case "IncDecStmt":
|
|
500
|
+
this.line(`${this.genExpr(stmt.expr)}${stmt.op};`);
|
|
501
|
+
break;
|
|
502
|
+
|
|
503
|
+
case "ExprStmt":
|
|
504
|
+
this.line(`${this.genExpr(stmt.expr)};`);
|
|
505
|
+
break;
|
|
506
|
+
|
|
507
|
+
case "ReturnStmt": {
|
|
508
|
+
if (stmt.values.length === 0) {
|
|
509
|
+
// bare return — emit named return vars if present
|
|
510
|
+
if (this.namedReturnVars?.length > 0) {
|
|
511
|
+
const vars = this.namedReturnVars;
|
|
512
|
+
this.line(
|
|
513
|
+
vars.length === 1
|
|
514
|
+
? `return ${vars[0]};`
|
|
515
|
+
: `return [${vars.join(", ")}];`,
|
|
516
|
+
);
|
|
517
|
+
} else {
|
|
518
|
+
this.line("return;");
|
|
519
|
+
}
|
|
520
|
+
} else if (stmt.values.length === 1) {
|
|
521
|
+
this.line(`return ${this.genExpr(stmt.values[0])};`);
|
|
522
|
+
} else {
|
|
523
|
+
this.line(
|
|
524
|
+
`return [${stmt.values.map((v) => this.genExpr(v)).join(", ")}];`,
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
break;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
case "IfStmt": {
|
|
531
|
+
if (stmt.init) {
|
|
532
|
+
// Wrap in a block so init variable is scoped
|
|
533
|
+
this.line("{");
|
|
534
|
+
this.indented(() => {
|
|
535
|
+
this.genStmt(stmt.init);
|
|
536
|
+
this._genIf(stmt);
|
|
537
|
+
});
|
|
538
|
+
this.line("}");
|
|
539
|
+
} else {
|
|
540
|
+
this._genIf(stmt);
|
|
541
|
+
}
|
|
542
|
+
break;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
case "ForStmt":
|
|
546
|
+
this.genFor(stmt);
|
|
547
|
+
break;
|
|
548
|
+
|
|
549
|
+
case "SwitchStmt":
|
|
550
|
+
this.genSwitch(stmt);
|
|
551
|
+
break;
|
|
552
|
+
|
|
553
|
+
case "DeferStmt":
|
|
554
|
+
this.line(`__defers.push(() => { ${this.genExpr(stmt.call)}; });`);
|
|
555
|
+
break;
|
|
556
|
+
|
|
557
|
+
case "BranchStmt":
|
|
558
|
+
if (stmt.keyword !== "fallthrough") this.line(`${stmt.keyword};`);
|
|
559
|
+
// fallthrough: omit — JS switch falls through naturally without a break
|
|
560
|
+
break;
|
|
561
|
+
|
|
562
|
+
case "Block":
|
|
563
|
+
this.line("{");
|
|
564
|
+
this.indented(() => this.genBlock(stmt));
|
|
565
|
+
this.line("}");
|
|
566
|
+
break;
|
|
567
|
+
|
|
568
|
+
default:
|
|
569
|
+
break;
|
|
570
|
+
}
|
|
571
|
+
// Record source mapping: first output line this statement produced
|
|
572
|
+
if (srcLine != null && this.out.length > _line0) {
|
|
573
|
+
this._srcMappings.push({ genLine: _line0, srcLine: srcLine - 1 });
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
_genIf(stmt) {
|
|
578
|
+
this.line(`if (${this.genExpr(stmt.cond)}) {`);
|
|
579
|
+
this.indented(() => this.genBlock(stmt.body));
|
|
580
|
+
this._genElse(stmt.elseBody);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
_genElse(elseBody) {
|
|
584
|
+
if (!elseBody) {
|
|
585
|
+
this.line("}");
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
if (elseBody.kind === "IfStmt") {
|
|
589
|
+
this.line(`} else if (${this.genExpr(elseBody.cond)}) {`);
|
|
590
|
+
this.indented(() => this.genBlock(elseBody.body));
|
|
591
|
+
this._genElse(elseBody.elseBody);
|
|
592
|
+
} else {
|
|
593
|
+
this.line("} else {");
|
|
594
|
+
this.indented(() => this.genBlock(elseBody));
|
|
595
|
+
this.line("}");
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
genFor(stmt) {
|
|
600
|
+
// Detect range: init is DefineStmt with rhs[0] being a RangeExpr
|
|
601
|
+
if (this.isRangeFor(stmt)) {
|
|
602
|
+
this.genRangeFor(stmt);
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (!stmt.init && !stmt.post) {
|
|
607
|
+
if (!stmt.cond) {
|
|
608
|
+
// infinite loop
|
|
609
|
+
this.line("while (true) {");
|
|
610
|
+
} else {
|
|
611
|
+
this.line(`while (${this.genExpr(stmt.cond)}) {`);
|
|
612
|
+
}
|
|
613
|
+
this.indented(() => this.genBlock(stmt.body));
|
|
614
|
+
this.line("}");
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Standard C-style for
|
|
619
|
+
const init = stmt.init ? this.stmtInline(stmt.init) : "";
|
|
620
|
+
const cond = stmt.cond ? this.genExpr(stmt.cond) : "";
|
|
621
|
+
const post = stmt.post ? this.stmtInline(stmt.post) : "";
|
|
622
|
+
this.line(`for (${init}; ${cond}; ${post}) {`);
|
|
623
|
+
this.indented(() => this.genBlock(stmt.body));
|
|
624
|
+
this.line("}");
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
isRangeFor(stmt) {
|
|
628
|
+
if (!stmt.init) return false;
|
|
629
|
+
const init = stmt.init;
|
|
630
|
+
if (init.kind !== "DefineStmt" && init.kind !== "AssignStmt") return false;
|
|
631
|
+
return init.rhs?.[0]?.kind === "RangeExpr";
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
genRangeFor(stmt) {
|
|
635
|
+
const init = stmt.init;
|
|
636
|
+
const range = init.rhs[0];
|
|
637
|
+
const lhs = init.lhs.map((e) => e.name ?? this.genExpr(e));
|
|
638
|
+
const iteree = this.genExpr(range.expr);
|
|
639
|
+
const iterType = range.expr._type;
|
|
640
|
+
|
|
641
|
+
let iterExpr;
|
|
642
|
+
if (
|
|
643
|
+
iterType?.kind === "map" ||
|
|
644
|
+
(iterType?.kind === "named" && iterType.underlying?.kind === "map")
|
|
645
|
+
) {
|
|
646
|
+
// map: for k, v := range m → for (const [k, v] of Object.entries(m))
|
|
647
|
+
iterExpr = `Object.entries(${iteree})`;
|
|
648
|
+
} else if (iterType?.kind === "basic" && iterType.name === "string") {
|
|
649
|
+
// string: for i, ch := range s → for (const [i, ch] of s.split('').entries())
|
|
650
|
+
iterExpr =
|
|
651
|
+
lhs.length === 1
|
|
652
|
+
? `Array.from(${iteree}).keys()`
|
|
653
|
+
: `Array.from(${iteree}).entries()`;
|
|
654
|
+
} else {
|
|
655
|
+
// slice/array: for i, v := range arr → for (const [i, v] of __s(arr).entries())
|
|
656
|
+
// null-guard handles nil slices (zero value) gracefully.
|
|
657
|
+
this._usesSliceGuard = true;
|
|
658
|
+
if (lhs.length === 1) {
|
|
659
|
+
iterExpr = `__s(${iteree}).keys()`;
|
|
660
|
+
} else {
|
|
661
|
+
iterExpr = `__s(${iteree}).entries()`;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
const binding =
|
|
666
|
+
lhs.length === 1
|
|
667
|
+
? lhs[0] === "_"
|
|
668
|
+
? "_$"
|
|
669
|
+
: lhs[0]
|
|
670
|
+
: `[${lhs.map((n) => (n === "_" ? "_$" : n)).join(", ")}]`;
|
|
671
|
+
|
|
672
|
+
this.line(`for (const ${binding} of ${iterExpr}) {`);
|
|
673
|
+
this.indented(() => this.genBlock(stmt.body));
|
|
674
|
+
this.line("}");
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// Inline a statement as a for-init/post string (no semicolon)
|
|
678
|
+
stmtInline(stmt) {
|
|
679
|
+
switch (stmt.kind) {
|
|
680
|
+
case "DefineStmt": {
|
|
681
|
+
const rhs = stmt.rhs.map((e) => this.genExpr(e)).join(", ");
|
|
682
|
+
const lhs = stmt.lhs.map((e) => e.name ?? this.genExpr(e)).join(", ");
|
|
683
|
+
return `let ${lhs} = ${rhs}`;
|
|
684
|
+
}
|
|
685
|
+
case "AssignStmt": {
|
|
686
|
+
const rhs = stmt.rhs.map((e) => this.genExpr(e)).join(", ");
|
|
687
|
+
const lhs = stmt.lhs.map((e) => this.genExpr(e)).join(", ");
|
|
688
|
+
return `${lhs} ${stmt.op} ${rhs}`;
|
|
689
|
+
}
|
|
690
|
+
case "IncDecStmt":
|
|
691
|
+
return `${this.genExpr(stmt.expr)}${stmt.op}`;
|
|
692
|
+
case "ExprStmt":
|
|
693
|
+
return this.genExpr(stmt.expr);
|
|
694
|
+
default:
|
|
695
|
+
return "";
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
genSwitch(stmt) {
|
|
700
|
+
if (stmt.init) {
|
|
701
|
+
this.line("{");
|
|
702
|
+
this.indent++;
|
|
703
|
+
this.genStmt(stmt.init);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
const tag = stmt.tag ? this.genExpr(stmt.tag) : "true";
|
|
707
|
+
this.line(`switch (${tag}) {`);
|
|
708
|
+
this.indented(() => {
|
|
709
|
+
for (const c of stmt.cases) {
|
|
710
|
+
if (c.list) {
|
|
711
|
+
for (const e of c.list) this.line(`case ${this.genExpr(e)}:`);
|
|
712
|
+
} else {
|
|
713
|
+
this.line("default:");
|
|
714
|
+
}
|
|
715
|
+
// Wrap each case body in {} so `let` declarations don't bleed
|
|
716
|
+
// across sibling cases (JS switch shares one block scope).
|
|
717
|
+
this.line("{");
|
|
718
|
+
this.indented(() => {
|
|
719
|
+
for (const s of c.stmts) this.genStmt(s);
|
|
720
|
+
// Go doesn't fall through by default — add break unless last stmt is return/break
|
|
721
|
+
const last = c.stmts[c.stmts.length - 1];
|
|
722
|
+
if (
|
|
723
|
+
!last ||
|
|
724
|
+
(last.kind !== "ReturnStmt" && last.kind !== "BranchStmt")
|
|
725
|
+
) {
|
|
726
|
+
this.line("break;");
|
|
727
|
+
}
|
|
728
|
+
});
|
|
729
|
+
this.line("}");
|
|
730
|
+
}
|
|
731
|
+
});
|
|
732
|
+
this.line("}");
|
|
733
|
+
|
|
734
|
+
if (stmt.init) {
|
|
735
|
+
this.indent--;
|
|
736
|
+
this.line("}");
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// ── Expressions ──────────────────────────────────────────────
|
|
741
|
+
|
|
742
|
+
genExpr(expr) {
|
|
743
|
+
switch (expr.kind) {
|
|
744
|
+
case "BasicLit":
|
|
745
|
+
if (expr.litKind === "STRING") return JSON.stringify(expr.value);
|
|
746
|
+
return expr.value; // int, float, bool, nil(null)
|
|
747
|
+
|
|
748
|
+
case "Ident":
|
|
749
|
+
return expr.name;
|
|
750
|
+
|
|
751
|
+
case "UnaryExpr": {
|
|
752
|
+
const op = expr.op === "^" ? "~" : expr.op; // bitwise NOT
|
|
753
|
+
// Dereference/address-of — transparent in JS
|
|
754
|
+
if (op === "*" || op === "&") return this.genExpr(expr.operand);
|
|
755
|
+
return `${op}${this.genExpr(expr.operand)}`;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
case "BinaryExpr": {
|
|
759
|
+
const l = this.genExpr(expr.left);
|
|
760
|
+
const r = this.genExpr(expr.right);
|
|
761
|
+
// Integer division: only when both sides are int
|
|
762
|
+
if (
|
|
763
|
+
expr.op === "/" &&
|
|
764
|
+
this.isIntType(expr.left._type) &&
|
|
765
|
+
this.isIntType(expr.right._type)
|
|
766
|
+
) {
|
|
767
|
+
return `Math.trunc(${l} / ${r})`;
|
|
768
|
+
}
|
|
769
|
+
return `${l} ${expr.op} ${r}`;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
case "CallExpr":
|
|
773
|
+
return this.genCall(expr);
|
|
774
|
+
|
|
775
|
+
case "SelectorExpr": {
|
|
776
|
+
const base = this.genExpr(expr.expr);
|
|
777
|
+
// Bundled GoFront packages are inlined — drop the qualifier.
|
|
778
|
+
if (this.bundledPackages.has(base)) return expr.field;
|
|
779
|
+
// Pointer types are wrapped as { value: T }; route through .value
|
|
780
|
+
if (expr.expr._type?.kind === "pointer" && expr.field !== "value") {
|
|
781
|
+
return `${base}.value.${expr.field}`;
|
|
782
|
+
}
|
|
783
|
+
return `${base}.${expr.field}`;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
case "IndexExpr": {
|
|
787
|
+
const base = this.genExpr(expr.expr);
|
|
788
|
+
const idx = this.genExpr(expr.index);
|
|
789
|
+
if (expr._mapValueType && !expr._lvalue) {
|
|
790
|
+
const zero = this.zeroValueForType(expr._mapValueType);
|
|
791
|
+
return `(${base}[${idx}] ?? ${zero})`;
|
|
792
|
+
}
|
|
793
|
+
return `${base}[${idx}]`;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
case "SliceExpr": {
|
|
797
|
+
const base = this.genExpr(expr.expr);
|
|
798
|
+
const lo = expr.low ? this.genExpr(expr.low) : "";
|
|
799
|
+
const hi = expr.high ? this.genExpr(expr.high) : "";
|
|
800
|
+
if (!lo && !hi) return `${base}.slice()`;
|
|
801
|
+
if (!hi) return `${base}.slice(${lo})`;
|
|
802
|
+
return `${base}.slice(${lo}, ${hi})`;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
case "CompositeLit":
|
|
806
|
+
return this.genCompositeLit(expr);
|
|
807
|
+
|
|
808
|
+
case "FuncLit": {
|
|
809
|
+
const params = expr.params.map((p) => p.name).join(", ");
|
|
810
|
+
const asyncPrefix = expr.async ? "async " : "";
|
|
811
|
+
const saved = this.out;
|
|
812
|
+
this.out = [];
|
|
813
|
+
this.indented(() => this._genBody(expr.body));
|
|
814
|
+
const body = this.out.join("\n");
|
|
815
|
+
this.out = saved;
|
|
816
|
+
return `${asyncPrefix}function(${params}) {\n${body}\n${" ".repeat(this.indent)}}`;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
case "TypeConversion": {
|
|
820
|
+
const inner = this.genExpr(expr.expr);
|
|
821
|
+
const target = expr.targetType?.name;
|
|
822
|
+
switch (target) {
|
|
823
|
+
case "string":
|
|
824
|
+
return `String(${inner})`;
|
|
825
|
+
case "int":
|
|
826
|
+
case "byte":
|
|
827
|
+
case "rune":
|
|
828
|
+
return `Math.trunc(Number(${inner}))`;
|
|
829
|
+
case "float64":
|
|
830
|
+
return `Number(${inner})`;
|
|
831
|
+
case "bool":
|
|
832
|
+
return `Boolean(${inner})`;
|
|
833
|
+
default:
|
|
834
|
+
return inner;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
case "TypeAssertExpr": {
|
|
839
|
+
const val = this.genExpr(expr.expr);
|
|
840
|
+
if (!expr._commaOk) return val; // unsafe assertion — just pass value through
|
|
841
|
+
// comma-ok: emit [value, runtimeTypeCheck]
|
|
842
|
+
const check = this._typeCheckExpr(expr.type, val);
|
|
843
|
+
return `[${val}, ${check}]`;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
case "AwaitExpr":
|
|
847
|
+
return `await ${this.genExpr(expr.expr)}`;
|
|
848
|
+
|
|
849
|
+
case "RangeExpr":
|
|
850
|
+
return this.genExpr(expr.expr);
|
|
851
|
+
|
|
852
|
+
default:
|
|
853
|
+
throw new Error(`CodeGen: unhandled expression kind '${expr.kind}'`);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
genCall(expr) {
|
|
858
|
+
// Handle built-ins that need special JS translation
|
|
859
|
+
if (expr.func.kind === "Ident") {
|
|
860
|
+
switch (expr.func.name) {
|
|
861
|
+
case "append":
|
|
862
|
+
return this.genAppend(expr);
|
|
863
|
+
case "len": {
|
|
864
|
+
const arg = expr.args[0];
|
|
865
|
+
const t = arg?._type;
|
|
866
|
+
const js = this.genExpr(arg);
|
|
867
|
+
if (t?.kind === "map") return `Object.keys(${js}).length`;
|
|
868
|
+
this._usesLen = true;
|
|
869
|
+
return `__len(${js})`;
|
|
870
|
+
}
|
|
871
|
+
case "cap":
|
|
872
|
+
return `${this.genExpr(expr.args[0])}.length`;
|
|
873
|
+
case "make":
|
|
874
|
+
return this.genMake(expr);
|
|
875
|
+
case "delete": {
|
|
876
|
+
const [m, k] = expr.args.map((a) => this.genExpr(a));
|
|
877
|
+
return `(delete ${m}[${k}])`;
|
|
878
|
+
}
|
|
879
|
+
case "copy": {
|
|
880
|
+
const [dst, src] = expr.args.map((a) => this.genExpr(a));
|
|
881
|
+
return `((__cd,__cs)=>{const n=Math.min(__cd.length,__cs.length);__cd.splice(0,n,...__cs.slice(0,n));return n;})(${dst},${src})`;
|
|
882
|
+
}
|
|
883
|
+
case "new": {
|
|
884
|
+
const zero = this.zeroValueForExpr(expr.args[0]);
|
|
885
|
+
return `{ value: ${zero} }`;
|
|
886
|
+
}
|
|
887
|
+
case "print":
|
|
888
|
+
case "println": {
|
|
889
|
+
const args = expr.args.map((a) => this.genExpr(a)).join(", ");
|
|
890
|
+
return `console.log(${args})`;
|
|
891
|
+
}
|
|
892
|
+
case "panic": {
|
|
893
|
+
const arg = this.genExpr(expr.args[0]);
|
|
894
|
+
return `(() => { throw new Error(${arg}); })()`;
|
|
895
|
+
}
|
|
896
|
+
case "error": {
|
|
897
|
+
// errors are plain strings; nil (null) means no error
|
|
898
|
+
const arg = this.genExpr(expr.args[0]);
|
|
899
|
+
return arg;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// error.Error() → the error string itself (errors are plain strings at runtime)
|
|
905
|
+
if (
|
|
906
|
+
expr.func.kind === "SelectorExpr" &&
|
|
907
|
+
expr.func.field === "Error" &&
|
|
908
|
+
expr.func.expr._type?.name === "error"
|
|
909
|
+
) {
|
|
910
|
+
return this.genExpr(expr.func.expr);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// fmt.Sprintf / fmt.Printf / fmt.Println / fmt.Print / fmt.Errorf
|
|
914
|
+
if (
|
|
915
|
+
expr.func.kind === "SelectorExpr" &&
|
|
916
|
+
expr.func.expr.kind === "Ident" &&
|
|
917
|
+
expr.func.expr.name === "fmt"
|
|
918
|
+
) {
|
|
919
|
+
const fmtArgs = expr.args.map((a) => this.genExpr(a)).join(", ");
|
|
920
|
+
switch (expr.func.field) {
|
|
921
|
+
case "Sprintf":
|
|
922
|
+
this._usesSprintf = true;
|
|
923
|
+
return `__sprintf(${fmtArgs})`;
|
|
924
|
+
case "Errorf":
|
|
925
|
+
this._usesSprintf = true;
|
|
926
|
+
return `__sprintf(${fmtArgs})`;
|
|
927
|
+
case "Printf":
|
|
928
|
+
this._usesSprintf = true;
|
|
929
|
+
return `process?.stdout?.write(__sprintf(${fmtArgs}))`;
|
|
930
|
+
case "Println":
|
|
931
|
+
this._usesSprintf = true;
|
|
932
|
+
return `console.log(__sprintf(${fmtArgs}))`;
|
|
933
|
+
case "Print":
|
|
934
|
+
this._usesSprintf = true;
|
|
935
|
+
return `process?.stdout?.write(__sprintf(${fmtArgs}))`;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
const fn = this.genExpr(expr.func);
|
|
940
|
+
const args = expr.args.map((a) => this.genExpr(a)).join(", ");
|
|
941
|
+
return `${fn}(${args})`;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
genAppend(expr) {
|
|
945
|
+
const slice = this.genExpr(expr.args[0]);
|
|
946
|
+
const elems = expr.args.slice(1).map((a) => this.genExpr(a));
|
|
947
|
+
if (elems.length === 0) return slice;
|
|
948
|
+
this._usesAppend = true;
|
|
949
|
+
return `__append(${slice}, ${elems.join(", ")})`;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
genMake(expr) {
|
|
953
|
+
// make([]T, n) or make([]T, n, cap) → new Array(n).fill(zero)
|
|
954
|
+
// make(map[K]V) → {}
|
|
955
|
+
const typeArg = expr.args[0];
|
|
956
|
+
const typeNode = typeArg.kind === "TypeExpr" ? typeArg.type : typeArg;
|
|
957
|
+
const resolvedKind = typeArg._type?.kind;
|
|
958
|
+
if (typeNode.kind === "SliceType" || resolvedKind === "slice") {
|
|
959
|
+
const n = expr.args[1] ? this.genExpr(expr.args[1]) : "0";
|
|
960
|
+
// Use the proper zero value for the element type (e.g. new Point() not 0)
|
|
961
|
+
const elemNode = typeNode.kind === "SliceType" ? typeNode.elem : null;
|
|
962
|
+
const elemResolved =
|
|
963
|
+
typeArg._type?.kind === "slice" ? typeArg._type.elem : null;
|
|
964
|
+
let zero = "null";
|
|
965
|
+
if (elemResolved) {
|
|
966
|
+
zero = this.zeroValueForType(elemResolved);
|
|
967
|
+
} else if (elemNode) {
|
|
968
|
+
zero = this.zeroValueForTypeNode(elemNode);
|
|
969
|
+
}
|
|
970
|
+
// If zero value is a constructor call, use a factory function so each element is distinct
|
|
971
|
+
if (zero.startsWith("new ")) {
|
|
972
|
+
return `Array.from({length: ${n}}, () => ${zero})`;
|
|
973
|
+
}
|
|
974
|
+
return `new Array(${n}).fill(${zero})`;
|
|
975
|
+
}
|
|
976
|
+
if (typeNode.kind === "MapType" || resolvedKind === "map") {
|
|
977
|
+
return "{}";
|
|
978
|
+
}
|
|
979
|
+
// Fallback
|
|
980
|
+
return "{}";
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// Render KeyValueExpr elements as a JS field list string.
|
|
984
|
+
// Handles embedded-spread (_isEmbedInit) and plain key: value pairs.
|
|
985
|
+
_genStructFields(elems) {
|
|
986
|
+
return elems
|
|
987
|
+
.filter((e) => e.kind === "KeyValueExpr")
|
|
988
|
+
.map((e) =>
|
|
989
|
+
e._isEmbedInit
|
|
990
|
+
? `...${this.genExpr(e.value)}`
|
|
991
|
+
: `${e.key.name ?? this.genExpr(e.key)}: ${this.genExpr(e.value)}`,
|
|
992
|
+
)
|
|
993
|
+
.join(", ");
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
genCompositeLit(expr) {
|
|
997
|
+
const t = expr.typeExpr;
|
|
998
|
+
|
|
999
|
+
// Implicit composite literal: {X: 1} inside a slice/map — type inferred from context
|
|
1000
|
+
if (t === null) {
|
|
1001
|
+
if (expr.elems.length > 0 && expr.elems[0]?.kind === "KeyValueExpr") {
|
|
1002
|
+
const typeName = expr._type?.name ?? expr._type?.underlying?.name;
|
|
1003
|
+
if (typeName && this.structNames.has(typeName)) {
|
|
1004
|
+
return `new ${typeName}({ ${this._genStructFields(expr.elems)} })`;
|
|
1005
|
+
}
|
|
1006
|
+
const fields = expr.elems
|
|
1007
|
+
.map(
|
|
1008
|
+
(e) =>
|
|
1009
|
+
`${e.key.name ?? this.genExpr(e.key)}: ${this.genExpr(e.value)}`,
|
|
1010
|
+
)
|
|
1011
|
+
.join(", ");
|
|
1012
|
+
return `{ ${fields} }`;
|
|
1013
|
+
}
|
|
1014
|
+
return `[${expr.elems.map((e) => this.genExpr(e)).join(", ")}]`;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
const typeName = this.getTypeName(t);
|
|
1018
|
+
|
|
1019
|
+
// Struct: new Foo({ X: 1, Y: 2 })
|
|
1020
|
+
if (typeName && this.structNames.has(typeName)) {
|
|
1021
|
+
return `new ${typeName}({ ${this._genStructFields(expr.elems)} })`;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// Slice/array: [1, 2, 3]
|
|
1025
|
+
if (t?.kind === "SliceType" || t?.kind === "ArrayType") {
|
|
1026
|
+
const elems = expr.elems
|
|
1027
|
+
.map((e) =>
|
|
1028
|
+
e.kind === "KeyValueExpr" ? this.genExpr(e.value) : this.genExpr(e),
|
|
1029
|
+
)
|
|
1030
|
+
.join(", ");
|
|
1031
|
+
return `[${elems}]`;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// Map: { key: value }
|
|
1035
|
+
if (t?.kind === "MapType") {
|
|
1036
|
+
const entries = expr.elems
|
|
1037
|
+
.map((e) => {
|
|
1038
|
+
if (e.kind === "KeyValueExpr") {
|
|
1039
|
+
const k =
|
|
1040
|
+
e.key.litKind === "STRING"
|
|
1041
|
+
? JSON.stringify(e.key.value)
|
|
1042
|
+
: `[${this.genExpr(e.key)}]`;
|
|
1043
|
+
return `${k}: ${this.genExpr(e.value)}`;
|
|
1044
|
+
}
|
|
1045
|
+
return this.genExpr(e);
|
|
1046
|
+
})
|
|
1047
|
+
.join(", ");
|
|
1048
|
+
return `{ ${entries} }`;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// Fallback: key-value pairs → plain object, positional → array
|
|
1052
|
+
if (expr.elems.length > 0 && expr.elems[0]?.kind === "KeyValueExpr") {
|
|
1053
|
+
const fields = expr.elems
|
|
1054
|
+
.map(
|
|
1055
|
+
(e) =>
|
|
1056
|
+
`${e.key.name ?? this.genExpr(e.key)}: ${this.genExpr(e.value)}`,
|
|
1057
|
+
)
|
|
1058
|
+
.join(", ");
|
|
1059
|
+
return `{ ${fields} }`;
|
|
1060
|
+
}
|
|
1061
|
+
return `[${expr.elems.map((e) => this.genExpr(e)).join(", ")}]`;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// ── Helpers ───────────────────────────────────────────────────
|
|
1065
|
+
|
|
1066
|
+
getTypeName(typeNode) {
|
|
1067
|
+
if (!typeNode) return null;
|
|
1068
|
+
if (typeNode.kind === "TypeName") return typeNode.name;
|
|
1069
|
+
if (typeNode.kind === "Ident") return typeNode.name;
|
|
1070
|
+
if (typeNode.kind === "SelectorExpr") return typeNode.field;
|
|
1071
|
+
return null;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
isIntType(t) {
|
|
1075
|
+
return t?.kind === "basic" && t.name === "int";
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
// Returns the JS zero-value literal for a basic type name, or null if not a basic type.
|
|
1079
|
+
_zeroForBasicName(name) {
|
|
1080
|
+
switch (name) {
|
|
1081
|
+
case "int":
|
|
1082
|
+
case "float64":
|
|
1083
|
+
case "byte":
|
|
1084
|
+
case "rune":
|
|
1085
|
+
return "0";
|
|
1086
|
+
case "string":
|
|
1087
|
+
return '""';
|
|
1088
|
+
case "bool":
|
|
1089
|
+
return "false";
|
|
1090
|
+
default:
|
|
1091
|
+
return null;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
zeroValueForTypeNode(typeNode) {
|
|
1096
|
+
if (!typeNode) return "null";
|
|
1097
|
+
switch (typeNode.kind) {
|
|
1098
|
+
case "TypeName": {
|
|
1099
|
+
const basic = this._zeroForBasicName(typeNode.name);
|
|
1100
|
+
if (basic !== null) return basic;
|
|
1101
|
+
if (this.structNames.has(typeNode.name))
|
|
1102
|
+
return `new ${typeNode.name}()`;
|
|
1103
|
+
return "null";
|
|
1104
|
+
}
|
|
1105
|
+
case "SliceType":
|
|
1106
|
+
return "null";
|
|
1107
|
+
case "ArrayType":
|
|
1108
|
+
return "[]";
|
|
1109
|
+
case "MapType":
|
|
1110
|
+
return "{}";
|
|
1111
|
+
case "PointerType":
|
|
1112
|
+
return "null";
|
|
1113
|
+
default:
|
|
1114
|
+
return "null";
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
// zeroValueForType operates on typechecker type objects (not AST type nodes).
|
|
1119
|
+
zeroValueForType(t) {
|
|
1120
|
+
if (!t) return "null";
|
|
1121
|
+
switch (t.kind) {
|
|
1122
|
+
case "basic": {
|
|
1123
|
+
const basic = this._zeroForBasicName(t.name);
|
|
1124
|
+
return basic !== null ? basic : "null";
|
|
1125
|
+
}
|
|
1126
|
+
case "slice":
|
|
1127
|
+
return "null";
|
|
1128
|
+
case "map":
|
|
1129
|
+
return "{}";
|
|
1130
|
+
case "named":
|
|
1131
|
+
if (this.structNames.has(t.name)) return `new ${t.name}()`;
|
|
1132
|
+
return "null";
|
|
1133
|
+
default:
|
|
1134
|
+
return "null";
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
// Emit a JS boolean expression that checks whether `val` matches type node `t`.
|
|
1139
|
+
_typeCheckExpr(typeNode, val) {
|
|
1140
|
+
if (!typeNode) return "true";
|
|
1141
|
+
if (typeNode.kind === "TypeName") {
|
|
1142
|
+
switch (typeNode.name) {
|
|
1143
|
+
case "int":
|
|
1144
|
+
case "float64":
|
|
1145
|
+
case "byte":
|
|
1146
|
+
case "rune":
|
|
1147
|
+
return `typeof ${val} === "number"`;
|
|
1148
|
+
case "string":
|
|
1149
|
+
return `typeof ${val} === "string"`;
|
|
1150
|
+
case "bool":
|
|
1151
|
+
return `typeof ${val} === "boolean"`;
|
|
1152
|
+
default:
|
|
1153
|
+
if (this.structNames.has(typeNode.name))
|
|
1154
|
+
return `${val} instanceof ${typeNode.name}`;
|
|
1155
|
+
return "true"; // unknown type — can't check at runtime
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
return "true";
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
zeroValueForExpr(expr) {
|
|
1162
|
+
// For new(T) calls
|
|
1163
|
+
if (expr.kind === "Ident") {
|
|
1164
|
+
switch (expr.name) {
|
|
1165
|
+
case "int":
|
|
1166
|
+
case "float64":
|
|
1167
|
+
return "0";
|
|
1168
|
+
case "string":
|
|
1169
|
+
return '""';
|
|
1170
|
+
case "bool":
|
|
1171
|
+
return "false";
|
|
1172
|
+
default:
|
|
1173
|
+
if (this.structNames.has(expr.name)) return `new ${expr.name}()`;
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
return "null";
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
typeComment(typeNode) {
|
|
1180
|
+
if (!typeNode) return "unknown";
|
|
1181
|
+
switch (typeNode.kind) {
|
|
1182
|
+
case "TypeName":
|
|
1183
|
+
return typeNode.name;
|
|
1184
|
+
case "SliceType":
|
|
1185
|
+
return `[]${this.typeComment(typeNode.elem)}`;
|
|
1186
|
+
case "MapType":
|
|
1187
|
+
return `map[${this.typeComment(typeNode.key)}]${this.typeComment(typeNode.value)}`;
|
|
1188
|
+
default:
|
|
1189
|
+
return typeNode.kind;
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
}
|