tova 0.3.4 → 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,610 @@
1
+ // WASM binary code generator for @wasm-annotated Tova functions
2
+ // Compiles a subset of Tova (numeric types, control flow, recursion) to WebAssembly binary format
3
+ // No external dependencies — generates WASM binary directly
4
+
5
+ // WASM type constants
6
+ const I32 = 0x7F;
7
+ const I64 = 0x7E;
8
+ const F64 = 0x7C;
9
+ const VOID = 0x40;
10
+ const FUNC_TYPE = 0x60;
11
+
12
+ // WASM section IDs
13
+ const SEC_TYPE = 1;
14
+ const SEC_FUNCTION = 3;
15
+ const SEC_EXPORT = 7;
16
+ const SEC_CODE = 10;
17
+
18
+ // WASM opcodes
19
+ const OP = {
20
+ unreachable: 0x00,
21
+ nop: 0x01,
22
+ block: 0x02,
23
+ loop: 0x03,
24
+ if: 0x04,
25
+ else: 0x05,
26
+ end: 0x0B,
27
+ br: 0x0C,
28
+ br_if: 0x0D,
29
+ return: 0x0F,
30
+ call: 0x10,
31
+ drop: 0x1A,
32
+ select: 0x1B,
33
+ local_get: 0x20,
34
+ local_set: 0x21,
35
+ local_tee: 0x22,
36
+ i32_const: 0x41,
37
+ i64_const: 0x42,
38
+ f64_const: 0x44,
39
+ i32_eqz: 0x45,
40
+ i32_eq: 0x46,
41
+ i32_ne: 0x47,
42
+ i32_lt_s: 0x48,
43
+ i32_gt_s: 0x4A,
44
+ i32_le_s: 0x4C,
45
+ i32_ge_s: 0x4E,
46
+ f64_eq: 0x61,
47
+ f64_ne: 0x62,
48
+ f64_lt: 0x63,
49
+ f64_gt: 0x64,
50
+ f64_le: 0x65,
51
+ f64_ge: 0x66,
52
+ i32_add: 0x6A,
53
+ i32_sub: 0x6B,
54
+ i32_mul: 0x6C,
55
+ i32_div_s: 0x6D,
56
+ i32_rem_s: 0x6F,
57
+ i32_and: 0x71,
58
+ i32_or: 0x72,
59
+ f64_neg: 0x9A,
60
+ f64_add: 0xA0,
61
+ f64_sub: 0xA1,
62
+ f64_mul: 0xA2,
63
+ f64_div: 0xA3,
64
+ f64_convert_i32_s: 0xB7,
65
+ i32_trunc_f64_s: 0xAA,
66
+ };
67
+
68
+ // LEB128 encoding
69
+ function uleb128(value) {
70
+ const r = [];
71
+ do {
72
+ let b = value & 0x7F;
73
+ value >>>= 7;
74
+ if (value !== 0) b |= 0x80;
75
+ r.push(b);
76
+ } while (value !== 0);
77
+ return r;
78
+ }
79
+
80
+ function sleb128(value) {
81
+ const r = [];
82
+ let more = true;
83
+ while (more) {
84
+ let b = value & 0x7F;
85
+ value >>= 7;
86
+ if ((value === 0 && (b & 0x40) === 0) || (value === -1 && (b & 0x40) !== 0)) {
87
+ more = false;
88
+ } else {
89
+ b |= 0x80;
90
+ }
91
+ r.push(b);
92
+ }
93
+ return r;
94
+ }
95
+
96
+ function encodeString(s) {
97
+ const bytes = new TextEncoder().encode(s);
98
+ return [...uleb128(bytes.length), ...bytes];
99
+ }
100
+
101
+ function encodeSection(id, contents) {
102
+ return [id, ...uleb128(contents.length), ...contents];
103
+ }
104
+
105
+ function encodeF64(value) {
106
+ const buf = new ArrayBuffer(8);
107
+ new Float64Array(buf)[0] = value;
108
+ return [...new Uint8Array(buf)];
109
+ }
110
+
111
+ // Map Tova type annotations to WASM types
112
+ function tovaTypeToWasm(typeStr) {
113
+ if (!typeStr) return I32;
114
+ const t = typeof typeStr === 'string' ? typeStr : (typeStr.name || typeStr.value || String(typeStr));
115
+ switch (t) {
116
+ case 'Int': case 'int': case 'i32': case 'Bool': case 'bool': return I32;
117
+ case 'Float': case 'float': case 'f64': case 'Number': return F64;
118
+ default: return I32;
119
+ }
120
+ }
121
+
122
+ // Compile a single @wasm function to WASM binary
123
+ export function compileWasmFunction(funcNode) {
124
+ const ctx = new WasmFuncContext(funcNode);
125
+ const bodyBytes = ctx.compile();
126
+ return buildModule(funcNode.name, ctx.paramTypes, ctx.returnType, ctx.localTypes, bodyBytes);
127
+ }
128
+
129
+ // Compile multiple @wasm functions into a single module
130
+ export function compileWasmModule(funcNodes) {
131
+ if (funcNodes.length === 1) return compileWasmFunction(funcNodes[0]);
132
+ const contexts = funcNodes.map(f => new WasmFuncContext(f));
133
+ const nameMap = {};
134
+ funcNodes.forEach((f, i) => { nameMap[f.name] = i; });
135
+ contexts.forEach(ctx => { ctx.funcNameMap = nameMap; });
136
+ const bodies = contexts.map(ctx => ctx.compile());
137
+ return buildMultiModule(funcNodes.map(f => f.name), contexts, bodies);
138
+ }
139
+
140
+ function buildModule(name, paramTypes, returnType, localTypes, bodyBytes) {
141
+ const typeSection = encodeSection(SEC_TYPE, [
142
+ ...uleb128(1), FUNC_TYPE,
143
+ ...uleb128(paramTypes.length), ...paramTypes,
144
+ ...(returnType !== null ? [1, returnType] : [0])
145
+ ]);
146
+ const funcSection = encodeSection(SEC_FUNCTION, [...uleb128(1), ...uleb128(0)]);
147
+ const exportSection = encodeSection(SEC_EXPORT, [
148
+ ...uleb128(1), ...encodeString(name), 0x00, ...uleb128(0),
149
+ ]);
150
+ const localDecls = encodeLocalDecls(localTypes);
151
+ const funcBody = [...localDecls, ...bodyBytes, OP.end];
152
+ const codeSection = encodeSection(SEC_CODE, [...uleb128(1), ...uleb128(funcBody.length), ...funcBody]);
153
+
154
+ return new Uint8Array([
155
+ 0x00, 0x61, 0x73, 0x6D, 0x01, 0x00, 0x00, 0x00,
156
+ ...typeSection, ...funcSection, ...exportSection, ...codeSection,
157
+ ]);
158
+ }
159
+
160
+ function buildMultiModule(names, contexts, bodies) {
161
+ const types = [];
162
+ for (const ctx of contexts) {
163
+ types.push(FUNC_TYPE, ...uleb128(ctx.paramTypes.length), ...ctx.paramTypes,
164
+ ...(ctx.returnType !== null ? [1, ctx.returnType] : [0]));
165
+ }
166
+ const typeSection = encodeSection(SEC_TYPE, [...uleb128(contexts.length), ...types]);
167
+ const funcSection = encodeSection(SEC_FUNCTION, [...uleb128(contexts.length), ...contexts.map((_, i) => uleb128(i)).flat()]);
168
+ const exports = [];
169
+ for (let i = 0; i < names.length; i++) exports.push(...encodeString(names[i]), 0x00, ...uleb128(i));
170
+ const exportSection = encodeSection(SEC_EXPORT, [...uleb128(names.length), ...exports]);
171
+ const funcBodies = [];
172
+ for (let i = 0; i < contexts.length; i++) {
173
+ const fb = [...encodeLocalDecls(contexts[i].localTypes), ...bodies[i], OP.end];
174
+ funcBodies.push(...uleb128(fb.length), ...fb);
175
+ }
176
+ const codeSection = encodeSection(SEC_CODE, [...uleb128(contexts.length), ...funcBodies]);
177
+ return new Uint8Array([
178
+ 0x00, 0x61, 0x73, 0x6D, 0x01, 0x00, 0x00, 0x00,
179
+ ...typeSection, ...funcSection, ...exportSection, ...codeSection,
180
+ ]);
181
+ }
182
+
183
+ function encodeLocalDecls(localTypes) {
184
+ if (localTypes.length === 0) return uleb128(0);
185
+ const groups = [];
186
+ let cur = localTypes[0], count = 1;
187
+ for (let i = 1; i < localTypes.length; i++) {
188
+ if (localTypes[i] === cur) { count++; }
189
+ else { groups.push([count, cur]); cur = localTypes[i]; count = 1; }
190
+ }
191
+ groups.push([count, cur]);
192
+ const r = [...uleb128(groups.length)];
193
+ for (const [cnt, typ] of groups) r.push(...uleb128(cnt), typ);
194
+ return r;
195
+ }
196
+
197
+ // ─── WASM Function Context ─────────────────────────────────
198
+
199
+ class WasmFuncContext {
200
+ constructor(funcNode) {
201
+ this.funcNode = funcNode;
202
+ this.name = funcNode.name;
203
+ this.locals = new Map(); // name -> local index
204
+ this.localTypes = []; // types of non-param locals
205
+ this.paramTypes = [];
206
+ this.returnType = null;
207
+ this.funcNameMap = { [funcNode.name]: 0 };
208
+ this.blockDepth = 0;
209
+
210
+ // Parse params — Tova AST: param.name, param.typeAnnotation
211
+ for (const p of funcNode.params) {
212
+ const pName = p.name || '_';
213
+ const wt = tovaTypeToWasm(p.typeAnnotation);
214
+ this.locals.set(pName, this.locals.size);
215
+ this.paramTypes.push(wt);
216
+ }
217
+
218
+ // Parse return type
219
+ this.returnType = funcNode.returnType ? tovaTypeToWasm(funcNode.returnType) : I32;
220
+ }
221
+
222
+ addLocal(name, wasmType) {
223
+ if (this.locals.has(name)) return this.locals.get(name);
224
+ const idx = this.locals.size;
225
+ this.locals.set(name, idx);
226
+ this.localTypes.push(wasmType || I32);
227
+ return idx;
228
+ }
229
+
230
+ getLocal(name) { return this.locals.get(name); }
231
+
232
+ typeOf(name) {
233
+ const idx = this.locals.get(name);
234
+ if (idx === undefined) return I32;
235
+ if (idx < this.paramTypes.length) return this.paramTypes[idx];
236
+ return this.localTypes[idx - this.paramTypes.length];
237
+ }
238
+
239
+ compile() {
240
+ const body = this.funcNode.body;
241
+ if (body.type === 'BlockStatement') return this.compileBlockAsValue(body);
242
+ return this.compileExpr(body);
243
+ }
244
+
245
+ // ─── Block compilation (implicit return from last expression) ───
246
+
247
+ compileBlockAsValue(block) {
248
+ const stmts = block.body || [];
249
+ if (stmts.length === 0) return this.defaultValue();
250
+
251
+ const bytes = [];
252
+ for (let i = 0; i < stmts.length; i++) {
253
+ const stmt = stmts[i];
254
+ const isLast = i === stmts.length - 1;
255
+
256
+ if (isLast) {
257
+ if (stmt.type === 'ExpressionStatement') {
258
+ bytes.push(...this.compileExpr(stmt.expression));
259
+ } else if (stmt.type === 'ReturnStatement') {
260
+ if (stmt.value) bytes.push(...this.compileExpr(stmt.value));
261
+ bytes.push(OP.return);
262
+ } else if (stmt.type === 'IfStatement') {
263
+ bytes.push(...this.compileIfExpr(stmt));
264
+ } else {
265
+ bytes.push(...this.compileStatement(stmt));
266
+ bytes.push(...this.defaultValue());
267
+ }
268
+ } else {
269
+ bytes.push(...this.compileStatement(stmt));
270
+ }
271
+ }
272
+ return bytes;
273
+ }
274
+
275
+ compileBlockValue(block) {
276
+ if (block.type === 'BlockStatement') {
277
+ const stmts = block.body || [];
278
+ if (stmts.length === 0) return this.defaultValue();
279
+ const bytes = [];
280
+ for (let i = 0; i < stmts.length - 1; i++) bytes.push(...this.compileStatement(stmts[i]));
281
+ const last = stmts[stmts.length - 1];
282
+ if (last.type === 'ExpressionStatement') {
283
+ bytes.push(...this.compileExpr(last.expression));
284
+ } else if (last.type === 'ReturnStatement') {
285
+ if (last.value) bytes.push(...this.compileExpr(last.value));
286
+ bytes.push(OP.return);
287
+ } else if (last.type === 'IfStatement') {
288
+ bytes.push(...this.compileIfExpr(last));
289
+ } else {
290
+ bytes.push(...this.compileStatement(last));
291
+ bytes.push(...this.defaultValue());
292
+ }
293
+ return bytes;
294
+ }
295
+ return this.compileExpr(block);
296
+ }
297
+
298
+ defaultValue() {
299
+ return this.returnType === F64 ? [OP.f64_const, ...encodeF64(0)] : [OP.i32_const, ...sleb128(0)];
300
+ }
301
+
302
+ // ─── Statement compilation ───
303
+
304
+ compileStatement(stmt) {
305
+ switch (stmt.type) {
306
+ case 'VarDeclaration': return this.compileVarDecl(stmt);
307
+ case 'Assignment': return this.compileAssignment(stmt);
308
+ case 'ExpressionStatement': {
309
+ const bytes = this.compileExpr(stmt.expression);
310
+ bytes.push(OP.drop);
311
+ return bytes;
312
+ }
313
+ case 'ReturnStatement': {
314
+ const bytes = [];
315
+ if (stmt.value) bytes.push(...this.compileExpr(stmt.value));
316
+ bytes.push(OP.return);
317
+ return bytes;
318
+ }
319
+ case 'IfStatement': return this.compileIfStmt(stmt);
320
+ case 'WhileStatement': return this.compileWhile(stmt);
321
+ default:
322
+ throw new Error(`@wasm: unsupported statement type '${stmt.type}'`);
323
+ }
324
+ }
325
+
326
+ // Tova VarDeclaration: { targets: [identifier], values: [expression] }
327
+ compileVarDecl(node) {
328
+ const bytes = [];
329
+ const targets = node.targets || [];
330
+ const values = node.values || [];
331
+ for (let i = 0; i < targets.length; i++) {
332
+ const name = typeof targets[i] === 'string' ? targets[i] : targets[i].name;
333
+ const init = values[i];
334
+ let wt = I32;
335
+ if (init) wt = this.inferType(init);
336
+ const idx = this.addLocal(name, wt);
337
+ if (init) {
338
+ bytes.push(...this.compileExpr(init));
339
+ bytes.push(OP.local_set, ...uleb128(idx));
340
+ }
341
+ }
342
+ return bytes;
343
+ }
344
+
345
+ // Tova Assignment: { targets: [identifier/expr], values: [expression] }
346
+ compileAssignment(node) {
347
+ const bytes = [];
348
+ const targets = node.targets || [];
349
+ const values = node.values || [];
350
+ for (let i = 0; i < targets.length; i++) {
351
+ const target = targets[i];
352
+ const name = typeof target === 'string' ? target : target.name;
353
+ if (!name) throw new Error('@wasm: assignment target must be a simple identifier');
354
+ let idx = this.getLocal(name);
355
+ if (idx === undefined) {
356
+ // Implicit variable declaration (Tova allows `x = 5` without `var`)
357
+ const wt = values[i] ? this.inferType(values[i]) : I32;
358
+ idx = this.addLocal(name, wt);
359
+ }
360
+ bytes.push(...this.compileExpr(values[i]));
361
+ bytes.push(OP.local_set, ...uleb128(idx));
362
+ }
363
+ return bytes;
364
+ }
365
+
366
+ // ─── If statement (void) ───
367
+
368
+ compileIfStmt(node) {
369
+ const bytes = [];
370
+ bytes.push(...this.compileExpr(node.condition));
371
+ bytes.push(OP.if, VOID);
372
+ if (node.consequent) {
373
+ const stmts = node.consequent.body || [node.consequent];
374
+ for (const s of stmts) bytes.push(...this.compileStatement(s));
375
+ }
376
+ // Handle elif chains
377
+ if (node.alternates && node.alternates.length > 0) {
378
+ for (const alt of node.alternates) {
379
+ bytes.push(OP.else);
380
+ bytes.push(...this.compileExpr(alt.condition));
381
+ bytes.push(OP.if, VOID);
382
+ const altStmts = alt.body.body || [alt.body];
383
+ for (const s of altStmts) bytes.push(...this.compileStatement(s));
384
+ }
385
+ // Close all elif if-blocks
386
+ if (node.elseBody) {
387
+ bytes.push(OP.else);
388
+ const elseStmts = node.elseBody.body || [node.elseBody];
389
+ for (const s of elseStmts) bytes.push(...this.compileStatement(s));
390
+ }
391
+ for (let i = 0; i < node.alternates.length; i++) bytes.push(OP.end);
392
+ } else if (node.elseBody) {
393
+ bytes.push(OP.else);
394
+ const elseStmts = node.elseBody.body || [node.elseBody];
395
+ for (const s of elseStmts) bytes.push(...this.compileStatement(s));
396
+ }
397
+ bytes.push(OP.end);
398
+ return bytes;
399
+ }
400
+
401
+ // ─── If expression (returns a value) ───
402
+
403
+ compileIfExpr(node) {
404
+ const bytes = [];
405
+ bytes.push(...this.compileExpr(node.condition));
406
+ bytes.push(OP.if, this.returnType);
407
+
408
+ // Then branch
409
+ if (node.consequent) {
410
+ bytes.push(...this.compileBlockValue(node.consequent));
411
+ } else {
412
+ bytes.push(...this.defaultValue());
413
+ }
414
+
415
+ // Handle elif chains
416
+ if (node.alternates && node.alternates.length > 0) {
417
+ for (const alt of node.alternates) {
418
+ bytes.push(OP.else);
419
+ bytes.push(...this.compileExpr(alt.condition));
420
+ bytes.push(OP.if, this.returnType);
421
+ bytes.push(...this.compileBlockValue(alt.body));
422
+ }
423
+ // Final else
424
+ bytes.push(OP.else);
425
+ if (node.elseBody) {
426
+ bytes.push(...this.compileBlockValue(node.elseBody));
427
+ } else {
428
+ bytes.push(...this.defaultValue());
429
+ }
430
+ // Close all elif if-blocks
431
+ for (let i = 0; i < node.alternates.length; i++) bytes.push(OP.end);
432
+ } else {
433
+ // Simple if/else
434
+ bytes.push(OP.else);
435
+ if (node.elseBody) {
436
+ bytes.push(...this.compileBlockValue(node.elseBody));
437
+ } else {
438
+ bytes.push(...this.defaultValue());
439
+ }
440
+ }
441
+
442
+ bytes.push(OP.end);
443
+ return bytes;
444
+ }
445
+
446
+ // ─── While loop ───
447
+
448
+ compileWhile(node) {
449
+ const bytes = [];
450
+ bytes.push(OP.block, VOID);
451
+ bytes.push(OP.loop, VOID);
452
+ this.blockDepth += 2;
453
+
454
+ bytes.push(...this.compileExpr(node.condition));
455
+ bytes.push(OP.i32_eqz);
456
+ bytes.push(OP.br_if, ...uleb128(1));
457
+
458
+ const bodyStmts = node.body.body || [node.body];
459
+ for (const s of bodyStmts) bytes.push(...this.compileStatement(s));
460
+
461
+ bytes.push(OP.br, ...uleb128(0));
462
+ bytes.push(OP.end);
463
+ bytes.push(OP.end);
464
+ this.blockDepth -= 2;
465
+ return bytes;
466
+ }
467
+
468
+ // ─── Expression compilation ───
469
+
470
+ compileExpr(node) {
471
+ switch (node.type) {
472
+ case 'NumberLiteral': return this.compileNumber(node);
473
+ case 'BooleanLiteral': return [OP.i32_const, ...sleb128(node.value ? 1 : 0)];
474
+ case 'Identifier': return this.compileIdentifier(node);
475
+ case 'BinaryExpression': return this.compileBinary(node);
476
+ case 'UnaryExpression': return this.compileUnary(node);
477
+ case 'CallExpression': return this.compileCall(node);
478
+ case 'IfStatement': return this.compileIfExpr(node);
479
+ case 'LogicalExpression': return this.compileLogical(node);
480
+ case 'BlockStatement': return this.compileBlockAsValue(node);
481
+ default:
482
+ throw new Error(`@wasm: unsupported expression type '${node.type}'`);
483
+ }
484
+ }
485
+
486
+ compileNumber(node) {
487
+ const val = node.value;
488
+ if (Number.isInteger(val) && val >= -2147483648 && val <= 2147483647) {
489
+ return [OP.i32_const, ...sleb128(val)];
490
+ }
491
+ return [OP.f64_const, ...encodeF64(val)];
492
+ }
493
+
494
+ compileIdentifier(node) {
495
+ const name = node.name;
496
+ const idx = this.getLocal(name);
497
+ if (idx === undefined) throw new Error(`@wasm: undefined variable '${name}'`);
498
+ return [OP.local_get, ...uleb128(idx)];
499
+ }
500
+
501
+ compileBinary(node) {
502
+ const lt = this.inferType(node.left);
503
+ const rt = this.inferType(node.right);
504
+ const t = (lt === F64 || rt === F64) ? F64 : I32;
505
+
506
+ const bytes = [];
507
+ bytes.push(...this.compileExpr(node.left));
508
+ if (t === F64 && lt === I32) bytes.push(OP.f64_convert_i32_s);
509
+ bytes.push(...this.compileExpr(node.right));
510
+ if (t === F64 && rt === I32) bytes.push(OP.f64_convert_i32_s);
511
+
512
+ switch (node.operator) {
513
+ case '+': bytes.push(t === F64 ? OP.f64_add : OP.i32_add); break;
514
+ case '-': bytes.push(t === F64 ? OP.f64_sub : OP.i32_sub); break;
515
+ case '*': bytes.push(t === F64 ? OP.f64_mul : OP.i32_mul); break;
516
+ case '/': bytes.push(t === F64 ? OP.f64_div : OP.i32_div_s); break;
517
+ case '%': bytes.push(OP.i32_rem_s); break;
518
+ case '==': bytes.push(t === F64 ? OP.f64_eq : OP.i32_eq); break;
519
+ case '!=': bytes.push(t === F64 ? OP.f64_ne : OP.i32_ne); break;
520
+ case '<': bytes.push(t === F64 ? OP.f64_lt : OP.i32_lt_s); break;
521
+ case '>': bytes.push(t === F64 ? OP.f64_gt : OP.i32_gt_s); break;
522
+ case '<=': bytes.push(t === F64 ? OP.f64_le : OP.i32_le_s); break;
523
+ case '>=': bytes.push(t === F64 ? OP.f64_ge : OP.i32_ge_s); break;
524
+ default:
525
+ throw new Error(`@wasm: unsupported binary operator '${node.operator}'`);
526
+ }
527
+ return bytes;
528
+ }
529
+
530
+ compileUnary(node) {
531
+ switch (node.operator) {
532
+ case '-': {
533
+ const t = this.inferType(node.operand);
534
+ if (t === F64) {
535
+ return [...this.compileExpr(node.operand), OP.f64_neg];
536
+ }
537
+ return [OP.i32_const, ...sleb128(0), ...this.compileExpr(node.operand), OP.i32_sub];
538
+ }
539
+ case 'not': case '!':
540
+ return [...this.compileExpr(node.operand), OP.i32_eqz];
541
+ default:
542
+ throw new Error(`@wasm: unsupported unary operator '${node.operator}'`);
543
+ }
544
+ }
545
+
546
+ compileCall(node) {
547
+ const name = node.callee.name;
548
+ if (!name) throw new Error('@wasm: only direct function calls are supported');
549
+ const funcIdx = this.funcNameMap[name];
550
+ if (funcIdx === undefined) throw new Error(`@wasm: undefined function '${name}'`);
551
+ const bytes = [];
552
+ for (const arg of node.arguments) bytes.push(...this.compileExpr(arg));
553
+ bytes.push(OP.call, ...uleb128(funcIdx));
554
+ return bytes;
555
+ }
556
+
557
+ compileLogical(node) {
558
+ const bytes = [];
559
+ if (node.operator === 'and' || node.operator === '&&') {
560
+ bytes.push(...this.compileExpr(node.left));
561
+ bytes.push(OP.if, I32);
562
+ bytes.push(...this.compileExpr(node.right));
563
+ bytes.push(OP.else, OP.i32_const, ...sleb128(0));
564
+ bytes.push(OP.end);
565
+ } else {
566
+ bytes.push(...this.compileExpr(node.left));
567
+ bytes.push(OP.if, I32);
568
+ bytes.push(OP.i32_const, ...sleb128(1));
569
+ bytes.push(OP.else);
570
+ bytes.push(...this.compileExpr(node.right));
571
+ bytes.push(OP.end);
572
+ }
573
+ return bytes;
574
+ }
575
+
576
+ // ─── Type inference ───
577
+
578
+ inferType(node) {
579
+ if (!node) return I32;
580
+ switch (node.type) {
581
+ case 'NumberLiteral':
582
+ return (Number.isInteger(node.value) && node.value >= -2147483648 && node.value <= 2147483647) ? I32 : F64;
583
+ case 'BooleanLiteral': return I32;
584
+ case 'Identifier': return this.typeOf(node.name);
585
+ case 'BinaryExpression': {
586
+ if (['==', '!=', '<', '>', '<=', '>='].includes(node.operator)) return I32;
587
+ const lt = this.inferType(node.left);
588
+ const rt = this.inferType(node.right);
589
+ return (lt === F64 || rt === F64) ? F64 : I32;
590
+ }
591
+ case 'UnaryExpression': return this.inferType(node.operand);
592
+ case 'CallExpression': return this.returnType || I32;
593
+ default: return I32;
594
+ }
595
+ }
596
+ }
597
+
598
+ // Generate JS glue code for a @wasm function
599
+ export function generateWasmGlue(funcNode, wasmBytes) {
600
+ const bytesStr = Array.from(wasmBytes).join(',');
601
+ const name = funcNode.name;
602
+ return `const ${name} = new WebAssembly.Instance(new WebAssembly.Module(new Uint8Array([${bytesStr}]))).exports.${name};`;
603
+ }
604
+
605
+ // Generate JS glue code for a multi-function WASM module
606
+ export function generateMultiWasmGlue(funcNodes, wasmBytes) {
607
+ const bytesStr = Array.from(wasmBytes).join(',');
608
+ const names = funcNodes.map(f => f.name);
609
+ return `const { ${names.join(', ')} } = new WebAssembly.Instance(new WebAssembly.Module(new Uint8Array([${bytesStr}]))).exports;`;
610
+ }