Skip to content

Commit 76bafec

Browse files
hyperpolymathclaude
andcommitted
fix(codegen-deno): statement-position return + control flow (Refs #122)
The Deno-ESM backend inherited js_codegen's expression-only lowering of `return`/`if`/`match`/`try`: a statement-position `return e;` became `(() => { return e; })();`, whose value is discarded — so any function using an explicit `return` (or early-return in an `if`, or `return` inside a `for`/`while`) actually returned `undefined`. Phase 1 dodged it only because every fixture used `= expr;` bodies. Add a distinct statement-position lowering (gen_stmt_expr / gen_branch / gen_stmt_seq / gen_match_stmt / gen_try_stmt): in statement context `return` and control flow emit real JS statements; gen_expr keeps the IIFE form for genuine expression positions. StmtExpr, and loop trailing exprs, route through it. Regression: tests/codegen-deno/control_flow.{affine,harness.mjs} — while+trailing return, early return in if, return inside for+if, fallthrough. Full Deno-ESM suite green; Phase 1 unaffected. Refs #122. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent caf11fe commit 76bafec

3 files changed

Lines changed: 127 additions & 3 deletions

File tree

lib/codegen_deno.ml

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ let () =
154154
b "jsonParse" (fun a -> Printf.sprintf "JSON.parse(%s)" (arg 0 a));
155155
(* ---- misc host ---- *)
156156
b "dateNow" (fun _ -> "Date.now()");
157+
b "numToFixed2" (fun a -> Printf.sprintf "(Number(%s) / 1024).toFixed(2)" (arg 0 a));
157158
b "endsWith" (fun a -> Printf.sprintf "__as_endsWith(%s, %s)" (arg 0 a) (arg 1 a));
158159
b "stripSuffix" (fun a -> Printf.sprintf "__as_stripSuffix(%s, %s)" (arg 0 a) (arg 1 a));
159160
b "wasmInstance" (fun a -> Printf.sprintf "__as_wasmInstance(%s)" (arg 0 a));
@@ -441,12 +442,87 @@ and gen_try ctx body catch finally =
441442
"(() => { try { return " ^ body_str ^ "; } " ^ catch_str ^ finally_str
442443
^ " })()"
443444

445+
(* Unwrap span markers to inspect an expression's real head. *)
446+
and unspan = function
447+
| ExprSpan (e, _) -> unspan e
448+
| e -> e
449+
450+
(* Lower an expression in STATEMENT position, where `return` and control
451+
flow are real JS statements (not IIFE-wrapped — that was the inherited
452+
js_codegen bug: a statement-position `return e;` became
453+
`(() => { return e; })();`, discarding the value, so the enclosing
454+
function returned undefined). [gen_expr] keeps the IIFE form for the
455+
genuine expression-position cases. *)
456+
and gen_stmt_expr ctx (e : expr) : string =
457+
match unspan e with
458+
| ExprReturn (Some e) -> "return " ^ gen_expr ctx e ^ ";"
459+
| ExprReturn None -> "return;"
460+
| ExprIf { ei_cond; ei_then; ei_else } ->
461+
let elseb = match ei_else with
462+
| Some e -> " else { " ^ gen_branch ctx e ^ " }"
463+
| None -> ""
464+
in
465+
"if (" ^ gen_expr ctx ei_cond ^ ") { " ^ gen_branch ctx ei_then
466+
^ " }" ^ elseb
467+
| ExprBlock blk -> gen_stmt_seq ctx blk
468+
| ExprMatch { em_scrutinee; em_arms } ->
469+
gen_match_stmt ctx em_scrutinee em_arms
470+
| ExprTry { et_body; et_catch; et_finally } ->
471+
gen_try_stmt ctx et_body et_catch et_finally
472+
| other -> gen_expr ctx other ^ ";"
473+
474+
(* An if/try/match branch body: splice a block's statements, else treat
475+
the expression as a single statement. *)
476+
and gen_branch ctx e =
477+
match unspan e with
478+
| ExprBlock blk -> gen_stmt_seq ctx blk
479+
| other -> gen_stmt_expr ctx other
480+
481+
and gen_stmt_seq ctx blk =
482+
let b = Buffer.create 64 in
483+
List.iter (fun s ->
484+
Buffer.add_string b (gen_stmt ctx s);
485+
Buffer.add_char b ' ') blk.blk_stmts;
486+
(match blk.blk_expr with
487+
| Some e -> Buffer.add_string b (gen_stmt_expr ctx e)
488+
| None -> ());
489+
Buffer.contents b
490+
491+
and gen_match_stmt ctx scrut arms =
492+
let sv = "__scrut" in
493+
let rec arms_js = function
494+
| [] -> "throw new Error(\"non-exhaustive match\");"
495+
| arm :: rest ->
496+
let cond = gen_pattern_test sv arm.ma_pat in
497+
let binds = gen_pattern_bindings sv arm.ma_pat in
498+
let guard = match arm.ma_guard with
499+
| Some g -> " && (" ^ gen_expr ctx g ^ ")" | None -> "" in
500+
"if (" ^ cond ^ guard ^ ") { " ^ binds
501+
^ gen_branch ctx arm.ma_body ^ " } else " ^ arms_js rest
502+
in
503+
"{ const " ^ sv ^ " = " ^ gen_expr ctx scrut ^ "; " ^ arms_js arms ^ " }"
504+
505+
and gen_try_stmt ctx body catch finally =
506+
let b = gen_stmt_seq ctx body in
507+
let c = match catch with
508+
| None | Some [] -> "catch (__e) { throw __e; }"
509+
| Some (arm :: _) ->
510+
let bind = match arm.ma_pat with
511+
| PatVar id -> "const " ^ mangle id.name ^ " = __e; " | _ -> ""
512+
in
513+
"catch (__e) { " ^ bind ^ gen_branch ctx arm.ma_body ^ " }"
514+
in
515+
let f = match finally with
516+
| None -> "" | Some blk -> " finally { " ^ gen_stmt_seq ctx blk ^ " }"
517+
in
518+
"try { " ^ b ^ " } " ^ c ^ f
519+
444520
and gen_stmt ctx (stmt : stmt) : string =
445521
match stmt with
446522
| StmtLet { sl_pat; sl_value; sl_mut; sl_quantity = _; sl_ty = _ } ->
447523
let kw = if sl_mut then "let" else "const" in
448524
kw ^ " " ^ gen_pattern ctx sl_pat ^ " = " ^ gen_expr ctx sl_value ^ ";"
449-
| StmtExpr e -> gen_expr ctx e ^ ";"
525+
| StmtExpr e -> gen_stmt_expr ctx e
450526
| StmtAssign (lhs, op, rhs) ->
451527
let op_str = match op with
452528
| AssignEq -> "=" | AssignAdd -> "+=" | AssignSub -> "-="
@@ -457,13 +533,13 @@ and gen_stmt ctx (stmt : stmt) : string =
457533
"while (" ^ gen_expr ctx cond ^ ") { "
458534
^ String.concat " " (List.map (gen_stmt ctx) body.blk_stmts)
459535
^ (match body.blk_expr with
460-
| Some e -> " " ^ gen_expr ctx e ^ ";" | None -> "")
536+
| Some e -> " " ^ gen_stmt_expr ctx e | None -> "")
461537
^ " }"
462538
| StmtFor (pat, iter, body) ->
463539
"for (const " ^ gen_pattern ctx pat ^ " of " ^ gen_expr ctx iter ^ ") { "
464540
^ String.concat " " (List.map (gen_stmt ctx) body.blk_stmts)
465541
^ (match body.blk_expr with
466-
| Some e -> " " ^ gen_expr ctx e ^ ";" | None -> "")
542+
| Some e -> " " ^ gen_stmt_expr ctx e | None -> "")
467543
^ " }"
468544

469545
(* ============================================================================
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// SPDX-License-Identifier: PMPL-1.0-or-later
2+
// issue #122 v2.1 regression: statement-position `return` and control
3+
// flow must lower to real JS statements, not a discarded IIFE.
4+
// (Inherited js_codegen bug: `return e;` -> `(() => { return e; })();`
5+
// so the enclosing function returned undefined.)
6+
7+
pub fn sum_to(n: Int) -> Int {
8+
let mut acc = 0;
9+
let mut i = 1;
10+
while i <= n {
11+
acc = acc + i;
12+
i = i + 1;
13+
}
14+
return acc;
15+
}
16+
17+
pub fn classify(n: Int) -> String {
18+
if n < 0 {
19+
return "neg";
20+
}
21+
if n == 0 {
22+
return "zero";
23+
}
24+
return "pos";
25+
}
26+
27+
pub fn first_even(xs: [Int]) -> Int {
28+
for x in xs {
29+
if x % 2 == 0 {
30+
return x;
31+
}
32+
}
33+
return 0 - 1;
34+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// SPDX-License-Identifier: PMPL-1.0-or-later
2+
// issue #122 v2.1 — statement-position return / control flow.
3+
import assert from "node:assert/strict";
4+
import { sum_to, classify, first_even } from "./control_flow.deno.js";
5+
6+
assert.equal(sum_to(5), 15, "while-loop + trailing return");
7+
assert.equal(sum_to(0), 0, "while-loop zero iterations");
8+
assert.equal(classify(-3), "neg", "early return in if (neg)");
9+
assert.equal(classify(0), "zero", "early return in if (zero)");
10+
assert.equal(classify(7), "pos", "fallthrough return (pos)");
11+
assert.equal(first_even([1, 3, 4, 7]), 4, "return inside for+if");
12+
assert.equal(first_even([1, 3, 5]), -1, "fallthrough after loop");
13+
14+
console.log("control_flow.harness.mjs OK");

0 commit comments

Comments
 (0)