-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathjs_codegen.ml
More file actions
610 lines (572 loc) · 24.5 KB
/
Copy pathjs_codegen.ml
File metadata and controls
610 lines (572 loc) · 24.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
(* SPDX-License-Identifier: MPL-2.0 *)
(* SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell *)
(** JavaScript Code Generator (MVP).
Translates AffineScript AST to ES2020 JavaScript source code.
Targets Deno/Node — no Web Audio, no DOM. Output is a single self-contained
file that includes a minimal runtime prelude (Some/None/Ok/Err builders,
println). Effects are erased at this layer; IO operations call the prelude.
Phase 1 (this file): functions, arithmetic, control flow, let, tuples,
records, simple match, Option/Result constructors, try/catch. Sufficient
for the conformance subset that does not depend on ownership at runtime.
*)
open Ast
(* ============================================================================
Code Generation Context
============================================================================ *)
type codegen_ctx = {
output : Buffer.t;
indent : int;
symbols : Symbol.t;
in_function : bool;
(* #478: shared int-operand classifier. Lets us emit Math.trunc(a/b)
for Int/Int while leaving Float/Float as plain `/`. *)
idc : Int_div_classifier.state;
}
let create_ctx symbols = {
output = Buffer.create 1024;
indent = 0;
symbols;
in_function = false;
idc = Int_div_classifier.create ();
}
let emit ctx str =
Buffer.add_string ctx.output str
let emit_line ctx str =
let spaces = String.make (ctx.indent * 2) ' ' in
Buffer.add_string ctx.output spaces;
Buffer.add_string ctx.output str;
Buffer.add_char ctx.output '\n'
let increase_indent ctx = { ctx with indent = ctx.indent + 1 }
let decrease_indent ctx = { ctx with indent = max 0 (ctx.indent - 1) }
(* ============================================================================
Runtime prelude
Emitted once at the top of every output file. Keeps generated code free of
library dependencies — `deno run foo.js` or `node foo.js` is enough.
============================================================================ *)
let prelude = {|// ---- AffineScript JS runtime (MVP) ----
const Some = (value) => ({ tag: "Some", value });
const None = { tag: "None" };
const Ok = (value) => ({ tag: "Ok", value });
const Err = (error) => ({ tag: "Err", error });
const Unit = null;
const print = (s) => { (typeof Deno !== "undefined" ? Deno.stdout.writeSync(new TextEncoder().encode(String(s))) : process.stdout.write(String(s))); };
const println = (s) => { console.log(String(s)); };
// Synchronous line read. Buffers all of stdin on first call, then yields
// one line per invocation. Works under Node without await ceremony.
let __as_stdin_buf = null, __as_stdin_off = 0;
const read_line = () => {
if (__as_stdin_buf === null) {
try { __as_stdin_buf = require("fs").readFileSync(0, "utf8"); }
catch (_) { __as_stdin_buf = ""; }
}
const nl = __as_stdin_buf.indexOf("\n", __as_stdin_off);
if (nl < 0) { const r = __as_stdin_buf.slice(__as_stdin_off); __as_stdin_off = __as_stdin_buf.length; return r; }
const r = __as_stdin_buf.slice(__as_stdin_off, nl);
__as_stdin_off = nl + 1;
return r;
};
// ---- end runtime ----
|}
(* ============================================================================
Identifier sanitisation
AffineScript identifiers are mostly JS-safe. Reserved keywords are renamed
with a trailing underscore so generated code parses.
============================================================================ *)
let js_reserved = [
"abstract"; "arguments"; "await"; "boolean"; "break"; "byte"; "case";
"catch"; "char"; "class"; "const"; "continue"; "debugger"; "default";
"delete"; "do"; "double"; "else"; "enum"; "eval"; "export"; "extends";
"false"; "final"; "finally"; "float"; "for"; "function"; "goto"; "if";
"implements"; "import"; "in"; "instanceof"; "int"; "interface"; "let";
"long"; "native"; "new"; "null"; "package"; "private"; "protected";
"public"; "return"; "short"; "static"; "super"; "switch"; "synchronized";
"this"; "throw"; "throws"; "transient"; "true"; "try"; "typeof"; "var";
"void"; "volatile"; "while"; "with"; "yield";
]
let mangle (name : string) : string =
if List.mem name js_reserved then name ^ "_"
else name
(** Lower a UTF-8 byte string to a JS double-quoted literal that is
safe under strict-mode ESM.
OCaml's [String.escaped] emits non-ASCII bytes as [\NNN] *decimal*
sequences; JavaScript parses [\NNN] as *octal* escapes which strict
mode rejects ([SyntaxError: Octal escape sequences are not allowed
in strict mode]) and which would decode to wrong characters even
outside strict mode. This helper instead decodes the UTF-8 byte
sequence to code points and emits [\uXXXX] (BMP) or [\u{XXXXX}]
(non-BMP) Unicode escapes — accepted everywhere, no parser-mode
surprises, and preserves the original character. Closes #460. *)
let js_string_lit (s : string) : string =
let buf = Buffer.create (String.length s + 8) in
Buffer.add_char buf '"';
let n = String.length s in
let i = ref 0 in
while !i < n do
let b0 = Char.code s.[!i] in
if b0 < 0x80 then begin
(match Char.chr b0 with
| '\\' -> Buffer.add_string buf "\\\\"
| '"' -> Buffer.add_string buf "\\\""
| '\n' -> Buffer.add_string buf "\\n"
| '\r' -> Buffer.add_string buf "\\r"
| '\t' -> Buffer.add_string buf "\\t"
| c when b0 >= 0x20 && b0 <= 0x7E -> Buffer.add_char buf c
| _ -> Buffer.add_string buf (Printf.sprintf "\\x%02X" b0));
incr i
end else begin
let cp, len =
if b0 < 0xC0 then (b0, 1)
else if b0 < 0xE0 && !i + 1 < n then
let b1 = Char.code s.[!i + 1] in
(((b0 land 0x1F) lsl 6) lor (b1 land 0x3F), 2)
else if b0 < 0xF0 && !i + 2 < n then
let b1 = Char.code s.[!i + 1] in
let b2 = Char.code s.[!i + 2] in
(((b0 land 0x0F) lsl 12) lor ((b1 land 0x3F) lsl 6) lor (b2 land 0x3F), 3)
else if !i + 3 < n then
let b1 = Char.code s.[!i + 1] in
let b2 = Char.code s.[!i + 2] in
let b3 = Char.code s.[!i + 3] in
(((b0 land 0x07) lsl 18) lor ((b1 land 0x3F) lsl 12)
lor ((b2 land 0x3F) lsl 6) lor (b3 land 0x3F), 4)
else (b0, 1)
in
if cp <= 0xFFFF then
Buffer.add_string buf (Printf.sprintf "\\u%04X" cp)
else
Buffer.add_string buf (Printf.sprintf "\\u{%X}" cp);
i := !i + len
end
done;
Buffer.add_char buf '"';
Buffer.contents buf
(* ============================================================================
Expression Code Generation
============================================================================ *)
let rec gen_expr ctx (expr : expr) : string =
match expr with
| ExprLit lit -> gen_literal lit
| ExprVar name -> mangle name.name
| ExprApp (func, args) ->
let func_str = gen_expr ctx func in
let arg_strs = List.map (gen_expr ctx) args in
func_str ^ "(" ^ String.concat ", " arg_strs ^ ")"
(* #478: emit truncating divide for provably-Int operands. The
classifier is conservative — float `/` stays float. *)
| ExprBinary (e1, OpDiv, e2)
when Int_div_classifier.expr_is_int ctx.idc e1
&& Int_div_classifier.expr_is_int ctx.idc e2 ->
"Math.trunc((" ^ gen_expr ctx e1 ^ ") / (" ^ gen_expr ctx e2 ^ "))"
| ExprBinary (e1, op, e2) ->
let op_str = match op with
| OpAdd -> "+"
| OpSub -> "-"
| OpMul -> "*"
| OpDiv -> "/"
| OpMod -> "%"
| OpEq -> "==="
| OpNe -> "!=="
| OpLt -> "<"
| OpLe -> "<="
| OpGt -> ">"
| OpGe -> ">="
| OpAnd -> "&&"
| OpOr -> "||"
| OpBitAnd -> "&"
| OpBitOr -> "|"
| OpBitXor -> "^"
| OpShl -> "<<"
| OpShr -> ">>"
| OpConcat -> "+" (* JS string/array overload *)
in
"(" ^ gen_expr ctx e1 ^ " " ^ op_str ^ " " ^ gen_expr ctx e2 ^ ")"
| ExprUnary (op, e) ->
(match op with
| OpNeg -> "(-" ^ gen_expr ctx e ^ ")"
| OpNot -> "(!" ^ gen_expr ctx e ^ ")"
| OpBitNot -> "(~" ^ gen_expr ctx e ^ ")"
| OpRef -> "({ get: () => " ^ gen_expr ctx e ^ ", set: (_) => {} })"
| OpDeref -> "(" ^ gen_expr ctx e ^ ".get())")
| ExprIf { ei_cond; ei_then; ei_else } ->
let cond_str = gen_expr ctx ei_cond in
let then_str = gen_expr ctx ei_then in
let else_str = match ei_else with
| Some e -> gen_expr ctx e
| None -> "Unit"
in
"(" ^ cond_str ^ " ? " ^ then_str ^ " : " ^ else_str ^ ")"
| ExprLet { el_pat; el_value; el_body; el_mut; el_quantity = _; el_ty = _ } ->
let pat_str = gen_pattern ctx el_pat in
let val_str = gen_expr ctx el_value in
let kw = if el_mut then "let" else "const" in
(match el_body with
| Some body ->
let body_str = gen_expr ctx body in
"((() => { " ^ kw ^ " " ^ pat_str ^ " = " ^ val_str ^ "; return " ^
body_str ^ "; })())"
| None ->
(* Statement-position let folded into expression: emit IIFE returning Unit. *)
"((() => { " ^ kw ^ " " ^ pat_str ^ " = " ^ val_str ^ "; return Unit; })())")
| ExprTuple exprs ->
let expr_strs = List.map (gen_expr ctx) exprs in
"[" ^ String.concat ", " expr_strs ^ "]"
| ExprArray exprs ->
let expr_strs = List.map (gen_expr ctx) exprs in
"[" ^ String.concat ", " expr_strs ^ "]"
| ExprIndex (arr, idx) ->
gen_expr ctx arr ^ "[" ^ gen_expr ctx idx ^ "]"
| ExprTupleIndex (e, n) ->
gen_expr ctx e ^ "[" ^ string_of_int n ^ "]"
| ExprRecord { er_fields; er_spread } ->
let field_strs = List.map (fun (name, e_opt) ->
let val_str = match e_opt with
| Some e -> gen_expr ctx e
| None -> mangle name.name (* punning: { x } -> { x: x } *)
in
mangle name.name ^ ": " ^ val_str
) er_fields in
let spread_str = match er_spread with
| Some e -> "...(" ^ gen_expr ctx e ^ "), "
| None -> ""
in
"({ " ^ spread_str ^ String.concat ", " field_strs ^ " })"
| ExprField (record, field) ->
gen_expr ctx record ^ "." ^ mangle field.name
| ExprMatch { em_scrutinee; em_arms } ->
gen_match ctx em_scrutinee em_arms
| ExprBlock block ->
gen_block_expr ctx block
| ExprReturn (Some e) ->
"(() => { return " ^ gen_expr ctx e ^ "; })()"
| ExprReturn None ->
"(() => { return Unit; })()"
(* #459: see codegen_deno's matching comment. In statement position
`break`/`continue` lower to bare JS keywords; the expression-
position fallback uses an IIFE that can't actually escape the
loop, but legal AffineScript places these inside a loop body so
the statement path (gen_stmt) is what fires. *)
| ExprBreak _ -> "(() => { break; })()"
| ExprContinue _ -> "(() => { continue; })()"
| ExprLambda { elam_params; elam_body; elam_ret_ty = _ } ->
let param_strs = List.map (fun (p : param) -> mangle p.p_name.name) elam_params in
"((" ^ String.concat ", " param_strs ^ ") => " ^ gen_expr ctx elam_body ^ ")"
| ExprTry { et_body; et_catch; et_finally } ->
gen_try ctx et_body et_catch et_finally
| ExprVariant (ty, ctor) ->
(* `Type::Variant` — emit a tagged object factory. Special-cases for
the prelude builders so `Option::None` / `Result::Ok` map directly. *)
(match ty.name, ctor.name with
| _, "None" -> "None"
| _, "Some" -> "Some"
| _, "Ok" -> "Ok"
| _, "Err" -> "Err"
| _, name -> Printf.sprintf "({ tag: %S })" name)
| ExprSpan (inner, _) -> gen_expr ctx inner
| ExprRowRestrict (e, _field) -> gen_expr ctx e (* runtime no-op *)
| ExprHandle _ ->
(* Was: silent erasure (body-only, handler arms dropped) — wrong-value
miscompiles even for effects-free return arms (issue #555). Fail
loudly at compile time; `Failure` is converted to `Error` at this
backend's entry point. *)
failwith
"effect handler (handle { ... }) is not supported by the JS-text \
backend — handler arms cannot be dispatched (Refs #555); \
use `--interp` / `-i`"
| ExprResume _ ->
failwith
"`resume` is not supported by the JS-text backend — only valid \
inside a `handle` block (Refs #555); use `--interp` / `-i`"
| ExprUnsafe _ ->
"(() => { throw new Error('unsafe op not supported in JS backend'); })()"
and gen_literal (lit : literal) : string =
match lit with
| LitInt (n, _) -> string_of_int n
| LitFloat (f, _) ->
(* OCaml's string_of_float can produce "1." — patch to "1.0" so JS parses
identically and the output is stable. *)
let s = string_of_float f in
if String.length s > 0 && s.[String.length s - 1] = '.' then s ^ "0" else s
| LitBool (true, _) -> "true"
| LitBool (false, _) -> "false"
| LitString (s, _) -> js_string_lit s
| LitChar (c, _) -> js_string_lit (String.make 1 c)
| LitUnit _ -> "Unit"
and gen_pattern ctx (pat : pattern) : string =
(* Used in binder positions: let x = ..., function params, for x in ... *)
match pat with
| PatWildcard _ -> "_"
| PatVar name -> mangle name.name
| PatLit _ -> "_" (* Literal patterns can't bind; only meaningful in match *)
| PatTuple pats ->
let pat_strs = List.map (gen_pattern ctx) pats in
"[" ^ String.concat ", " pat_strs ^ "]"
| PatRecord (fields, _) ->
let strs = List.map (fun (n, sub) ->
match sub with
| None -> mangle n.name
| Some sub -> mangle n.name ^ ": " ^ gen_pattern ctx sub
) fields in
"{ " ^ String.concat ", " strs ^ " }"
| PatAs (id, _) -> mangle id.name
| PatCon (id, _) -> mangle id.name (* approximate *)
| PatOr (p, _) -> gen_pattern ctx p
and gen_match ctx scrutinee arms =
(* Lower `match` to an IIFE that destructures the scrutinee once and runs an
if-else cascade. Each arm gets a freshly-scoped block so binders don't
collide. *)
let scrutinee_str = gen_expr ctx scrutinee in
let scrut_var = "__scrut" in
let rec gen_arms = function
| [] ->
"throw new Error(\"non-exhaustive match\");"
| arm :: rest ->
let cond = gen_pattern_test scrut_var arm.ma_pat in
let bindings = gen_pattern_bindings scrut_var arm.ma_pat in
let guard = match arm.ma_guard with
| Some g -> " && (" ^ gen_expr ctx g ^ ")"
| None -> ""
in
let body = gen_expr ctx arm.ma_body in
let inner_bindings =
if bindings = "" then ""
else bindings ^ " "
in
let prefix =
(* When we have bindings, emit them inside the branch so they are
scoped to that arm and visible to the guard check. *)
if bindings = "" then
"if (" ^ cond ^ guard ^ ") { return " ^ body ^ "; }"
else if arm.ma_guard = None then
"if (" ^ cond ^ ") { " ^ inner_bindings ^ "return " ^ body ^ "; }"
else
"if (" ^ cond ^ ") { " ^ inner_bindings ^
"if (" ^ (match arm.ma_guard with Some g -> gen_expr ctx g | None -> "true") ^
") { return " ^ body ^ "; } }"
in
prefix ^ " " ^ gen_arms rest
in
"((" ^ scrut_var ^ ") => { " ^ gen_arms arms ^ " })(" ^ scrutinee_str ^ ")"
and gen_pattern_test scrut pat =
match pat with
| PatWildcard _ | PatVar _ -> "true"
| PatLit lit -> scrut ^ " === " ^ gen_literal lit
| PatCon (id, _) ->
(* Tagged-union variant: { tag: "Some", value: ... } *)
scrut ^ ".tag === " ^ Printf.sprintf "%S" id.name
| PatTuple pats ->
let conds = List.mapi (fun i p ->
gen_pattern_test (scrut ^ "[" ^ string_of_int i ^ "]") p
) pats in
String.concat " && " (("Array.isArray(" ^ scrut ^ ")") :: conds)
| PatRecord (fields, _) ->
let conds = List.map (fun (n, sub) ->
match sub with
| None -> "true"
| Some sub -> gen_pattern_test (scrut ^ "." ^ mangle n.name) sub
) fields in
String.concat " && " conds
| PatAs (_, p) -> gen_pattern_test scrut p
| PatOr (p1, p2) ->
"((" ^ gen_pattern_test scrut p1 ^ ") || (" ^ gen_pattern_test scrut p2 ^ "))"
and gen_pattern_bindings scrut pat =
(* Emit `const x = scrut.<path>;` declarations for every binder reachable
from this pattern, skipping the wildcard and literal cases. *)
let buf = Buffer.create 64 in
let rec walk path = function
| PatWildcard _ | PatLit _ -> ()
| PatVar id ->
Buffer.add_string buf
("const " ^ mangle id.name ^ " = " ^ path ^ "; ")
| PatTuple pats ->
List.iteri (fun i p ->
walk (path ^ "[" ^ string_of_int i ^ "]") p
) pats
| PatRecord (fields, _) ->
List.iter (fun (n, sub) ->
let sub_path = path ^ "." ^ mangle n.name in
match sub with
| None ->
Buffer.add_string buf
("const " ^ mangle n.name ^ " = " ^ sub_path ^ "; ")
| Some sub -> walk sub_path sub
) fields
| PatCon (_, args) ->
(* Convention: variant payload lives at .value (single-arg, like Some)
or .values[i] (multi-arg). Use .value for arity 1 to match prelude. *)
(match args with
| [] -> ()
| [single] -> walk (path ^ ".value") single
| many ->
List.iteri (fun i p ->
walk (path ^ ".values[" ^ string_of_int i ^ "]") p
) many)
| PatAs (id, sub) ->
Buffer.add_string buf ("const " ^ mangle id.name ^ " = " ^ path ^ "; ");
walk path sub
| PatOr (p, _) -> walk path p
in
walk scrut pat;
Buffer.contents buf
and gen_block_expr ctx block =
(* JS has no block-as-expression. Emit an IIFE; statements run in order, and
the trailing expression (if any) becomes the return value. *)
let body = Buffer.create 64 in
List.iter (fun s ->
Buffer.add_string body (gen_stmt ctx s);
Buffer.add_string body " "
) block.blk_stmts;
let result = match block.blk_expr with
| Some e -> "return " ^ gen_expr ctx e ^ ";"
| None -> "return Unit;"
in
"(() => { " ^ Buffer.contents body ^ result ^ " })()"
and gen_try ctx body catch finally =
let body_str = gen_block_expr ctx body in
let catch_str = match catch with
| None | Some [] -> "catch (__e) { throw __e; }"
| Some (arm :: _) ->
let bind = match arm.ma_pat with
| PatVar id -> "const " ^ mangle id.name ^ " = __e; "
| _ -> ""
in
"catch (__e) { " ^ bind ^ "return " ^ gen_expr ctx arm.ma_body ^ "; }"
in
let finally_str = match finally with
| None -> ""
| Some blk -> " finally { " ^ gen_block_expr ctx blk ^ "; }"
in
"(() => { try { return " ^ body_str ^ "; } " ^ catch_str ^ finally_str ^ " })()"
and gen_stmt ctx (stmt : stmt) : string =
match stmt with
| StmtLet { sl_pat; sl_value; sl_mut; sl_quantity = _; sl_ty = _ } ->
let pat_str = gen_pattern ctx sl_pat in
let val_str = gen_expr ctx sl_value in
let kw = if sl_mut then "let" else "const" in
(* #478: track int-ness of single-name let-bindings so `n / 2`
downstream lowers truncating-ly. *)
(match Int_div_classifier.pat_var_name sl_pat with
| Some n ->
Int_div_classifier.track_int_binding ctx.idc n sl_value;
Int_div_classifier.track_int_array_binding ctx.idc n sl_value
| None -> ());
kw ^ " " ^ pat_str ^ " = " ^ val_str ^ ";"
| StmtExpr e ->
gen_expr ctx e ^ ";"
| StmtAssign (lhs, op, rhs) ->
let op_str = match op with
| AssignEq -> "="
| AssignAdd -> "+="
| AssignSub -> "-="
| AssignMul -> "*="
| AssignDiv -> "/="
in
gen_expr ctx lhs ^ " " ^ op_str ^ " " ^ gen_expr ctx rhs ^ ";"
| StmtWhile (cond, body) ->
"while (" ^ gen_expr ctx cond ^ ") { " ^
String.concat " " (List.map (gen_stmt ctx) body.blk_stmts) ^
(match body.blk_expr with
| Some e -> " " ^ gen_expr ctx e ^ ";"
| None -> "") ^
" }"
| StmtFor (pat, iter, body) ->
"for (const " ^ gen_pattern ctx pat ^ " of " ^ gen_expr ctx iter ^ ") { " ^
String.concat " " (List.map (gen_stmt ctx) body.blk_stmts) ^
(match body.blk_expr with
| Some e -> " " ^ gen_expr ctx e ^ ";"
| None -> "") ^
" }"
(* ============================================================================
Top-Level Declaration Code Generation
============================================================================ *)
let gen_function ctx (fd : fn_decl) : unit =
let name = mangle fd.fd_name.name in
let param_strs = List.map (fun (p : param) -> mangle p.p_name.name) fd.fd_params in
let header = Printf.sprintf "function %s(%s) {" name (String.concat ", " param_strs) in
emit_line ctx header;
(* #478: reset int-var tables and seed from typed params before
emitting the body. *)
Int_div_classifier.enter_fn_scope ctx.idc fd.fd_params;
let ctx_body = increase_indent { ctx with in_function = true } in
(match fd.fd_body with
| FnExpr body_expr ->
emit_line ctx_body ("return " ^ gen_expr ctx_body body_expr ^ ";")
| FnBlock block ->
List.iter (fun s -> emit_line ctx_body (gen_stmt ctx_body s)) block.blk_stmts;
(match block.blk_expr with
| Some e -> emit_line ctx_body ("return " ^ gen_expr ctx_body e ^ ";")
| None -> ()));
emit_line (decrease_indent ctx_body) "}";
emit ctx "\n"
let gen_type_decl ctx (td : type_decl) : unit =
(* Phase 1: emit constructor factories for enum variants so pattern matches
and `Type::Variant` references both work. Structs and aliases are erased. *)
match td.td_body with
| TyEnum variants ->
List.iter (fun (vd : variant_decl) ->
let name = mangle vd.vd_name.name in
let arity = List.length vd.vd_fields in
if arity = 0 then
emit_line ctx (Printf.sprintf "const %s = { tag: \"%s\" };" name vd.vd_name.name)
else if arity = 1 then
emit_line ctx
(Printf.sprintf "const %s = (value) => ({ tag: \"%s\", value });"
name vd.vd_name.name)
else
let params = List.init arity (fun i -> "v" ^ string_of_int i) in
emit_line ctx
(Printf.sprintf "const %s = (%s) => ({ tag: \"%s\", values: [%s] });"
name (String.concat ", " params)
vd.vd_name.name (String.concat ", " params))
) variants;
emit ctx "\n"
| TyStruct _ | TyAlias _ ->
emit_line ctx (Printf.sprintf "// type %s (erased)" td.td_name.name)
let gen_top_level ctx (top : top_level) : unit =
match top with
| TopFn fd -> gen_function ctx fd
| TopType td -> gen_type_decl ctx td
| TopConst { tc_name; tc_value; _ } ->
emit_line ctx
(Printf.sprintf "const %s = %s;" (mangle tc_name.name)
(gen_expr ctx tc_value))
| TopEffect _ -> emit_line ctx "// effect declaration (erased)"
| TopTrait _ -> emit_line ctx "// trait declaration (erased)"
| TopImpl _ -> emit_line ctx "// impl block (erased)"
(* ============================================================================
Main Code Generation Entry Point
============================================================================ *)
let generate (program : program) (symbols : Symbol.t) : string =
let ctx = create_ctx symbols in
(* #478: register every Int-returning fn before emitting any body, so
a forward call (or recursive call) counts as an Int operand. *)
List.iter (function
| TopFn fd ->
(match fd.fd_ret_ty with
| Some t when Int_div_classifier.type_head_is_int t ->
Int_div_classifier.register_int_fn ctx.idc fd.fd_name.name
| _ -> ())
| TopImpl ib ->
List.iter (function
| ImplFn fd ->
(match fd.fd_ret_ty with
| Some t when Int_div_classifier.type_head_is_int t ->
Int_div_classifier.register_int_fn ctx.idc fd.fd_name.name
| _ -> ())
| ImplType _ -> ()) ib.ib_items
| _ -> ()) program.prog_decls;
emit_line ctx "// Generated by AffineScript compiler";
emit_line ctx "// SPDX-License-Identifier: MPL-2.0";
emit ctx prelude;
List.iter (gen_top_level ctx) program.prog_decls;
(* If a `main` function exists, invoke it so `node foo.js` actually runs. *)
let has_main = List.exists (function
| TopFn fd -> fd.fd_name.name = "main"
| _ -> false
) program.prog_decls in
if has_main then emit_line ctx "main();";
Buffer.contents ctx.output
let codegen_js (program : program) (symbols : Symbol.t) : (string, string) result =
try Ok (generate program symbols)
with
| Failure msg -> Error ("JS codegen error: " ^ msg)
| e -> Error ("JS codegen error: " ^ Printexc.to_string e)