123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530openWax_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)|({desc=Block_;_}asinner)::body->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)->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(i:locationinstr):locationinstr={iwithdesc=rewrite_desci.desc}(* Fold the [escape] block [i] (and the trailing statements after it, which
become the default) into a [match]. *)andtry_fold(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_listbody)))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_instrscrut;arms;default=no_loc(rewrite_listtrailing);};})andrewrite_liststmts=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]). *)matchtry_foldirestwith|Some(hoisted,m)->List.maprewrite_instrhoisted@[m]|None->(matchcollect_armsNonestmtswith|(_::_asarms),Somescrut,hoisted,trailing->List.maprewrite_instrhoisted@[{iwithdesc=Match{scrutinee=rewrite_instrscrut;arms=List.map(fun(p,b)->(p,no_loc(rewrite_listb)))arms;default=no_loc(rewrite_listtrailing);};};]|_->rewrite_instri::rewrite_listrest))andrewrite_desc(desc:locationinstr_desc):locationinstr_desc=matchdescwith|Block{label;typ;block}->Block{label;typ;block={blockwithdesc=rewrite_listblock.desc}}|Loop{label;typ;block}->Loop{label;typ;block={blockwithdesc=rewrite_listblock.desc}}|While{label;cond;step;block}->While{label;cond=rewrite_instrcond;step=Option.maprewrite_instrstep;block={blockwithdesc=rewrite_listblock.desc};}|If{label;typ;cond;if_block;else_block}->If{label;typ;cond=rewrite_instrcond;if_block={if_blockwithdesc=rewrite_listif_block.desc};else_block=Option.map(funb->{bwithdesc=rewrite_listb.desc})else_block;}|TryTable{label;typ;catches;block}->TryTable{label;typ;catches;block={blockwithdesc=rewrite_listblock.desc};}|TryCatch{label;typ;block;arms}->TryCatch{label;typ;block={blockwithdesc=rewrite_listblock.desc};arms=List.map(funa->{awitharm_body={a.arm_bodywithdesc=rewrite_lista.arm_body.desc};})arms;}|Try{label;typ;block;catches;catch_all}->Try{label;typ;block={blockwithdesc=rewrite_listblock.desc};catches=List.map(fun(t,l)->(t,{lwithdesc=rewrite_listl.desc}))catches;catch_all=Option.map(funb->{bwithdesc=rewrite_listb.desc})catch_all;}|Dispatch{index;cases;default;arms}->Dispatch{index=rewrite_instrindex;cases;default;arms=List.map(fun(l,b)->(l,{bwithdesc=rewrite_listb.desc}))arms;}|Match{scrutinee;arms;default}->Match{scrutinee=rewrite_instrscrutinee;arms=List.map(fun(p,b)->(p,{bwithdesc=rewrite_listb.desc}))arms;default={defaultwithdesc=rewrite_listdefault.desc};}|If_annotation{cond;then_body;else_body}->If_annotation{cond;then_body={then_bodywithdesc=rewrite_listthen_body.desc};else_body=Option.map(funb->{bwithdesc=rewrite_listb.desc})else_body;}|Set(x,op,e)->Set(x,op,rewrite_instre)|Tee(x,e)->Tee(x,rewrite_instre)|Labelled(l,e)->Labelled(l,rewrite_instre)|Call(t,args)->Call(rewrite_instrt,List.maprewrite_instrargs)|TailCall(t,args)->TailCall(rewrite_instrt,List.maprewrite_instrargs)|Cast(e,t)->Cast(rewrite_instre,t)|CastDesc(e,t,d)->CastDesc(rewrite_instre,t,rewrite_instrd)|Test(e,t)->Test(rewrite_instre,t)|NonNulle->NonNull(rewrite_instre)|Struct(idx,fs)->Struct(idx,List.map(fun(n,e)->(n,Option.maprewrite_instre))fs)|StructDesc(d,fs)->StructDesc(rewrite_instrd,List.map(fun(n,e)->(n,Option.maprewrite_instre))fs)|StructDefaultDescd->StructDefaultDesc(rewrite_instrd)|StructGet(e,x)->StructGet(rewrite_instre,x)|GetDescriptore->GetDescriptor(rewrite_instre)|StructSet(e,x,v)->StructSet(rewrite_instre,x,rewrite_instrv)|Array(idx,a,b)->Array(idx,rewrite_instra,rewrite_instrb)|ArrayDefault(idx,e)->ArrayDefault(idx,rewrite_instre)|ArrayFixed(idx,l)->ArrayFixed(idx,List.maprewrite_instrl)|ArraySegment(idx,d,a,b)->ArraySegment(idx,d,rewrite_instra,rewrite_instrb)|ArrayGet(a,b)->ArrayGet(rewrite_instra,rewrite_instrb)|ArraySet(a,b,c)->ArraySet(rewrite_instra,rewrite_instrb,rewrite_instrc)|BinOp(op,a,b)->BinOp(op,rewrite_instra,rewrite_instrb)|UnOp(op,e)->UnOp(op,rewrite_instre)|Let(bs,e)->Let(bs,Option.maprewrite_instre)|Br(l,e)->Br(l,Option.maprewrite_instre)|Br_if(l,e)->Br_if(l,rewrite_instre)|Hinted(h,e)->Hinted(h,rewrite_instre)|On(e,h)->On(rewrite_instre,h)|Br_table(ls,e)->Br_table(ls,rewrite_instre)|Br_on_null(l,e)->Br_on_null(l,rewrite_instre)|Br_on_non_null(l,e)->Br_on_non_null(l,rewrite_instre)|Br_on_cast(l,t,e)->Br_on_cast(l,t,rewrite_instre)|Br_on_cast_fail(l,t,e)->Br_on_cast_fail(l,t,rewrite_instre)|Br_on_cast_desc_eq(l,t,e,d)->Br_on_cast_desc_eq(l,t,rewrite_instre,rewrite_instrd)|Br_on_cast_desc_eq_fail(l,t,e,d)->Br_on_cast_desc_eq_fail(l,t,rewrite_instre,rewrite_instrd)|Throw(idx,e)->Throw(idx,List.maprewrite_instre)|ThrowRefe->ThrowRef(rewrite_instre)|ContNew(ct,e)->ContNew(ct,rewrite_instre)|ContBind(src,dst,l)->ContBind(src,dst,List.maprewrite_instrl)|Suspend(tag,l)->Suspend(tag,List.maprewrite_instrl)|Resume(ct,h,l)->Resume(ct,h,List.maprewrite_instrl)|ResumeThrow(ct,tag,h,l)->ResumeThrow(ct,tag,h,List.maprewrite_instrl)|ResumeThrowRef(ct,h,l)->ResumeThrowRef(ct,h,List.maprewrite_instrl)|Switch(ct,tag,l)->Switch(ct,tag,List.maprewrite_instrl)|Returne->Return(Option.maprewrite_instre)|Sequencel->Sequence(List.maprewrite_instrl)|Select(a,b,c)->Select(rewrite_instra,rewrite_instrb,rewrite_instrc)|(Unreachable|Nop|Hole|Null|Get_|Path_|Char_|String_|Int_|Float_|StructDefault_)asx->xletrecfield_desc(f:locationmodulefield)=letmap_fields=List.map(funa->{awithdesc=field_desca.desc})inmatchfwith|Func({body=label,instrs;_}asr)->Func{rwithbody=(label,rewrite_listinstrs)}|Conditional({then_fields;else_fields;_}asr)->Conditional{rwiththen_fields={then_fieldswithdesc=map_fieldsthen_fields.desc};else_fields=Option.map(funb->{bwithdesc=map_fieldsb.desc})else_fields;}|(Type_|Module_annotation_|Import_|Import_group_|Global_|Tag_|Memory_|Data_|Table_|Elem_)asf->fletmodule_(m:locationmodule_):locationmodule_=List.map(funa->{awithdesc=field_desca.desc})m