123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411openWax_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] 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 [Set] / a fused [let]). *)letconsume_stepstmts=letdecls,stmts=split_declsstmtsinmatchstmtswith|{desc=Let([(Somex,_)],Someinner);_}::bodywhenis_blockinner->Some(decls,Somex,inner,body)|{desc=Set(x,_,inner);_}::bodywhenis_blockinner->Some(decls,Somex,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(* 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]). *)letarm_blockstmt=matchstmt.descwith|Let([(None,_)],Some{desc=Block{label=Someself;typ;block={desc=test::body;_}};_;})whentyp.params=[||]&&Array.lengthtyp.results=1&&diverges_listbody->(matchtest.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)|_->None)|_->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->assertfalseintakepatbodysrest'|_->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_namesinletchain_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_okthenNoneelseletarm(_,pat_kind)(_,binding,body)=letpat=match(pat_kind,binding)with|`Castrt,Somex->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 declaration whose name an arm rebinds is redundant (re-lowering
reintroduces it); the rest are genuine locals to hoist. *)letbound=List.filter_map(fun(p,_)->matchpwithMatchCast(Somex,_)->Somex.desc|_->None)armsinlethoisted=List.filter(fund->matchd.descwith|Let([(Somex,_)],None)->not(List.memx.descbound)|_->true)declsinSome(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_|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