Skip to content

Commit 5a3b569

Browse files
committed
compiler: string-wall slice 8b — type-directed string ++ lowering
The full fix the slice-8a guard (#575) stood in for. String ++ now lowers correctly AND completely to wasm (incl. pure variable-to-variable, which the syntactic guard could not reach). Channel (type-directed elaboration): - ast.ml: new ExprStringConcat of expr * expr (not produced by the parser). - typecheck.ml: synth records each ++ node it types as String concat, by physical identity (string_concat_sites); elaborate_string_concat rewrites exactly those nodes to ExprStringConcat. Physical-identity keying is sound because typecheck and codegen run over the same prog object (parse_with_face's lowered prog, shared by resolve/typecheck/codegen); ExprBinary carries no span and same-text ++ occurrences are value-equal, so == is the correct key. - bin/main.ml: the wasm path runs elaborate_string_concat after typecheck, before Opt.fold_constants_program. The interpreter and non-wasm backends keep the original prog (String ++ = ExprBinary _ OpConcat _), so the oracle is unchanged and only the wasm backend sees the new node. Lowering (codegen.ml): byte concat — allocate 4 + la + lb, write the length word, copy a's then b's bytes — mirroring the list-concat handler but with 1-byte elements and a single length word (instead of 4-byte i32 elements, which was exactly the bug: the list path copied a string's [len][utf8] as i32 elements, so "ab" ++ "cd" read byte 2 as the length word of "cd" = 2 instead of 'c' = 99). Effect parity (effect_sites.ml): ExprStringConcat recurses like ExprBinary and is NOT counted as an ExprApp call site, so effect-ordinals stay identical between the interpreter (which sees ExprBinary) and the wasm backend (which sees ExprStringConcat) — avoiding a #555-class desync. An intrinsic-call encoding (ExprApp "__string_concat") would have shifted the ordinals; the dedicated node avoids that. opt.ml folds its sub-expressions; interp.ml handles it defensively as ordinary String ++. The 8a guard is retained as a backstop: any String ++ reaching codegen un-elaborated still errors loudly rather than emitting garbage. Tests: tests/codegen/string_concat.{affine,mjs} — executable wasm parity, byte-exact via the slice-1 reader: the "ab" ++ "cd" byte-2 = 99 regression (was 2), the var-var case the guard could not catch, chained a ++ b ++ c, and empty operands; oracle 6513269. test/test_e2e.ml "E2E String-wall slice 8 guard" gains a lowers-after-elaboration case. Verified: full run_codegen_wasm_tests.sh green incl. list_concat + slices 1-7 + effect tests; string ++ correct in if/match/fn/nested contexts. Design + ledger: proposals/DESIGN-string-concat.adoc (8b LANDED), proposals/MIGRATION-PLAN.adoc. https://claude.ai/code/session_01WoKhFQePiRsAj7aqnxbG8s
1 parent 34987ab commit 5a3b569

13 files changed

Lines changed: 420 additions & 24 deletions

File tree

bin/main.ml

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -675,7 +675,11 @@ let compile_file face json wasm_gc vscode_ext vscode_adapter vscode_no_lc
675675
Affinescript.Wasm_gc_encode.write_gc_module_to_file output gc_module
676676
end else if Filename.check_suffix output ".cjs" then begin
677677
(* Issue #35 Phase 1: Node-CJS shim around the compiled wasm. *)
678-
let optimized_prog = Affinescript.Opt.fold_constants_program prog in
678+
(* String-wall slice 8b: rewrite String `++` to ExprStringConcat
679+
(byte-concat) for the wasm backend before const-folding. No-op
680+
unless typecheck recorded String-concat sites. *)
681+
let prog = Affinescript.Typecheck.elaborate_string_concat prog in
682+
let optimized_prog = Affinescript.Opt.fold_constants_program prog in
679683
match Affinescript.Codegen.generate_module ~loader optimized_prog with
680684
| Error e ->
681685
add { severity = Error; code = "E0810";
@@ -694,7 +698,11 @@ let compile_file face json wasm_gc vscode_ext vscode_adapter vscode_no_lc
694698
output_string oc cjs;
695699
close_out oc
696700
end else begin
697-
let optimized_prog = Affinescript.Opt.fold_constants_program prog in
701+
(* String-wall slice 8b: rewrite String `++` to ExprStringConcat
702+
(byte-concat) for the wasm backend before const-folding. No-op
703+
unless typecheck recorded String-concat sites. *)
704+
let prog = Affinescript.Typecheck.elaborate_string_concat prog in
705+
let optimized_prog = Affinescript.Opt.fold_constants_program prog in
698706
match Affinescript.Codegen.generate_module ~loader optimized_prog with
699707
| Error e ->
700708
add { severity = Error; code = "E0801";
@@ -908,7 +916,11 @@ let compile_file face json wasm_gc vscode_ext vscode_adapter vscode_no_lc
908916
`Ok ())
909917
else if Filename.check_suffix output ".cjs" then
910918
(* Issue #35 Phase 1: Node-CJS shim around the compiled wasm. *)
911-
let optimized_prog = Affinescript.Opt.fold_constants_program prog in
919+
(* String-wall slice 8b: rewrite String `++` to ExprStringConcat
920+
(byte-concat) for the wasm backend before const-folding. No-op
921+
unless typecheck recorded String-concat sites. *)
922+
let prog = Affinescript.Typecheck.elaborate_string_concat prog in
923+
let optimized_prog = Affinescript.Opt.fold_constants_program prog in
912924
(match Affinescript.Codegen.generate_module ~loader optimized_prog with
913925
| Error e ->
914926
Format.eprintf "@[<v>Node-CJS codegen error: %s@]@."
@@ -929,7 +941,11 @@ let compile_file face json wasm_gc vscode_ext vscode_adapter vscode_no_lc
929941
(if vscode_ext then ", --vscode-extension" else "");
930942
`Ok ())
931943
else
932-
let optimized_prog = Affinescript.Opt.fold_constants_program prog in
944+
(* String-wall slice 8b: rewrite String `++` to ExprStringConcat
945+
(byte-concat) for the wasm backend before const-folding. No-op
946+
unless typecheck recorded String-concat sites. *)
947+
let prog = Affinescript.Typecheck.elaborate_string_concat prog in
948+
let optimized_prog = Affinescript.Opt.fold_constants_program prog in
933949
(match Affinescript.Codegen.generate_module ~loader optimized_prog with
934950
| Error e ->
935951
Format.eprintf "@[<v>Code generation error: %s@]@."
@@ -1086,6 +1102,10 @@ let compile_to_wasm_module face path
10861102
(Affinescript.Face.format_quantity_error face err);
10871103
Error "Quantity error"
10881104
| Ok () ->
1105+
(* String-wall slice 8b: rewrite String `++` to ExprStringConcat
1106+
(byte-concat) for the wasm backend before const-folding. No-op
1107+
unless typecheck recorded String-concat sites. *)
1108+
let prog = Affinescript.Typecheck.elaborate_string_concat prog in
10891109
let optimized_prog = Affinescript.Opt.fold_constants_program prog in
10901110
(match Affinescript.Codegen.generate_module ~loader optimized_prog with
10911111
| Error e ->
@@ -1146,6 +1166,10 @@ let verify_file face path =
11461166
(Affinescript.Face.format_quantity_error face err);
11471167
`Error (false, "Quantity error")
11481168
| Ok () ->
1169+
(* String-wall slice 8b: rewrite String `++` to ExprStringConcat
1170+
(byte-concat) for the wasm backend before const-folding. No-op
1171+
unless typecheck recorded String-concat sites. *)
1172+
let prog = Affinescript.Typecheck.elaborate_string_concat prog in
11491173
let optimized_prog = Affinescript.Opt.fold_constants_program prog in
11501174
(match Affinescript.Codegen.generate_module ~loader optimized_prog with
11511175
| Error e ->

lib/ast.ml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,15 @@ type expr =
143143
}
144144
| ExprRowRestrict of expr * ident (** e \ field *)
145145
| ExprBinary of expr * binary_op * expr
146+
| ExprStringConcat of expr * expr
147+
(** String concatenation, `a ++ b` where both sides are `String`.
148+
Not produced by the parser: it is introduced by a post-typecheck
149+
*elaboration* (see {!Typecheck.elaborate_string_concat}) that
150+
rewrites the String case of the polymorphic `++` (`ExprBinary (_,
151+
OpConcat, _)`) into this node, so the wasm backend can lower it as
152+
byte concatenation rather than the list-element copy used for array
153+
`++`. The interpreter and non-wasm backends treat it as ordinary
154+
string concatenation. String-wall slice 8b. *)
146155
| ExprUnary of unary_op * expr
147156
| ExprBlock of block
148157
| ExprReturn of expr option

lib/codegen.ml

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,8 @@ let rec find_free_vars (bound_vars : string list) (expr : expr) : string list =
275275
if List.mem id.name bound_vars then [] else [id.name]
276276
| ExprBinary (e1, _, e2) ->
277277
find_free_vars bound_vars e1 @ find_free_vars bound_vars e2
278+
| ExprStringConcat (e1, e2) ->
279+
find_free_vars bound_vars e1 @ find_free_vars bound_vars e2
278280
| ExprUnary (_, e) ->
279281
find_free_vars bound_vars e
280282
| ExprIf ei ->
@@ -562,6 +564,65 @@ let rec gen_expr (ctx : context) (expr : expr) : (context * instr list) result =
562564
in
563565
Ok (ctxA, code)
564566

567+
| ExprStringConcat (left, right) ->
568+
(* String byte-concatenation `a ++ b` (both `String`). String-wall slice
569+
8b — the type-directed lowering the slice-8a guard stood in for.
570+
Introduced by Typecheck.elaborate_string_concat for the String case of
571+
the polymorphic `++`, so codegen no longer has to guess string-vs-list.
572+
573+
String layout (slices 1-7): `[len@+0][utf8 byte i @ +4+i]`. This
574+
mirrors the list-concat handler above but copies *1-byte* elements past
575+
a *single* length word, instead of 4-byte i32 elements — which is
576+
exactly the bug the guard caught: the list path copied a string's
577+
[len][utf8] as i32 elements, so `"ab" ++ "cd"` read byte 2 as the
578+
length word of "cd" (= 2) instead of 'c' (= 99). Allocate 4 + la + lb
579+
bytes, write the length word, copy a's then b's bytes. *)
580+
let* (ctx1, left_code) = gen_expr ctx left in
581+
let* (ctx2, right_code) = gen_expr ctx1 right in
582+
let (ctx3, heap_idx) = ensure_heap_ptr ctx2 in
583+
let (ctx4, a) = alloc_local ctx3 "__scat_a" in
584+
let (ctx5, b) = alloc_local ctx4 "__scat_b" in
585+
let (ctx6, la) = alloc_local ctx5 "__scat_la" in
586+
let (ctx7, lb) = alloc_local ctx6 "__scat_lb" in
587+
let (ctx8, dst) = alloc_local ctx7 "__scat_dst" in
588+
let (ctx9, k) = alloc_local ctx8 "__scat_k" in
589+
let (ctxA, zero) = alloc_local ctx9 "__scat_zero" in
590+
let copy_bytes src_ptr count dst_base_off =
591+
(* for k in 0..count: dst[4 + dst_base_off + k] = src[4 + k]
592+
(the +4 that skips the length word is folded into the load/store
593+
static offset, matching the slice-1..7 byte idiom). *)
594+
[ I32Const 0l; LocalSet k;
595+
Block (BtEmpty, [ Loop (BtEmpty, [
596+
LocalGet k; LocalGet count; I32GeS; BrIf 1;
597+
(* dst slot addr (pre-+4): dst + dst_base_off + k *)
598+
LocalGet dst; LocalGet dst_base_off; I32Add; LocalGet k; I32Add;
599+
(* value: byte at src + k (+4 via the load offset) *)
600+
LocalGet src_ptr; LocalGet k; I32Add;
601+
I32Load8U (0, 4);
602+
I32Store8 (0, 4);
603+
LocalGet k; I32Const 1l; I32Add; LocalSet k;
604+
Br 0 ]) ]) ]
605+
in
606+
let code =
607+
left_code @ [LocalSet a] @ right_code @ [LocalSet b] @
608+
[ LocalGet a; I32Load (2, 0); LocalSet la;
609+
LocalGet b; I32Load (2, 0); LocalSet lb;
610+
I32Const 0l; LocalSet zero;
611+
(* dst = heap; heap += 4 + (la + lb) *)
612+
GlobalGet heap_idx; LocalSet dst;
613+
GlobalGet heap_idx;
614+
I32Const 4l;
615+
LocalGet la; LocalGet lb; I32Add;
616+
I32Add; I32Add;
617+
GlobalSet heap_idx;
618+
(* dst length word = la + lb *)
619+
LocalGet dst; LocalGet la; LocalGet lb; I32Add; I32Store (2, 0) ]
620+
@ copy_bytes a la zero (* dst[0..la) := a's bytes *)
621+
@ copy_bytes b lb la (* dst[la..) := b's bytes *)
622+
@ [ LocalGet dst ]
623+
in
624+
Ok (ctxA, code)
625+
565626
| ExprBinary (left, op, right) ->
566627
let* (ctx', left_code) = gen_expr ctx left in
567628
let* (ctx'', right_code) = gen_expr ctx' right in

lib/effect_sites.ml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,11 @@ let rec visit_expr (visit : expr -> unit) (e : expr) : unit =
7373
| ExprField (e, _) | ExprTupleIndex (e, _) | ExprRowRestrict (e, _)
7474
| ExprSpan (e, _) | ExprUnary (_, e) ->
7575
go_expr e
76-
| ExprIndex (a, b) | ExprBinary (a, _, b) ->
76+
| ExprIndex (a, b) | ExprBinary (a, _, b) | ExprStringConcat (a, b) ->
77+
(* ExprStringConcat (slice 8b) recurses like ExprBinary and is NOT a call
78+
site: string `++` is not an effect operation, so keeping it out of the
79+
ExprApp census preserves effect-ordinal parity between the interpreter
80+
(which sees the original ExprBinary) and the wasm backend. *)
7781
go_expr a;
7882
go_expr b
7983
| ExprTuple es | ExprArray es -> List.iter go_expr es

lib/interp.ml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,12 @@ let rec eval (env : env) (expr : expr) : value result =
132132
let* arg_vals = eval_list env args in
133133
apply_function func_val arg_vals
134134

135+
| ExprStringConcat (left, right) ->
136+
(* String-wall slice 8b: the interpreter is the oracle and never sees the
137+
wasm-only elaboration, but handle it defensively as ordinary String
138+
`++` so the node has identical semantics in every backend. *)
139+
eval env (ExprBinary (left, OpConcat, right))
140+
135141
| ExprBinary (left, op, right) ->
136142
let* left_val = eval env left in
137143
let* right_val = eval env right in

lib/opt.ml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,16 @@ let rec fold_constants_expr (expr : expr) : expr =
6161
else
6262
ExprBinary (left', op, right')
6363

64+
(* String-wall slice 8b: fold sub-expressions of a String concat (the node
65+
itself isn't a constant). Introduced post-typecheck on the wasm path. *)
66+
| ExprStringConcat (left, right) ->
67+
let left' = fold_constants_expr left in
68+
let right' = fold_constants_expr right in
69+
if left == left' && right == right' then
70+
expr
71+
else
72+
ExprStringConcat (left', right')
73+
6474
| ExprUnary (op, operand) ->
6575
let operand' = fold_constants_expr operand in
6676
if operand == operand' then

lib/typecheck.ml

Lines changed: 142 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -736,6 +736,26 @@ and type_of_literal (lit : literal) : ty =
736736
| LitString _ -> ty_string
737737
| LitUnit _ -> ty_unit
738738

739+
(** String-wall slice 8b. Physical-identity record of the `++` ([OpConcat])
740+
nodes that typed as {b String} concatenation (rather than array concat).
741+
Populated by {!synth} while checking, consumed by
742+
{!elaborate_string_concat} to rewrite exactly those nodes into
743+
{!Ast.ExprStringConcat} for the byte-concat wasm lowering.
744+
745+
Keyed by physical identity ([==], via [List.memq]) and not by span/value:
746+
[ExprBinary] carries no span, and two distinct same-text `++` occurrences
747+
are value-equal. This is sound because typecheck and the elaboration run
748+
over the {e same} program object — [parse_with_face]'s lowered [prog],
749+
shared by resolve/typecheck/codegen — so the node [synth] records is
750+
physically the node the elaboration rewrites. Stale entries from a prior
751+
compile never match the current [prog]'s nodes, so accumulation across
752+
compiles is harmless; {!elaborate_string_concat} clears the list when it
753+
consumes it. *)
754+
let string_concat_sites : expr list ref = ref []
755+
756+
(** Discard any recorded String-concat sites (e.g. before re-checking). *)
757+
let reset_string_concat_sites () : unit = string_concat_sites := []
758+
739759
(** {1 Expression synthesis (mode ⇒)} *)
740760

741761
(** Synthesize a type for an expression. *)
@@ -1015,7 +1035,7 @@ let rec synth (ctx : context) (expr : expr) : ty result =
10151035
synthesise lhs, walk through repr to pierce variables; if it's a Float,
10161036
pin rhs to Float and return the matching result type; otherwise fall
10171037
through to the legacy [type_of_binop] path which is Int-monomorphic. *)
1018-
| ExprBinary (lhs, op, rhs) ->
1038+
| ExprBinary (lhs, op, rhs) as concat_node ->
10191039
let arith_or_bitwise = match op with
10201040
| OpAdd | OpSub | OpMul | OpDiv | OpMod
10211041
| OpBitAnd | OpBitOr | OpBitXor | OpShl | OpShr -> true
@@ -1065,6 +1085,10 @@ let rec synth (ctx : context) (expr : expr) : ty result =
10651085
Ok (TApp (TCon "Array", [elem]))
10661086
| TCon "String" ->
10671087
let* () = check ctx rhs ty_string in
1088+
(* String-wall slice 8b: record this `++` node (physical identity)
1089+
as a String concat, so elaborate_string_concat can rewrite it to
1090+
ExprStringConcat for the byte-concat wasm lowering. *)
1091+
string_concat_sites := concat_node :: !string_concat_sites;
10681092
Ok ty_string
10691093
| _ ->
10701094
(* lhs type not yet determined (e.g. `let mut acc = []`):
@@ -1077,6 +1101,8 @@ let rec synth (ctx : context) (expr : expr) : ty result =
10771101
| _ ->
10781102
let* () = unify_or_err lhs_ty ty_string in
10791103
let* () = unify_or_err rhs_ty ty_string in
1104+
(* Slice 8b: also a String concat (lhs was undetermined). *)
1105+
string_concat_sites := concat_node :: !string_concat_sites;
10801106
Ok ty_string))
10811107
end else begin
10821108
let (lhs_ty, rhs_ty, result_ty) = type_of_binop op in
@@ -1421,6 +1447,121 @@ and synth_and_unify (ctx : context) (expr : expr) (expected : ty) : unit result
14211447
let* got = synth ctx expr in
14221448
unify_or_err expected got
14231449

1450+
(** {1 String-concat elaboration (slice 8b)} *)
1451+
1452+
(* Rewrite every `++` node {!synth} recorded as a String concat into
1453+
[ExprStringConcat], leaving array `++` as [ExprBinary _ OpConcat _]. The
1454+
traversal mirrors {!Resolve.lower_expr} exactly so it is total over the
1455+
expression grammar — any constructor it failed to descend into would leave
1456+
a String `++` un-elaborated, where the slice-8a guard would (loudly) catch
1457+
the obvious cases and a var-var case could still miscompile. *)
1458+
let rec elab_expr (e : expr) : expr =
1459+
match e with
1460+
| ExprBinary (l, op, r) ->
1461+
let l' = elab_expr l in
1462+
let r' = elab_expr r in
1463+
if List.memq e !string_concat_sites
1464+
then ExprStringConcat (l', r')
1465+
else ExprBinary (l', op, r')
1466+
| ExprStringConcat (l, r) -> ExprStringConcat (elab_expr l, elab_expr r)
1467+
| ExprSpan (e', sp) -> ExprSpan (elab_expr e', sp)
1468+
| ExprLit _ | ExprVar _ | ExprVariant _ -> e
1469+
| ExprField (base, fld) -> ExprField (elab_expr base, fld)
1470+
| ExprLet r ->
1471+
ExprLet { r with el_value = elab_expr r.el_value;
1472+
el_body = Option.map elab_expr r.el_body }
1473+
| ExprIf r ->
1474+
ExprIf { ei_cond = elab_expr r.ei_cond;
1475+
ei_then = elab_expr r.ei_then;
1476+
ei_else = Option.map elab_expr r.ei_else }
1477+
| ExprMatch r ->
1478+
ExprMatch { em_scrutinee = elab_expr r.em_scrutinee;
1479+
em_arms = List.map elab_arm r.em_arms }
1480+
| ExprLambda r -> ExprLambda { r with elam_body = elab_expr r.elam_body }
1481+
| ExprApp (f, args) -> ExprApp (elab_expr f, List.map elab_expr args)
1482+
| ExprTupleIndex (e1, i) -> ExprTupleIndex (elab_expr e1, i)
1483+
| ExprIndex (a, i) -> ExprIndex (elab_expr a, elab_expr i)
1484+
| ExprTuple es -> ExprTuple (List.map elab_expr es)
1485+
| ExprArray es -> ExprArray (List.map elab_expr es)
1486+
| ExprRecord r ->
1487+
ExprRecord
1488+
{ er_fields =
1489+
List.map (fun (id, eo) -> (id, Option.map elab_expr eo)) r.er_fields;
1490+
er_spread = Option.map elab_expr r.er_spread }
1491+
| ExprRowRestrict (e1, id) -> ExprRowRestrict (elab_expr e1, id)
1492+
| ExprUnary (op, e1) -> ExprUnary (op, elab_expr e1)
1493+
| ExprBlock b -> ExprBlock (elab_block b)
1494+
| ExprReturn eo -> ExprReturn (Option.map elab_expr eo)
1495+
| ExprBreak _ | ExprContinue _ -> e
1496+
| ExprTry r ->
1497+
ExprTry { et_body = elab_block r.et_body;
1498+
et_catch = Option.map (List.map elab_arm) r.et_catch;
1499+
et_finally = Option.map elab_block r.et_finally }
1500+
| ExprHandle r ->
1501+
ExprHandle { eh_body = elab_expr r.eh_body;
1502+
eh_handlers = List.map elab_handler r.eh_handlers }
1503+
| ExprResume eo -> ExprResume (Option.map elab_expr eo)
1504+
| ExprUnsafe ops -> ExprUnsafe (List.map elab_unsafe ops)
1505+
1506+
and elab_arm a =
1507+
{ a with ma_guard = Option.map elab_expr a.ma_guard;
1508+
ma_body = elab_expr a.ma_body }
1509+
1510+
and elab_handler = function
1511+
| HandlerReturn (p, e) -> HandlerReturn (p, elab_expr e)
1512+
| HandlerOp (id, ps, e) -> HandlerOp (id, ps, elab_expr e)
1513+
1514+
and elab_unsafe = function
1515+
| UnsafeRead e -> UnsafeRead (elab_expr e)
1516+
| UnsafeWrite (a, b) -> UnsafeWrite (elab_expr a, elab_expr b)
1517+
| UnsafeOffset (a, b) -> UnsafeOffset (elab_expr a, elab_expr b)
1518+
| UnsafeTransmute (t1, t2, e) -> UnsafeTransmute (t1, t2, elab_expr e)
1519+
| UnsafeForget e -> UnsafeForget (elab_expr e)
1520+
1521+
and elab_block b =
1522+
{ blk_stmts = List.map elab_stmt b.blk_stmts;
1523+
blk_expr = Option.map elab_expr b.blk_expr }
1524+
1525+
and elab_stmt = function
1526+
| StmtLet r -> StmtLet { r with sl_value = elab_expr r.sl_value }
1527+
| StmtExpr e -> StmtExpr (elab_expr e)
1528+
| StmtAssign (a, op, b) -> StmtAssign (elab_expr a, op, elab_expr b)
1529+
| StmtWhile (e, b) -> StmtWhile (elab_expr e, elab_block b)
1530+
| StmtFor (p, e, b) -> StmtFor (p, elab_expr e, elab_block b)
1531+
1532+
let elab_fn_body = function
1533+
| FnBlock b -> FnBlock (elab_block b)
1534+
| FnExpr e -> FnExpr (elab_expr e)
1535+
| FnExtern -> FnExtern
1536+
1537+
let elab_top = function
1538+
| TopFn fd -> TopFn { fd with fd_body = elab_fn_body fd.fd_body }
1539+
| TopConst r -> TopConst { r with tc_value = elab_expr r.tc_value }
1540+
| TopImpl ib ->
1541+
TopImpl { ib with ib_items = List.map (function
1542+
| ImplFn fd -> ImplFn { fd with fd_body = elab_fn_body fd.fd_body }
1543+
| ImplType _ as it -> it) ib.ib_items }
1544+
| TopTrait td ->
1545+
TopTrait { td with trd_items = List.map (function
1546+
| TraitFnDefault fd ->
1547+
TraitFnDefault { fd with fd_body = elab_fn_body fd.fd_body }
1548+
| other -> other) td.trd_items }
1549+
| (TopType _ | TopEffect _ | TopExternType _ | TopExternFn _) as t -> t
1550+
1551+
(** Rewrite the String case of `++` to {!Ast.ExprStringConcat} across [program],
1552+
using the sites {!synth} recorded during type-checking. Pure apart from
1553+
clearing the recorded sites; returns [program] unchanged when nothing was
1554+
recorded. Intended to run on the wasm-codegen path only (the interpreter
1555+
and other backends handle String `++` directly). *)
1556+
let elaborate_string_concat (program : program) : program =
1557+
let result =
1558+
match !string_concat_sites with
1559+
| [] -> program
1560+
| _ -> { program with prog_decls = List.map elab_top program.prog_decls }
1561+
in
1562+
reset_string_concat_sites ();
1563+
result
1564+
14241565
(** {1 Declaration checking} *)
14251566

14261567
(** Register built-in types and functions. *)

0 commit comments

Comments
 (0)