123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502openWax_langopenAst(* Recover a [match] from the nested type-test ladder that
{!Ast_utils.lower_match} emits (and that hand-written GC code uses): a stack
of blocks, the innermost holding a threaded [br_on_cast]/[br_on_null] chain on
the scrutinee, each test branching out to its arm's block, with the arm body
after that block and a void [escape] block past them all; the [default]
follows the [escape] block as trailing code.
'escape: do {
'Lₙ₋₁: do { … 'L₀: do { _ = br_on_cast 'L₀ … (br_on_cast 'L₁ … (… v)); br 'escape }
<bind/drop block 'L₀>; arm₀ } … <bind/drop>; armₙ₋₁ }
…trailing default…
⇒ match v { …armᵢ… _ => { …trailing… } }
Because every arm body leaves the [match] (diverges), absorbing the trailing
statements into the default preserves semantics — they only run on the
no-match (fall-through) path. Bound cast arms surface their binding as a
[local.set] ([Set] / a fused [let]); {!Sink_let} sinks a local used across
several arms to their common-ancestor ladder block, where it surfaces as a
leading declaration. The fold hoists those out before the [match] (a
declaration an arm rebinds is dropped instead, since re-lowering reintroduces
it). A Wax [match] thus round-trips through the binary. Meant to run on
{!Sink_let.module_} output.
We also recover a [match] from the *flat* [br_on_cast_fail] chain that
hand-written GC code more often uses — one discarded block per arm rather than
the nested ladder; see {!collect_arms}. That round trip is not byte-for-byte
(re-lowering emits the ladder), only semantically faithful. *)letis_blocki=matchi.descwithBlock_->true|_->falseletis_chaini=matchi.descwithBr_on_cast_|Br_on_null_->true|_->false(* Parse the threaded chain (innermost operand the scrutinee). Returns the tests
in source order — innermost test ([L₀]) first — each as its label and the
pattern's cast target ([`Cast]) or null ([`Null]); the binding (if any) comes
from the wrapping block's consume, not the chain. *)letrecchain_testse=matche.descwith|Br_on_cast(l,rt,operand)->lettests,scrut=chain_testsoperandin(tests@[(l,`Castrt)],scrut)|Br_on_null(l,operand)->lettests,scrut=chain_testsoperandin(tests@[(l,`Null)],scrut)|_->([],e)(* Split off leading bare local declarations [let x;]. {!Sink_let} sinks a local
used across several arms to their common-ancestor ladder block, where it shows
up here as a leading declaration; the fold hoists those out before the match
(they are *not* arm bindings — those are fused into the consume — so dropping
them would unbind the local). Returns the declarations and the rest. *)letsplit_declsstmts=letrecauxacc=function|({desc=Let([(Some_,_)],None);_}asd)::rest->aux(d::acc)rest|rest->(List.revacc,rest)inaux[]stmts(* Strip a ladder block's consume of its inner block, returning any leading
declarations to hoist, the binding ([Some (x, set)] for a bound cast arm),
that inner block, and the trailing arm body. A [null] arm consumes nothing (a
bare block), an unbound cast drops the block (an anonymous [Let],
[_ = block]), a bound cast binds it (a fused [let], [set = false], or a [Set]
of a local declared elsewhere, [set = true]). *)letconsume_stepstmts=letdecls,stmts=split_declsstmtsinmatchstmtswith|{desc=Let([(Somex,_)],Someinner);_}::bodywhenis_blockinner->Some(decls,Some(x,false),inner,body)|{desc=Set(x,_,inner);_}::bodywhenis_blockinner->Some(decls,Some(x,true),inner,body)|{desc=Let([(None,_)],Someinner);_}::bodywhenis_blockinner->Some(decls,None,inner,body)(* The bare-block arm is the NULL arm, which consumes nothing — so its block
must be void. Without the guard, a RESULT-carrying arm whose binding was
separated from the block (a [nop] between the [end] and the [local.set],
say) was folded as if it were a null arm: the binding's value vanished
into the rebuilt [match] and the emitted Wax no longer typed, on a module
the validator accepts (a recover-shapes near-miss finding). Left alone,
the unrecovered print of the same shape is fine — the trailing hole
reconnects to the block's value through the statement. *)|({desc=Block{typ;_};_}asinner)::bodywhentyp.params=[||]&&typ.results=[||]->Some(decls,None,inner,body)|_->None(* Descend the ladder from block [blk]. Returns the wrapper levels (outer→inner,
one per arm: the block label, the arm's binding, and its body), the innermost
block's label, the chain, the escape label, and the declarations to hoist. *)letrecdescendblk=matchblk.descwith|Block{label=Somelbl;block={desc=body;_};_}->(letdecls0,body=split_declsbodyinmatchbodywith|[{desc=Let([(None,_)],Somechain);_};{desc=Br(escape,None);_};]whenis_chainchain->Some([],lbl,chain,escape,decls0)|_->(matchconsume_stepbodywith|Some(decls1,binding,inner,arm_body)->(matchdescendinnerwith|Some(levels,inner_lbl,chain,escape,decls)->Some((lbl,binding,arm_body)::levels,inner_lbl,chain,escape,decls0@decls1@decls)|None->None)|None->None))|_->None(* --- Flat [br_on_cast_fail] chain --------------------------------------- *)(* Hand-written GC code (and pre-ladder Wax output) takes a value apart with a
flat run of discarded blocks rather than the nested ladder {!descend} folds:
_ = 'L: do S { let x = br_on_cast_fail 'L &T v; body } (a bound cast arm)
_ = 'L: do S { _ = br_on_cast_fail 'L &T v; body } (an unbound cast arm)
_ = 'L: do S { br_on_non_null 'L v; body } (a null arm)
Each block re-reads the same scrutinee [v] and, on a failed test, branches to
its own label (forwarding [v], type [S]) and is dropped, falling through to
the next block; on success the (optionally bound) narrowed value falls into
[body], which diverges. So such a run is a [match] on [v], the trailing
statements its default. Re-lowering a recovered match emits the nested ladder,
not this flat chain, so the round trip is not byte-for-byte — but it is
semantically equivalent (the scrutinee is side-effect-free, see
{!same_scrut}). *)(* Structural equality of scrutinee expressions, ignoring source locations: the
side-effect-free forms a re-read scrutinee may take. Anything else compares
unequal, so the run simply does not fold. *)letrecsame_scrutab=match(a.desc,b.desc)with|Getx,Gety->x.desc=y.desc|Null,Null->true|Ints,Intt->s=t|NonNulle,NonNullf->same_scrutef|Cast(e,s),Cast(f,t)->s=t&&same_scrutef|Test(e,s),Test(f,t)->s=t&&same_scrutef|StructGet(e,x),StructGet(f,y)->x.desc=y.desc&&same_scrutef|ArrayGet(e,i),ArrayGet(f,j)->same_scrutef&&same_scrutij|_->false(* Whether control cannot fall off the end of [body] — a conservative,
syntactic check (the last statement is a clear terminator, or an [if]/[match]
whose every branch diverges). A flat-chain arm is a genuine [match] arm only
when its success path leaves the [match]; folding a non-diverging body (the
block's [do S] result is produced and dropped instead) would be wrong. *)letrecdiverges_instri=matchi.descwith|Return_|Br_|Br_table_|Unreachable|Throw_|ThrowRef_|TailCall_->true|If{if_block;else_block=Someelse_block;_}->diverges_listif_block.desc&&diverges_listelse_block.desc|Match{arms;default;_}->List.for_all(fun(_,(b:(_instrlist,_)Ast.annotated))->diverges_listb.desc)arms&&diverges_listdefault.desc|Loop{block;_}->(* A loop whose body always branches (back to the loop or out) never falls
through to the statement after it. *)diverges_listblock.desc|_->falseanddiverges_listl=matchList.revlwith[]->false|last::_->diverges_instrlast(* Whether [i] branches to a label named [name] bound outside it. A nested
construct rebinding [name] shadows it within its body (nested ladders reuse
the same label names), so only the parts evaluated outside that body are
searched there. *)letrecbranches_outname(i:locationinstr)=List.exists(fun(l:label)->l.desc=name)(Recover_trycatch.target_labelsi.desc)||matchi.descwith|Block{label=Somel;_}|Loop{label=Somel;_}|TryTable{label=Somel;_}|Try{label=Somel;_}|TryCatch{label=Somel;_}whenl.desc=name->false|If{label=Somel;cond;_}whenl.desc=name->branches_outnamecond|While{label=Somel;cond;step;_}whenl.desc=name->(branches_outnamecond||matchstepwithSomes->branches_outnames|None->false)|_->branches_out_listname(Ast_utils.sub_instrsi)andbranches_out_listnamel=List.exists(branches_outname)l(* Recognise one flat-chain arm block. Returns its pattern, scrutinee, body, and
whether a bound cast carries its binding itself (a fused [let x = …],
[`Fused]) or names a local declared just before the block ([`Decl x], which
the fold drops). The body must diverge (leave the [match]), and neither it
nor the scrutinee may branch to the block's own label: the [match] replaces the block, so such a
branch (a second [br_on_cast_fail 'L] in the same block, say) would be left
unbound. *)letarm_blockstmt=matchstmt.descwith|Let([(None,_)],Some{desc=Block{label=Someself;typ;block={desc=test::body;_}};_;})whentyp.params=[||]&&Array.lengthtyp.results=1&&diverges_listbody&¬(branches_out_listself.descbody)->(matchmatchtest.descwith|Let([(Somex,_)],Some{desc=Br_on_cast_fail(l,rt,scrut);_})whenl.desc=self.desc->Some(MatchCast(Somex,rt),scrut,body,`Fused)|Set(x,_,{desc=Br_on_cast_fail(l,rt,scrut);_})whenl.desc=self.desc->Some(MatchCast(Somex,rt),scrut,body,`Declx)|Let([(None,_)],Some{desc=Br_on_cast_fail(l,rt,scrut);_})whenl.desc=self.desc->Some(MatchCast(None,rt),scrut,body,`Fused)|Br_on_non_null(l,scrut)whenl.desc=self.desc->Some(MatchNull,scrut,body,`Fused)|_->Nonewith(* The scrutinee moves out of the block too. *)|Some(_,scrut,_,_)whenbranches_outself.descscrut->None|arm->arm)|_->Noneletcompatscruts=matchscrutwithNone->true|Somes0->same_scruts0s(* Collect a maximal run of flat-chain arms from the front of [stmts], threading
the shared scrutinee. Returns the arms, the shared scrutinee, bare local
declarations to hoist before the match, and the remaining (default)
statements.
A bare local declaration [let x;] between arms is either the binding for the
next [`Decl]-form arm (dropped — the recovered arm re-declares it) or an
unrelated local that {!Sink_let} floated into the run (hoisted before the
match, where it stays in scope for every arm; this only fires when more arms
follow, so a trailing declaration stays in the default). *)letreccollect_armsscrutstmts=lettakepatbodysrest=letscrut=matchscrutwithNone->Somes|some->someinletarms,scrut,hoisted,rest=collect_armsscrutrestin((pat,body)::arms,scrut,hoisted,rest)inmatchstmtswith|({desc=Let([(Somex,_)],None);_}asdecl)::rest->(matchrestwith|blk::rest'whenmatcharm_blockblkwith|Some(_,s,_,`Decly)->y.desc=x.desc&&compatscruts|_->false->letpat,s,body=matcharm_blockblkwith|Some(pat,s,body,_)->(pat,s,body)|None->assertfalsein(* The arm's binding replaces the local [x], but only within its
body. A use in the scrutinee, a later arm or the default would be
left reading a local the arm no longer writes (reached again past
a loop back-edge, say), so the run stops before this arm. *)ifSink_let.occurs_listx.desc(s::rest')then([],scrut,[],stmts)elsetakepatbodysrest'|_->letarms,scrut,hoisted,trailing=collect_armsscrutrestinifarms=[]then([],scrut,[],stmts)else(arms,scrut,decl::hoisted,trailing))|blk::rest->(matcharm_blockblkwith|Some(pat,s,body,`Fused)whencompatscruts->takepatbodysrest|_->([],scrut,[],stmts))|[]->([],scrut,[],stmts)letrecrewrite_instr~faithful(i:locationinstr):locationinstr=letd=rewrite_desc~faithfuli.descinifd==i.descthenielse{iwithdesc=d}(* Fold the [escape] block [i] (and the trailing statements after it, which
become the default) into a [match]. *)andtry_fold~faithful(i:locationinstr)(trailing:locationinstrlist):(locationinstrlist*locationinstr)option=matchdescendiwith|None->None|Some(levels,inner_lbl,chain,escape,decls)->lettests,scrut=chain_testschaininletn=List.lengthtestsin(* Each test branches to its arm's block; descending nests them
innermost→outermost as the chain orders them, with the escape block
outermost. Checking that pins the fold to a genuine ladder. *)letblock_labels=inner_lbl::List.rev_map(fun(l,_,_)->l)levelsinletrectakek=function|x::rwhenk>0->x::take(k-1)r|_->[]inletlabel_names=List.map(fun(l:label)->l.desc)block_labelsinletdistinct=List.length(List.sort_uniqcomparelabel_names)=List.lengthlabel_namesin(* An arm body is moved out of the ladder blocks into the [match], which
binds none of their labels, so a body still branching to one (a
[br_if 'Lᵢ] to a later arm's consume, a [br 'escape] to the default)
would be left dangling. The scrutinee moves out of them too. *)letbodies_free=not(List.exists(funbody->List.exists(funname->branches_out_listnamebody)label_names)([scrut]::List.map(fun(_,_,body)->body)levels))inletchain_ok=List.lengthlevels=n&&List.map(fun((l:label),_)->l.desc)tests=takenlabel_names&&matchList.revblock_labelswith|last::_->last.desc=escape.desc|[]->falseinifn<1||(notdistinct)||(notchain_ok)||notbodies_freethenNoneelseletarm(_,pat_kind)(_,binding,body)=letpat=match(pat_kind,binding)with|`Castrt,Some(x,_)->Some(MatchCast(Somex,rt))|`Castrt,None->Some(MatchCast(None,rt))|`Null,None->SomeMatchNull|`Null,Some_->NoneinOption.map(funpat->(pat,no_loc(rewrite_list~faithfulbody)))patinletarms=List.map2armtests(List.revlevels)inifList.existsOption.is_nonearmsthenNoneelseletarms=List.filter_mapFun.idarmsin(* A local an arm's binding replaces must not be used outside the
arms rebinding it (in the scrutinee, another arm or the default):
that use would read a local the arm no longer writes. So such an
arm is folded only when the local is declared within the ladder
(so no use lies beyond it) and has no use outside. *)letused_outsidex=Sink_let.occurs_listx(scrut::trailing)||List.exists(fun(p,(b:(_instrlist,_)Ast.annotated))->(matchpwith|MatchCast(Somey,_)->y.desc<>x|_->true)&&Sink_let.occurs_listxb.desc)armsinletdeclaredx=List.exists(fund->matchd.descwith|Let([(Somey,_)],None)->y.desc=x|_->false)declsinletreplaced_ok=List.for_all(fun(_,binding,_)->matchbindingwith|Some((x:ident),true)->declaredx.desc&¬(used_outsidex.desc)|_->true)levelsin(* A declaration whose name an arm rebinds is redundant (re-lowering
reintroduces it) unless the local is still used outside the arms
rebinding it (then it is a different local that a fused binding
only shadows). The rest are genuine locals to hoist. *)lethoisted=List.filter(fund->matchd.descwith|Let([(Somex,_)],None)->(not(List.exists(fun(p,_)->matchpwith|MatchCast(Somey,_)->y.desc=x.desc|_->false)arms))||used_outsidex.desc|_->true)declsinifnotreplaced_okthenNoneelseSome(hoisted,{iwithdesc=Match{scrutinee=rewrite_instr~faithfulscrut;arms;default=no_loc(rewrite_list~faithfultrailing);};})andrewrite_list~faithfulstmts=matchstmtswith|[]->[]|i::rest->((* First the nested ladder (the shape {!Ast_utils.lower_match} emits),
then a flat [br_on_cast_fail] chain — folded even for a single arm (a
lone downcast-or-branch reads as a one-arm [match]). The flat-chain arm
re-lowers to the nested ladder rather than the original flat chain, so
[--faithful] ([faithful]) skips it while keeping the exact-inverse
nested-ladder fold. *)matchtry_fold~faithfulirestwith|Some(hoisted,m)->List.map(rewrite_instr~faithful)hoisted@[m]|None->(matchiffaithfulthen([],None,[],[])elsecollect_armsNonestmtswith|(_::_asarms),Somescrut,hoisted,trailing->List.map(rewrite_instr~faithful)hoisted@[{iwithdesc=Match{scrutinee=rewrite_instr~faithfulscrut;arms=List.map(fun(p,b)->(p,no_loc(rewrite_list~faithfulb)))arms;default=no_loc(rewrite_list~faithfultrailing);};};]|_->leti'=rewrite_instr~faithfuliandrest'=rewrite_list~faithfulrestinifi'==i&&rest'==restthenstmtselsei'::rest'))andrewrite_desc~faithful(desc:locationinstr_desc):locationinstr_desc=(* Purely structural recursion; the folding lives in [try_fold]/[rewrite_list].
Share-preserving, so an untouched subtree is not rebuilt. *)Ast_utils.map_desc~instr:(rewrite_instr~faithful)~block:(rewrite_list~faithful)descletrecfield_desc~faithful(f:locationmodulefield)=letmap_fields=List.map(fun(a:(locationmodulefield,_)Ast.annotated)->{awithdesc=field_desc~faithfula.desc})inmatchfwith|Func({body=label,instrs;_}asr)->Func{rwithbody=(label,rewrite_list~faithfulinstrs)}|Conditional({then_fields;else_fields;_}asr)->Conditional{rwiththen_fields={then_fieldswithdesc=map_fieldsthen_fields.desc};else_fields=Option.map(fun(b:((locationmodulefield,location)Ast.annotatedlist,location)Ast.annotated)->{bwithdesc=map_fieldsb.desc})else_fields;}|(Type_|Type_alias_|Module_annotation_|Import_|Import_group_|Global_|Tag_|Memory_|Data_|Table_|Elem_)asf->fletmodule_?(faithful=false)(m:locationmodule_):locationmodule_=List.map(fun(a:(locationmodulefield,location)Ast.annotated)->{awithdesc=field_desc~faithfula.desc})m