123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326432743284329433043314332433343344335433643374338433943404341434243434344434543464347434843494350435143524353435443554356435743584359436043614362436343644365436643674368436943704371437243734374437543764377437843794380438143824383438443854386438743884389439043914392439343944395439643974398439944004401440244034404440544064407440844094410441144124413441444154416441744184419442044214422442344244425442644274428442944304431443244334434443544364437443844394440444144424443444444454446444744484449445044514452445344544455445644574458445944604461446244634464446544664467446844694470447144724473447444754476447744784479448044814482448344844485448644874488448944904491449244934494449544964497449844994500450145024503450445054506450745084509451045114512451345144515451645174518451945204521452245234524452545264527452845294530453145324533453445354536453745384539454045414542454345444545454645474548454945504551455245534554455545564557455845594560456145624563456445654566456745684569457045714572457345744575457645774578457945804581458245834584458545864587458845894590459145924593459445954596459745984599460046014602460346044605460646074608460946104611461246134614461546164617461846194620462146224623462446254626462746284629463046314632463346344635463646374638463946404641464246434644464546464647464846494650465146524653465446554656465746584659466046614662466346644665466646674668466946704671467246734674467546764677467846794680468146824683468446854686468746884689469046914692469346944695469646974698469947004701470247034704470547064707470847094710471147124713471447154716471747184719472047214722472347244725472647274728472947304731473247334734473547364737473847394740474147424743474447454746474747484749475047514752475347544755475647574758475947604761476247634764476547664767476847694770477147724773477447754776477747784779478047814782478347844785478647874788478947904791479247934794479547964797479847994800480148024803480448054806480748084809481048114812481348144815481648174818481948204821482248234824482548264827482848294830483148324833483448354836483748384839484048414842484348444845484648474848484948504851485248534854485548564857485848594860486148624863486448654866486748684869487048714872487348744875487648774878487948804881488248834884488548864887488848894890489148924893489448954896489748984899490049014902490349044905490649074908490949104911491249134914491549164917491849194920492149224923492449254926492749284929493049314932493349344935493649374938493949404941494249434944494549464947494849494950495149524953495449554956495749584959496049614962496349644965496649674968496949704971497249734974497549764977497849794980498149824983498449854986498749884989499049914992499349944995499649974998499950005001500250035004500550065007500850095010501150125013501450155016501750185019502050215022502350245025502650275028502950305031503250335034503550365037503850395040504150425043504450455046504750485049505050515052505350545055505650575058505950605061506250635064506550665067506850695070507150725073507450755076507750785079508050815082508350845085508650875088508950905091509250935094509550965097509850995100510151025103510451055106510751085109511051115112511351145115511651175118511951205121512251235124512551265127512851295130513151325133513451355136513751385139514051415142514351445145514651475148514951505151515251535154515551565157515851595160516151625163516451655166516751685169517051715172517351745175517651775178517951805181518251835184518551865187518851895190519151925193519451955196519751985199520052015202520352045205520652075208520952105211521252135214521552165217521852195220522152225223522452255226522752285229523052315232523352345235523652375238523952405241524252435244524552465247524852495250525152525253525452555256525752585259526052615262526352645265526652675268526952705271527252735274527552765277527852795280528152825283528452855286528752885289529052915292529352945295529652975298529953005301530253035304530553065307530853095310531153125313531453155316531753185319532053215322532353245325532653275328532953305331533253335334533553365337533853395340534153425343534453455346534753485349535053515352535353545355535653575358535953605361536253635364536553665367536853695370537153725373537453755376537753785379538053815382538353845385538653875388538953905391539253935394539553965397539853995400540154025403540454055406540754085409541054115412541354145415541654175418541954205421542254235424542554265427542854295430543154325433543454355436543754385439544054415442544354445445544654475448544954505451545254535454545554565457545854595460546154625463546454655466546754685469547054715472547354745475547654775478547954805481548254835484548554865487548854895490549154925493549454955496549754985499550055015502550355045505550655075508550955105511551255135514551555165517551855195520552155225523552455255526552755285529553055315532553355345535553655375538553955405541554255435544554555465547554855495550555155525553555455555556555755585559556055615562556355645565556655675568556955705571557255735574557555765577557855795580558155825583558455855586558755885589559055915592559355945595559655975598559956005601560256035604560556065607560856095610561156125613561456155616561756185619562056215622562356245625562656275628562956305631563256335634563556365637563856395640564156425643564456455646564756485649565056515652565356545655565656575658565956605661566256635664566556665667566856695670567156725673567456755676567756785679568056815682568356845685568656875688568956905691569256935694569556965697569856995700570157025703570457055706570757085709571057115712571357145715571657175718571957205721572257235724572557265727572857295730573157325733573457355736573757385739574057415742574357445745574657475748574957505751575257535754575557565757575857595760576157625763576457655766576757685769577057715772577357745775577657775778577957805781578257835784578557865787578857895790579157925793579457955796579757985799580058015802580358045805580658075808580958105811581258135814581558165817581858195820582158225823582458255826582758285829583058315832583358345835583658375838583958405841584258435844584558465847584858495850585158525853585458555856585758585859586058615862586358645865586658675868586958705871587258735874587558765877587858795880588158825883588458855886588758885889589058915892589358945895589658975898589959005901590259035904590559065907590859095910591159125913591459155916591759185919592059215922592359245925592659275928592959305931593259335934593559365937593859395940594159425943594459455946594759485949595059515952595359545955595659575958595959605961596259635964596559665967596859695970597159725973597459755976597759785979598059815982598359845985598659875988598959905991599259935994599559965997599859996000600160026003600460056006600760086009601060116012601360146015601660176018601960206021602260236024602560266027602860296030603160326033603460356036603760386039604060416042604360446045604660476048604960506051605260536054605560566057605860596060606160626063606460656066606760686069607060716072607360746075607660776078607960806081608260836084608560866087608860896090609160926093609460956096609760986099610061016102610361046105610661076108610961106111611261136114611561166117611861196120612161226123612461256126612761286129613061316132613361346135613661376138613961406141614261436144614561466147614861496150615161526153615461556156615761586159616061616162616361646165616661676168616961706171617261736174617561766177617861796180618161826183618461856186618761886189619061916192619361946195619661976198619962006201620262036204620562066207620862096210621162126213621462156216621762186219622062216222622362246225622662276228622962306231623262336234623562366237623862396240624162426243624462456246624762486249625062516252625362546255625662576258625962606261626262636264626562666267626862696270627162726273627462756276627762786279628062816282628362846285628662876288628962906291629262936294629562966297629862996300630163026303630463056306630763086309631063116312631363146315631663176318631963206321632263236324632563266327632863296330633163326333633463356336633763386339634063416342634363446345634663476348634963506351635263536354635563566357openWax_langmoduleSrc=Wax_wasm.Ast.TextmoduleSimd=Wax_wasm.SimdmoduleAtomics=Wax_wasm.AtomicsmoduleUint32=Wax_utils.Uint32moduleCond=Wax_wasm.Cond_solver(* Raised by [Sequence.get] for a numeric field reference in a module with
conditional annotations: the field's index depends on which branch is taken,
so it cannot be resolved to a single Wax name. Caught in [module_] and
reported as a located diagnostic. *)exceptionNumeric_ref_in_conditionalofWax_wasm.Ast.location(* Raised when an index or label reference resolves to nothing — it is out of
range, or names an undeclared entity. This only happens on a module that
validation would reject (with an "unknown ..." error), so conversion gives up
rather than inventing a target. *)exceptionUnresolved_referenceofWax_wasm.Ast.location(*** Symbol tables and stacks ***)moduleSequence=structtypet={index_mapping:(Uint32.t,string)Hashtbl.t;label_mapping:(string,string)Hashtbl.t;export_mapping:(string,string)Hashtbl.t;mutablelast_index:int;mutablecurrent_index:int;namespace:Namespace.t;default:string;forbid_numeric:bool;(* When set (module-level sequences of a module containing conditional
annotations), numeric references are refused: a field's index depends
on which branch is taken, so it cannot be resolved to one name. *)is_conditional:bool;diagnostics:Wax_utils.Diagnostic.contextoption;(* Where to report a [naming-conflict] / [reserved-word-rename] warning
when a source name has to be renamed; [None] silences them (for
internal namespaces without a source identifier to point at). *)}letmake?(forbid_numeric=false)?is_conditional?diagnosticsnamespacedefault=letis_conditional=Option.value~default:forbid_numericis_conditionalin{index_mapping=Hashtbl.create16;label_mapping=Hashtbl.create16;export_mapping=Hashtbl.create16;last_index=0;current_index=0;namespace;default;forbid_numeric;is_conditional;diagnostics;}(* Report that the source name [original] had to be renamed to [renamed]
(because it is a reserved word, or collides with another name), pointing at
the source identifier. For a collision, [previous] (when known) points the
related label at the occurrence that first claimed the name. *)letreport_renamediagnostics~location~previous~reserved~original~renamed=letwarning,message=ifreservedthen(Wax_utils.Warning.Reserved_word_rename,Wax_utils.Message.text(Printf.sprintf"'%s' is a reserved word; renaming this identifier to '%s'."originalrenamed))else(Wax_utils.Warning.Naming_conflict,Wax_utils.Message.text(Printf.sprintf"The name '%s' is already in use; renaming this occurrence to \
'%s'."originalrenamed))inletrelated=matchpreviouswith|Somelocation->[{Wax_utils.Diagnostic.location;message=Wax_utils.Message.text(Printf.sprintf"'%s' first claimed here"original);};]|None->[]inWax_utils.Diagnostic.reportdiagnostics~location~severity:Warning~warning~related~message()letregister'?hint?claimedseqexport_tbl(kind:Src.exportableoption)(id:Src.nameoption)exports=letidx=Uint32.of_intseq.last_indexin(* The same entity may already have been registered in another branch of a
conditional. Its identity is the [$id] or, lacking one, a shared export
name (export names are unique per resolved module, so a collision can
only mean mutually-exclusive branches). Reuse the Wax name so references
stay coherent, but still consume an index slot below so positional naming
via [get_current] stays aligned with the conversion order. This only
applies to module-level sequences of a conditional module
([forbid_numeric]); locals reuse a single sequence across functions,
where a repeated [$id] is a distinct variable, not the same entity. *)letreused=ifseq.is_conditionalthenmatchidwith|Somenm->(* An explicit [$id] is authoritative: it is reused only when the
same id was already bound in another branch. Do not fall back to
export-name matching, which would conflate this entity with a
different one that merely shares an export name in a
mutually-exclusive branch (e.g. [$unix_isatty] versus the
imported [$isatty], both exporting [unix_isatty]). *)Hashtbl.find_optseq.label_mappingnm.Ast.desc|None->letfound=List.find_map(funnm->Hashtbl.find_optseq.export_mappingnm.Wax_utils.Ast.desc)exportsinifOption.is_nonefound&¬seq.forbid_numericthenHashtbl.find_optseq.index_mappingUint32.zeroelsefoundelseNonein(* A source name already claimed by the caller's priority pass (see the
local sequence's pre-pass): it is reserved in the namespace under this
name and any rename was already reported, so take it as-is. This lets a
real source name win the plain name over a generated default. *)letpre_claimed=match(claimed,id)with|Sometbl,Somenm->Hashtbl.find_opttblnm.Ast.desc|_->Noneinletname=match(reused,pre_claimed)with|Somename,_|_,Somename->name|None,None->(* [src] is the source identifier the name was taken from (with its
location), or [None] for a synthesized default; only a renamed
source identifier is worth a warning. *)(* An inferred name -- an export name, or the import-name / parent-field
[hint] -- is usable only when it is a valid Wax identifier that is
not a keyword: borrowing a keyword would force a suffixed rename
(e.g. [memory_2]) that reads worse than the generated default. An
explicit [$id] is authoritative and kept as-is even when it is a
keyword (it is renamed with a warning, as before). *)letusable_inferrednm=Lexer.is_valid_identifiernm.Wax_utils.Ast.desc&¬(Namespace.is_reservedseq.namespacenm.Ast.desc)inletdefault_or_hint()=matchhintwith|Somehwhennot(Namespace.is_reservedseq.namespaceh)->(h,None)|_->(seq.default,None)inletcandidate,src=match(id,exports)with|Somenm,_whenLexer.is_valid_identifiernm.Ast.desc->(nm.Ast.desc,Somenm)|None,nm::_whenusable_inferrednm->(nm.Ast.desc,Somenm)|_->(matchkindwith|None->default_or_hint()|Somekind->(matchHashtbl.find_optexport_tbl(kind,Src.Numidx)with|Some(nm::_)whenusable_inferrednm->(nm.Ast.desc,Somenm)|_->default_or_hint()))inletname,outcome=matchsrcwith|Somenm->Namespace.add'~loc:nm.Ast.infoseq.namespacecandidate|None->Namespace.add'seq.namespacecandidatein(match(src,outcome,seq.diagnostics)with|Somenm,Namespace.Renamed{reserved;previous},Somediagnostics->report_renamediagnostics~location:nm.Ast.info~previous~reserved~original:candidate~renamed:name|_->());nameinseq.last_index<-seq.last_index+1;Hashtbl.addseq.index_mappingidxname;Option.iter(funid->Hashtbl.replaceseq.label_mappingid.Wax_utils.Ast.descname)id;(* Record only the head export as this entity's cross-branch identity, not
every export: a single multi-export function in one branch may correspond
to several distinct single-export functions in another (e.g. one wasi
function exporting [unix_getuid]/[unix_geteuid]/… versus one function per
id elsewhere). Recording all of them would let each sibling match and
reuse this one name, binding the same Wax name twice in that branch. *)(matchexportswith|nm::_->Hashtbl.replaceseq.export_mappingnm.Ast.descname|[]->());nameletregister?hint?claimedseqexport_tblkindidexports=ignore(register'?hint?claimedseqexport_tblkindidexports)(* Claim source name [candidate] in the namespace ahead of positional
registration, reporting a rename (reserved word, or a collision with an
already-claimed name) exactly as [register'] would. Returns the final,
possibly-renamed name. Used to give real source names priority over the
generated default before any unnamed entity is registered. *)letclaim_nameseq~loccandidate=letname,outcome=Namespace.add'~locseq.namespacecandidatein(match(outcome,seq.diagnostics)with|Namespace.Renamed{reserved;previous},Somediagnostics->report_renamediagnostics~location:loc~previous~reserved~original:candidate~renamed:name|_->());nameletgetseq(idx:Src.idx)={idxwithdesc=(matchidx.descwith|Numn->(ifseq.forbid_numericthenraise(Numeric_ref_in_conditionalidx.Ast.info);matchHashtbl.find_optseq.index_mappingnwith|Somename->name|None->raise(Unresolved_referenceidx.Ast.info))|Idid->(matchHashtbl.find_optseq.label_mappingidwith|Somename->name|None->raise(Unresolved_referenceidx.Ast.info)));}letget_currentseq=leti=seq.current_indexinseq.current_index<-i+1;Ast.no_loc(Hashtbl.findseq.index_mapping(Uint32.of_inti))(* A fresh, unique name in this sequence's namespace, for an entity not in the
source (e.g. an element segment synthesised from an inline table init). *)letfresh_nameseq=Ast.no_loc(Namespace.addseq.namespaceseq.default)(* Bind [name] at a specific [idx], for an entity materialised on demand
outside the normal registration order — an implicit (inline-signature) type
first referenced from a ref-type position (see [type_ref_name]). *)letfind_boundseqidx=Hashtbl.find_optseq.index_mappingidxletbind_atseqidxname=Hashtbl.replaceseq.index_mappingidxnameletmint_nameseq=Namespace.addseq.namespaceseq.defaultletconsume_currentsseq=seq.current_index<-seq.last_index(* Consume an index slot without binding a name, for an entity rendered
anonymously (a [_] parameter). Later positional references stay aligned. *)letskipseq=seq.last_index<-seq.last_index+1end(* Turn a Wasm identifier into a valid Wax identifier. Wasm identifiers are
ASCII (see the Wasm lexer's [idchar]), so every character Wax does not accept
in an identifier is mapped to an underscore ([$label$n] -> [label_n]), then
one more is prefixed when the result still cannot start an identifier (it
begins with a digit or a ['], as in [$0_bytes] -> [_0_bytes]). We give up
(returning [None], so the caller falls back to a generated name) when two
rejected characters sit side by side: a lone separator reads fine, but a run
of them ([$!!!]) collapses to a [__] blob that no longer resembles a name. *)letsanitize_identifiers=ifLexer.is_valid_identifiersthenSomeselseifs=""thenNoneelseletis_idcharc=(c>='a'&&c<='z')||(c>='A'&&c<='Z')||(c>='0'&&c<='9')||c='_'||c='\''inletrecadjacent_rejectsi=i+1<String.lengths&&(((not(is_idchars.[i]))&¬(is_idchars.[i+1]))||adjacent_rejects(i+1))inifadjacent_rejects0thenNoneelseletmapped=String.map(func->ifis_idcharcthencelse'_')sinletcandidate=matchmapped.[0]with'0'..'9'|'\''->"_"^mapped|_->mappedinifLexer.is_valid_identifiercandidatethenSomecandidateelseNonemoduleLabelStack=structtypet={ns:Namespace.t;stack:(stringoption*(string*boolref))list;}letpush?diagnostics?(targeted=true)st(label:Src.nameoption)=letns=Namespace.dupst.nsinletused=reffalsein(* The source label name made into a valid Wax identifier (sanitizing e.g. a
leading digit, [$0_bytes] -> ['_0_bytes]); [None] when the source had no
name or it cannot be sanitized, in which case we fall back to the
generated "l". *)letsrc=matchlabelwith|Somelabel->(matchsanitize_identifierlabel.Ast.descwith|Somedesc->Some{labelwithAst.desc}|None->None)|None->Noneinletcandidate=matchsrcwithSomel->l.Ast.desc|None->"l"in(* Only claim a name for a label that will actually render: a source-named
block always renders (see below), and an anonymous block renders only
when a branch targets it ([targeted]). Reserving a name for an anonymous,
untargeted block would waste the fallback "l" and needlessly bump a real
inner label of the same name — the block renders label-free, so it needs
no name. When not reserved, [name] is a bare candidate that is never
emitted (its [used] stays false); it would only leak if [targeted]
under-approximated, which the round-trip corpus would flag. *)letname,outcome=ifOption.is_somesrc||targetedthenmatchsrcwith|Somel->Namespace.add'~loc:l.Ast.infonscandidate|None->Namespace.add'nscandidateelse(candidate,Namespace.Available)in((fun()->(* Render the label when a branch targets it, or when the source named
the block with a name we could keep — a named block keeps its label
even if no branch targets it, so the name survives the round-trip. An
anonymous (or unsalvageably-named) unbranched block stays
label-free. *)if!used||Option.is_somesrcthen((* A label namespace reserves no words, so a rename is always a
collision with an enclosing label of the same name. *)(match(src,outcome,diagnostics)with|Somel,Namespace.Renamed{reserved;previous},Somediagnostics->Sequence.report_renamediagnostics~location:l.Ast.info~previous~reserved~original:candidate~renamed:name|_->());Some(matchlabelwith|Somelabel->{labelwithdesc=name}|None->Ast.no_locname))elseNone),{ns;stack=(Option.map(fun(l:Src.name)->l.Wax_utils.Ast.desc)label,(name,used))::st.stack;})letgetst(idx:Src.idx)=letname,used=matchidx.descwith|Numn->(matchList.nth_optst.stack(Uint32.to_intn)with|Someentry->sndentry|None->raise(Unresolved_referenceidx.Ast.info))|Idid->(matchList.assoc_opt(Someid)st.stackwith|Someentry->entry|None->raise(Unresolved_referenceidx.Ast.info))inused:=true;{idxwithdesc=name}letmake()={ns=Namespace.make~kind:`Label();stack=[]}endmoduleCondTbl=struct(* A single Wax name may stand for several declarations across conditional
branches with different definitions (e.g. a function imported with a
different signature, hence a different arity, in each branch of an
[(@if …)]). Each declaration is recorded with the assumption under which
it holds, and a lookup resolves against the current branch's assumption,
so a reference in a given branch sees the matching declaration. With a
single declaration this degenerates to a plain name-keyed table. *)type'at=(string,(Cond.t*'a)list)Hashtbl.tletmake():_t=Hashtbl.create16letaddtblasmnamev=letprev=tryHashtbl.findtblnamewithNot_found->[]inHashtbl.replacetblname((asm,v)::prev)(* Raises [Not_found] when the name is unknown, like the plain table did. *)letfindtblasmname=matchHashtbl.findtblnamewith|[(_,v)]->v|entries->((* Resolve to the declaration whose branch is reachable under the
current assumption, pruning declarations from mutually-exclusive
branches. Falls back to the most recent if none is compatible
(only for a reference that is itself unreachable). *)matchList.find_opt(fun(c,_)->Cond.is_satisfiable(Cond.and_asmc))entrieswith|Some(_,v)->v|None->snd(List.hdentries))(* All declarations whose branch is reachable under [asm]. More than one
means the reference does not select a single branch. *)letcompatibletblasmname=matchHashtbl.find_opttblnamewith|None->[]|Someentries->List.filter_map(fun(c,v)->ifCond.is_satisfiable(Cond.and_asmc)thenSomevelseNone)entriesend(*** The conversion context ***)(* How a value's own printed form re-types it on a re-parse, as far as the
dead-code backing scan can classify it from the node (and the tables below):
a REFERENCE settled in a hierarchy — [eq] telling whether it is provably an
[eq]-subtype, i.e. a valid [ref.eq] operand, which only an [any]-hierarchy
reference other than a bare [&any] can be — a NULL (which every hierarchy
accepts), a NON-REFERENCE value, or unclassifiable. What the dead-code
reference pins ([ref.is_null] / [ref.eq] / the cross-hierarchy converts) ask
of a residual their hole would reconnect to (see [backing_class_of]). *)typebacking_class=|Ref_classof{hier:[`Any|`Extern|`Func|`Exn|`Cont];eq:bool}|Null_class|Value_class|Unknown_classtypectx={types:Sequence.t;struct_fields:(string,Sequence.t*stringlist)Hashtbl.t;globals:Sequence.t;functions:Sequence.t;memories:Sequence.t;tables:Sequence.t;tags:Sequence.t;datas:Sequence.t;elems:Sequence.t;referenced_elems:(string,unit)Hashtbl.t;(* Wax names of element segments used by table.init / elem.drop /
array.*_elem. A declarative segment is normally dropped (regenerated by
[to_wasm] from ref.func usage), but one that is referenced this way
needs an explicit declaration so the reference resolves. *)type_defs:Src.subtypeCondTbl.t;implicit_types:(Uint32.t,Src.functype)Hashtbl.t;(* Function types that the WAT text format synthesises from inline
[(param)]/[(result)] signatures (the type-use abbreviation), keyed by
the type index they occupy. The source AST keeps such uses inline and
does not materialise them as [Types] fields, so this table is what lets
a numeric [(type N)] elsewhere resolve to the implicit type. These types
are anonymous: they are rendered inline ([&fn(..)] / an inline [sign]),
never as a named Wax type. Empty for modules with conditional
annotations, where numeric references are forbidden anyway. *)mutablenamed_implicit:(string*Src.functype)list;(* Implicit function types that had to be given a name because they are
referenced from a ref-type position (where Wax has no inline
function-type form). Each is emitted as a [type <name> = fn(..)]
declaration; accumulated in reverse order of first use. *)function_types:Src.typeuseCondTbl.t;exports:(Src.exportable*string,(Cond.t*Wax_wasm.Ast.cond*Src.name)list)Hashtbl.t;(* Standalone [(export …)] fields, keyed by the Wax name of their target,
attached to that target as [#[export]] attributes. Each is paired with
the conditional-branch assumption under which it appears -- both the
solved form (for satisfiability/implication tests) and the syntactic
condition (for a [#[export …, if <cond>]] guard) -- so a target that
exists in several mutually exclusive branches receives only the exports
of its own branch, and an export narrower than its target's reachability
is emitted as a guarded attribute. *)starts:(string,(Cond.t*Wax_wasm.Ast.cond)list)Hashtbl.t;(* [(start …)] fields, keyed by the Wax name of their function, rendered as
a [#[start]] attribute on it rather than a separate field. As with
[exports], each is paired with the branch condition under which it
appears, so a start narrower than its function's reachability becomes a
guarded [#[start, if <cond>]] and mutually exclusive starts (at most one
per configuration) stay on their own functions. *)locals:Sequence.t;local_valtypes:(string,Ast.valtype)Hashtbl.t;(* The Wax type of each local (parameters included), keyed by the Wax name;
a fresh table per function, like [locals] itself. *)global_valtypes:(string,Ast.valtype)Hashtbl.t;(* The same for the module's globals, imported ones included, filled while
their names are registered (before any body is converted, so a forward
reference resolves).
[LocalGet]/[GlobalGet] record a NUMERIC type from these on the node they
emit (see [expect]), which is what tells [Stack.effective_backing] that
such a residual cannot be the reference backing a dead [ref.is_null] /
[ref.eq] hole: a [Get] carries no width tag and states no type of its
own — its type lives in its declaration — so without the record a
numeric local or global read looked like a reference and left the hole
unpinned, re-lowering [!] to [i32.eqz]. *)address_types:(string,Ast.valtype)Hashtbl.t;(* The address type ([i32], or [i64] under memory64) of each memory and
table, keyed by the Wax name — what [memory.size]/[memory.grow] and
[table.size]/[table.grow] return. Filled in the naming pre-pass, where the
field's limits are in hand, and read at those four instructions so a dead
residual of one is known not to be a reference backing (see
[Stack.effective_backing]). Memories and tables share one table: their Wax
names live in different index spaces but are drawn from one namespace, so
a name identifies at most one of them. *)multi_ref_results:(string,backing_classarray)Hashtbl.t;(* For a function with a MULTI-value signature: the classification of
each result, in result order (see [backing_class]), keyed by the Wax
name (like [address_types]). The expectation channel is single-valued,
so a multi-value call residual cannot record its composition on the
node; this is what lets a dead reference op ask what such a backing
would hand its reconnecting hole. Reconnection is POSITIONAL — a hole
takes the topmost pending value — so the consulting op indexes from
the entry's top by the claims interposed holes have already eaten (see
[backing_class_of]). Filled at each [Call] emission (any call node the
backing scan can see was emitted before the consulting op). *)labels:LabelStack.t;tag_types:Src.typeuseCondTbl.t;label_arities:(stringoption*int)list;block_params:Src.valtypearray;(* The parameters of the INNERMOST enclosing block, which it takes off the
enclosing stack and which are therefore its first stack values. A
reference among them BACKS a hole popped there, so the dead-code
reference pins ([ref.is_null]) leave it bare instead of pinning a
hierarchy of their own: the hole reconnects to the parameter on re-parse
and takes its type. A function's parameters are locals, not stack values,
so the function level leaves this empty. *)return_arity:int;strict_constants:bool;(* When set, every numeric constant is wrapped in a cast to its concrete
type ([0 as i32], [0.0 as f64], ...). This keeps Wax type inference
from re-typing an otherwise polymorphic literal, so a type mismatch in
the source survives the round-trip. *)faithful:bool;(* When set (the [--faithful] decompilation mode), the recoveries that
rewrite the instruction stream to a shorter or differently-shaped one
are turned off, so the decompiled Wax re-lowers to the exact original
opcodes. Here it keeps the [!(a == b)] form of [t.eq; i32.eqz] rather
than fusing it to [a != b] (which recompiles to a single [t.ne]); it is
also threaded into {!Recover_match} to disable the flat
[br_on_cast_fail]-chain arm. *)diagnostics:Wax_utils.Diagnostic.context;cond_env:Cond.env;plan:Wax_wasm.Cond_plan.t;(* The configuration plan the typer will build for the emitted module
(computed here over the source, which has the same conditionals at the
same spans): at each emitted conditional annotation, the branch the
typer's run owning the enclosing branch selects is the one whose
claims and leftovers are applied to the enclosing stack model, so the
scan predicts the claims of the world the typer types. *)cond_diag:Wax_utils.Diagnostic.context;mutablecond_asm:Cond.t;(* Assumption for the conditional branch currently being registered or
converted; threaded through [Module_if_annotation]/[If_annotation] so
the type tables above resolve to the right per-branch declaration. *)}(*** Names, indices, and type conversions ***)letget_annote=fste.Wax_utils.Ast.descletget_typee=snde.Wax_utils.Ast.desc(* Build a located [annotated_array] element ([name : type] in a struct, or a
subtype in a rec group), keeping the source location so a trailing comment
attaches to the whole entry. *)letannotatedlocat={Ast.desc=(a,t);info=loc}letidxctxkindi=matchkindwith|`Type->Sequence.getctx.typesi|`Global->Sequence.getctx.globalsi|`Func->Sequence.getctx.functionsi|`Mem->Sequence.getctx.memoriesi|`Table->Sequence.getctx.tablesi|`Tag->Sequence.getctx.tagsi|`Data->Sequence.getctx.datasi|`Elem->Sequence.getctx.elemsi|`Local->Sequence.getctx.localsiletlabelctxi=LabelStack.getctx.labelsi(* The Wax name for a concrete type reference [i] appearing in a ref-type. An
implicit (inline-signature) function type has no source name and is normally
rendered inline, but a ref-type position has no inline function-type form, so
such a type is given a name on first use and emitted as a [type] declaration
(see [named_implicit] / [extra_type_decls]). *)lettype_ref_namectx(i:Src.idx)=matchi.Ast.descwith|Src.NumnwhenHashtbl.memctx.implicit_typesn->letname=matchSequence.find_boundctx.typesnwith|Somename->name|None->letname=Sequence.mint_namectx.typesinSequence.bind_atctx.typesnname;ctx.named_implicit<-(name,Hashtbl.findctx.implicit_typesn)::ctx.named_implicit;namein{iwithdesc=name}|_->idxctx`Typei(* The spine ([heaptype]…[fieldtype]) copies each constructor through, naming
each index via [type_ref_name]; [functype]/[comptype]/[subtype] below stay
hand-written because they allocate Wax names (with rename diagnostics) and
look up struct-field names. *)moduleMap=Wax_wasm.Ast.Map_types_spine(Src)(Ast)(structtypenonrecctx=ctxletidxsti=type_ref_namestiend)letheaptype=Map.heaptypeletreftype=Map.reftypeletvaltype=Map.valtypeletstoragetypectx(st:Src.storagetype):Ast.storagetype=matchstwithValuev->Value(valtypectxv)|Packedp->Packedp(* Convert one WAT data-segment element back to a Wax data element: a string
stays a string, a scalar numlist becomes a [Data_run] of literal strings (the
type is stated once, so nan/inf need no suffix), and a [v128] run stays one
[Data_v128] grouping all its constants (preserving the WAT grouping). *)letdata_elem_to_waxctx(e:(Src.datavalelem,Ast.location)Ast.annotated):Ast.data_elem=matche.Ast.descwith|Strs->Ast.Data_strings|Numlist(st,vals)->Ast.Data_run(storagetypectxst,List.mapAst.no_locvals)|V128listvs->Ast.Data_v128(List.mapAst.no_locvs)letdata_init_to_waxctxinit=List.map(data_elem_to_waxctx)init(* Render a function type's parameters into a fresh namespace, renaming a named
parameter that is a reserved word or collides with an earlier one (and
warning about it, as for any other declared name). Unnamed parameters stay
anonymous. Shared by function-type definitions and inline signatures. *)letfunctype_paramsctxparams=letns=Namespace.make()inArray.map(funp->letid,t=p.Wax_utils.Ast.descinletid=Option.map(funid->letname,outcome=Namespace.add'~loc:(id:Src.name).Wax_utils.Ast.infonsid.Wax_utils.Ast.descin(matchoutcomewith|Namespace.Renamed{reserved;previous}->Sequence.report_renamectx.diagnostics~location:id.Ast.info~previous~reserved~original:id.Ast.desc~renamed:name|Namespace.Available->());{idwithAst.desc=name})idin(* Keep the parameter's source location on the Wax side too. *)annotatedp.Ast.infoid(valtypectxt))paramsletfunctypest(t:Src.functype):Ast.functype={params=functype_paramsstt.params;results=Array.map(funt->valtypestt)t.results;}letmuttypetypst(t:_Src.muttype):_Ast.muttype={twithtyp=typstt.typ}letfieldtype=Map.fieldtypeletcomptypestname(t:Src.comptype):Ast.comptype=matchtwith|Funct->Func(functypestt)|Structl->letseq=fst(Hashtbl.findst.struct_fieldsname)inStruct(Array.mapi(funit->letid=Sequence.getseq(matchget_annottwith|None->Ast.no_loc(Src.Num(Uint32.of_inti))|Someid->{idwithdesc=Idid.Wax_utils.Ast.desc})inannotatedt.Ast.infoid(fieldtypest(get_typet)))l)|Arrayt->Array(fieldtypestt)|Conti->Cont(idxst`Typei)letsubtypestname(t:Src.subtype):Ast.subtype={typ=comptypestnamet.typ;supertype=Option.map(funi->idxst`Typei)t.supertype;final=t.final;descriptor=Option.map(funi->idxst`Typei)t.descriptor;describes=Option.map(funi->idxst`Typei)t.describes;}letrectypest(t:Src.rectype):Ast.rectype=Array.map(fun(t:(_,Ast.location)Ast.Annot.annotated)->letname:Ast.ident=Sequence.get_currentst.typesinannotatedt.infoname(subtypestname.desc(get_typet)))tletglobaltypest=muttypevaltypest(* Remember a global's non-reference type under its Wax name, for the
numeric-residual test in [Stack.effective_backing] (see [global_valtypes]).
Read off the SOURCE type rather than through [globaltype]: this runs while
the names are being registered, before the type section is, so resolving a
reference type here would report an unbound name. Only the non-reference
types are wanted anyway — they rule a residual out as a reference, [v128]
included (its record is what tells [effective_backing] a dead vector
residual is not the reference a bare hole reconnects to; leaving it out was
a recording gap the [--debug width-record] census found) — and they need no
resolution. *)letrecord_global_valtypectx(typ:Src.globaltype)name=matchtyp.Wax_wasm.Ast.typwith|I32->Hashtbl.replacectx.global_valtypesnameAst.I32|I64->Hashtbl.replacectx.global_valtypesnameAst.I64|F32->Hashtbl.replacectx.global_valtypesnameAst.F32|F64->Hashtbl.replacectx.global_valtypesnameAst.F64|V128->Hashtbl.replacectx.global_valtypesnameAst.V128|Ref_->()(*** Type lookup and arity ***)type_kind=|Type:Src.subtypekind|Func:Src.typeusekind|Tag:Src.typeusekind(* Run [f] with [ctx.cond_asm] extended by the branch condition [cond] (taken
positively for [@then], negatively for [@else]), restoring it afterwards.
Used in both the name-registration passes and the conversion so that type
declarations are recorded under, and references resolved against, the
assumption of the branch they appear in. *)letwith_condctx~locationcondpositivef=letsaved=ctx.cond_asminletc=Cond.of_condctx.cond_envctx.cond_diag~locationcondinctx.cond_asm<-Cond.and_saved(ifpositivethencelseCond.not_c);Fun.protect~finally:(fun()->ctx.cond_asm<-saved)fletlookup_type(typetyp)ctx(kind:typkind)idx:typ=letgetseqtblidx=CondTbl.findtblctx.cond_asm(Sequence.getseqidx).descinmatchkindwith|Type->getctx.typesctx.type_defsidx|Func->getctx.functionsctx.function_typesidx|Tag->getctx.tagsctx.tag_typesidxletregister_type(typetyp)?hintctxexport_tbl(kind:typkind)idxexports(typ:typ)=letregisterseqtblkindidx=CondTbl.addtblctx.cond_asm(Sequence.register'?hintseqexport_tblkindidxexports)typinmatchkindwith|Type->assertfalse|Func->registerctx.functionsctx.function_types(SomeFunc)idx|Tag->registerctx.tagsctx.tag_types(SomeTag)idx(* The source module is converted without being validated first (validation is
off by default), so it may be type-invalid in ways the conversion cannot
represent. Report such a case and abort the conversion rather than crashing
on an [assert false]. *)letconversion_errorctx~locationmessage=Wax_utils.Diagnostic.reportctx.diagnostics~location~severity:Error~message();Wax_utils.Diagnostic.abort()(* The field sequence and names of the struct type [type_name] refers to.
[ctx.struct_fields] holds only struct types, so a miss means the index names a
non-struct type -- a [struct.new]/[.get]/[.set] validation would reject.
Report it and abort like other conversion errors rather than crash on the
missing table entry. *)letstruct_fieldsctxtype_name=matchHashtbl.find_optctx.struct_fieldstype_name.Wax_utils.Ast.descwith|Somefields->fields|None->conversion_errorctx~location:type_name.Ast.info(Wax_utils.Message.text"This type should be a struct type.")(* Decompilation ergonomics: when a reconstructed struct's leading fields exactly
match (name and type) its supertype's full field list, replace that prefix
with a [..] splice sentinel so the printer renders [type c: p = { .., delta }].
A renamed or covariantly-refined inherited field breaks the match and stays
explicit. Field types are compared at the Src level, which carries no Wax
source locations (Ast field types would differ on location alone). The
supertype is always defined-before (earlier in the group or in an earlier
group), so the reconstructed [..] re-typechecks. *)letcollapse_splicesctx(rt:Ast.rectype):Ast.rectype=letsrc_structname=matchtrySome(CondTbl.findctx.type_defsctx.cond_asmname.Wax_utils.Ast.desc)withNot_found->Nonewith|Some{Src.typ=Structfields;_}->Somefields|_->Nonein(* Compare field types without their source locations (which differ between a
supertype's declaration and the subtype's copy): [Src] text-format indices
carry a location, so print the reconstructed Wax type and compare that. *)letsame_type(a:Ast.fieldtype)(b:Ast.fieldtype)=lets(ft:Ast.fieldtype)=Wax_utils.Printer.run_string(funpp->Wax_lang.Output.storagetypeppft.typ)ina.mut=b.mut&&String.equal(sa)(sb)inArray.map(funelt->let(name:Ast.ident),(sub:Ast.subtype)=elt.Wax_utils.Ast.descinmatch(sub.typ,sub.supertype)with|Structchild_ast_fields,Someparent_name->(match(src_structparent_name,Hashtbl.find_optctx.struct_fieldsparent_name.Ast.desc)with|Someparent_src,Some(_,parent_names)->letparent_names=Array.of_listparent_namesinletn=Array.lengthparent_srcinletprefix_matches=(* [n = 0] would splice nothing, so [..] is pure noise there. *)n>=1&&n<=Array.lengthchild_ast_fields&&n<=Array.lengthparent_names&&letok=reftrueinfori=0ton-1doifnot(String.equal(fstchild_ast_fields.(i).Ast.desc).Ast.descparent_names.(i)&&same_type(sndchild_ast_fields.(i).Ast.desc)(fieldtypectx(get_typeparent_src.(i))))thenok:=falsedone;!okinifprefix_matchesthenletdelta=Array.subchild_ast_fieldsn(Array.lengthchild_ast_fields-n)inletfields=Array.append[|Ast.splice_fieldname.Ast.info|]deltain{eltwithAst.desc=(name,{subwithtyp=Structfields})}elseelt|_->elt)|_->elt)rtletfunctype_arity{Src.params;results}=(Array.lengthparams,Array.lengthresults)(* The implicit (anonymous) function type a numeric [(type N)] denotes, if [N]
was synthesised from an inline signature; [None] for a named/explicit type or
a symbolic reference. Consulted before the named-type tables so such a
reference resolves to its signature rather than raising. *)letimplicit_functypectx(idx:Src.idx)=matchidx.Ast.descwith|Src.Numn->Hashtbl.find_optctx.implicit_typesn|Id_->None(* The type to RECORD on a call's result node, so a dead call residual is known
not to be the reference a bare hole reconnects to (see
[Stack.effective_backing]): the single non-reference result — or, the
expectation channel being single-valued, the FIRST result of a multi-value
signature none of whose results is a reference. On a multi-value node the
record's only reader is the backing scan's not-a-reference test (the width
reconciliation and the census look at single-cell nodes only), and "provably
no reference among these values" is exactly what it asks: unrecorded, an
all-numeric pair read as `Backing`, a dead [ref.is_null]'s hole was left
bare, and on re-parse the pair was consumed by earlier numeric holes and the
bare [!_] re-defaulted to [i32.eqz] (a backing-scan grid finding). [None]
for a void signature or any signature with a reference result, whichever
position it is in: a reference result is exactly what must stay a candidate
backing. *)letfunctype_value_resultctx{Src.results;_}=letnonreft=matchvaltypectxtwithAst.Ref_->None|t->SometinmatchArray.to_listresultswith|[]->None|t::rest->ifList.for_all(funt->Option.is_some(nonreft))restthennonreftelseNonelettype_arityctxidx=matchimplicit_functypectxidxwith|Somety->functype_arityty|None->(match(lookup_typectxTypeidx).typwith|Functy->functype_arityty|Struct_|Array_|Cont_->conversion_errorctx~location:idx.Ast.info(Wax_utils.Message.text"This type should be a function type."))(* The value type a NON-PACKED, non-reference field or element holds — what an
UNSIGNED aggregate read yields ([struct.get]/[array.get]), recorded so a dead read
is known not to be a reference backing (see [Stack.effective_backing]). A packed
field is read through the signed/unsigned path, whose i32 result is recorded
there; a reference field records nothing, since that is exactly the value a hole
may reconnect to. The field is located by the name its immediate resolves to,
against the ordered name list [ctx.struct_fields] already keeps for the type. *)letsrc_typedefctx(name:Ast.ident)=trySome(CondTbl.findctx.type_defsctx.cond_asmname.Wax_utils.Ast.desc)withNot_found->Noneletfield_value_typectx(ft:Src.fieldtype)=matchft.Src.typwith|Src.Valuev->(matchvaltypectxvwithRef_->None|t->Somet)|Src.Packed_->Noneletstruct_field_value_typectxtype_name(field_name:Ast.ident)=matchsrc_typedefctxtype_namewith|Some{Src.typ=Structfields;_}->(letnames=Array.of_list(snd(struct_fieldsctxtype_name))inletrecpositionk=ifk>=Array.lengthnamesthenNoneelseifString.equalnames.(k)field_name.Wax_utils.Ast.descthenSomekelseposition(k+1)inmatchposition0with|Somekwhenk<Array.lengthfields->field_value_typectx(get_typefields.(k))|_->None)|_->Noneletarray_element_value_typectxtype_name=matchsrc_typedefctxtype_namewith|Some{Src.typ=Arrayft;_}->field_value_typectxft|_->Nonelettype_value_resultctxidx=matchimplicit_functypectxidxwith|Somety->functype_value_resultctxty|None->(match(lookup_typectxTypeidx).typwith|Functy->functype_value_resultctxty|Struct_|Array_|Cont_->None)(* Resolve a typeuse to its function type, through an implicit or a named
type; [None] if the name resolves to no function type. *)lettypeuse_functypectx((i,ty):Src.typeuse)=matchtywith|Someft->Someft|None->(matchiwith|Somei->(matchimplicit_functypectxiwith|Someft->Someft|None->(match(lookup_typectxTypei).typwith|Funcft->Someft|Struct_|Array_|Cont_->None))|None->None)lettypeuse_value_resultctx(i,ty)=match(i,ty)with|_,Somet->functype_value_resultctxt|Somei,None->type_value_resultctxi|None,None->Nonelettypeuse_arityctx(i,ty)=match(i,ty)with|_,Somet->functype_arityt|Somei,None->type_arityctxi|None,None->assertfalseletblocktype_arityctx(typ:Src.blocktypeoption)=matchtypwith|None->(0,0)|Some(Valtype_)->(0,1)|Some(Typeuset)->typeuse_arityctxt(* The types a block takes off the enclosing stack as its own parameters (see
[ctx.block_params]). A [Valtype] blocktype declares a result, not a parameter,
and a missing one declares neither. *)letblocktype_paramsctx(typ:Src.blocktypeoption):Src.valtypearray=letof_functype{Src.params;_}=Array.map(funp->sndp.Wax_utils.Ast.desc)paramsinmatchtypwith|None|Some(Valtype_)->[||]|Some(Typeuse(i,ty))->(match(i,ty)with|_,Somet->of_functypet|Somei,None->((* Through [implicit_functype] first, as [type_arity] does: a blocktype
index may name a type synthesised from an inline signature, which the
named-type tables do not hold — looking it up there reports it as an
unbound reference. *)matchimplicit_functypectxiwith|Somet->of_functypet|None->(match(lookup_typectxTypei).typwith|Funct->of_functypet|Struct_|Array_|Cont_->[||]))|None,None->[||])(* The arity used to convert a reference (how many operands a call consumes) is
fixed in the produced Wax, so it must be the same in every branch reachable
here. If a name is declared with different arities in mutually-exclusive
branches and the reference does not select one (e.g. it sits in unconditional
code, as [dv_make] does in io.wat), there is no single faithful conversion;
report it rather than emit a wrong-arity call. *)letchecked_arityctxkindtblwhatname_idxcompatible=letarity=typeuse_arityctx(lookup_typectxkindname_idx)inletname=(Sequence.gettblname_idx).Ast.descin(matchcompatiblectx.cond_asmnamewith|_::_::_aslwhenList.exists(funt->typeuse_arityctxt<>arity)l->Wax_utils.Diagnostic.reportctx.diagnostics~location:name_idx.Ast.info~severity:Error~message:(Wax_utils.Message.text(Printf.sprintf"%s $%s is declared with different arities in \
mutually-exclusive conditional branches but referenced where \
the branch is undetermined; this cannot be converted to Wax."whatname))()|_->());arityletfunction_arityctxf=checked_arityctxFuncctx.functions"Function"f(CondTbl.compatiblectx.function_types)lettag_arityctxt=checked_arityctxTagctx.tags"Tag"t(CondTbl.compatiblectx.tag_types)letlabel_arityctx(idx:Src.idx)=matchidx.descwith|Idid->(matchList.find_opt(fune->matchewithSomeid',_->id=id'|_->false)ctx.label_aritieswith|Somee->snde|None->raise(Unresolved_referenceidx.Ast.info))|Numi->(matchList.nth_optctx.label_arities(Uint32.to_inti)with|Somee->snde|None->raise(Unresolved_referenceidx.Ast.info))(* (parameter count, result count) of the function type a continuation type
wraps. *)letcont_arityctxidx=match(lookup_typectxTypeidx).typwith|Contft->type_arityctxft|Func_|Struct_|Array_->conversion_errorctx~location:idx.Ast.info(Wax_utils.Message.text"This type should be a continuation type.")(* Number of values a [switch] to continuation [ct] produces: the parameters of
the continuation referenced by the last parameter of [ct]'s function type. *)letswitch_outputctxct=match(lookup_typectxTypect).typwith|Contft->(match(lookup_typectxTypeft).typwith|Func{params;_}whenArray.lengthparams>0->(matchsndparams.(Array.lengthparams-1).Ast.descwith|Ref{typ=Typect2;_}->fst(cont_arityctxct2)|_->0)|Func_|Struct_|Array_|Cont_->0)|Func_|Struct_|Array_->0leton_clausectx(c:Src.on_clause):Ast.on_clause=matchcwith|OnLabel(tag,lbl)->OnLabel(idxctx`Tagtag,labelctxlbl)|OnSwitchtag->OnSwitch(idxctx`Tagtag)(*
Step 1: traverse types and find existing names
Step 2: use this info to generate using names without reusing existing names
*)(* Remember the address type of a memory or table under the Wax [name] it was just
registered under (see [ctx.address_types]). *)letrecord_address_typectxname(at:[`I32|`I64])=Hashtbl.replacectx.address_typesname(matchatwith`I32->Ast.I32|`I64->Ast.I64)(*** Recorded type expectations ***)(* A Wasm opcode states the type of every value it produces; the Wax surface form
this conversion prints only *re-infers* it, and the two silently disagreeing is
the "width drift" bug class the width pins below guard against (an unpinned
[i64] literal tree re-defaults to [i32] on re-parse, so [i64.div_u] recompiles
as [i32.div_u]). [expect] records the Wasm-stated type on the node itself
([Ast.instr]'s [expected]), so the typer — which already runs over this
conversion's output — can compare its own inference against it and report a
drift instead of shipping it (see {!Wax_lang.Typing.f}'s [~width_check]).
Recording is annotation only: it never changes what is emitted.
Every non-reference value type is recorded — the four numeric scalars and
[v128]. Only the scalars are ever CHECKED (the drift class is a flexible numeric
literal defaulting to [i32]/[f64], and [v128] has no member in that lattice, so
the reconciliation's arms skip it: [numeric_width]/[numeric_valtype] return
nothing for it). A recorded [v128] serves a second purpose the check does not:
it marks the value as PROVABLY NOT A REFERENCE for
{!Stack.effective_backing}, which otherwise reads an untagged residual as the
reference a bare hole reconnects to — a dead v128 residual there left a
[ref.is_null] unpinned and it re-parsed as an [i32.eqz] (a smith finding). A
reference type is still not recorded: that is the one class outside this
channel — marked [Contextual] ("considered, deliberately no claim") rather
than left [Unset], so an [Unset] numeric node in this conversion's output
always means a recording GAP (see [--debug width-record]). *)letrecorded_expectation(ty:Ast.valtype):Ast.expectation=matchtywith|I32|I64|F32|F64|V128->Recordedty|Ref_->Contextualletexpect(ty:Ast.valtype)(i:_Ast.instr):_Ast.instr={iwithAst.expected=recorded_expectationty}(* Mark [i] [Contextual]: a position whose re-parse type is fixed by the
construct it sits in, so it needs no claim of its own — an IMMEDIATE of the
printed form (a memarg label, a lane index, a vector-constructor component),
or the literal under a [Neg]/pin whose enclosing node carries the claim for
the whole (their cells are one). NOT for a value that sat on the conversion
stack: record what its opcode states instead. What this buys is that [Unset]
stays reserved for a recording GAP, which is what the census
([--debug width-record]) reports. *)letcontextual(i:_Ast.instr):_Ast.instr={iwithAst.expected=Ast.Contextual}(* A deliberately BARE hole: ADAPTIVE on the re-parse — it takes whatever type
its context demands, which is the behaviour the emission site wants where a
pin would state a type the Wasm side leaves polymorphic. [Contextual]
because the site considered it; a hole that must state its type is built
with {!typed_hole} instead (and the width repair pins whichever bare hole
would resolve wrong — see {!Wax_lang.Typing.f}'s [~width_check]). *)letbare_hole():_Ast.instr=contextual(Ast.no_loc_instrAst.Hole)(* Record a call's single non-reference result type on its node (see
{!functype_value_result}); a void, multi-value or reference-returning call is
left as is. *)letexpect_value_resulttye=matchtywithSomet->expectte|None->e(* Record the address type of the memory or table [name] on [e] — the result type of
its [size]/[grow] (see [ctx.address_types]). *)letexpect_address_typectx(name:Ast.ident)e=matchHashtbl.find_optctx.address_typesname.Ast.descwith|Somet->expectte|None->eletvaltype_of_width:[`I32|`I64|`F32|`F64]->Ast.valtype=function|`I32->I32|`I64->I64|`F32->F32|`F64->F64(* The type a cast's *result* has, which is the new node's expectation — not the
operand's. *)letcast_result(ty:Ast.casttype):Ast.expectation=matchtywith|Ast.Valtypety->recorded_expectationty|Ast.Ascribedty->recorded_expectationty|Signedtype{typ;_}->Recorded(valtype_of_widthtyp)|Functype_->Contextual(* Wrap [e] in a cast to [ty]. The single constructor for every cast this
conversion inserts: the [{ e with … }] copy would otherwise carry [e]'s own
expectation onto a node of a different type. *)letcast_to(ty:Ast.casttype)(e:_Ast.instr):_Ast.instr={ewithAst.desc=Ast.Cast(e,ty);expected=cast_resultty}(* Wrap [e] in the parenthesized type ascription [(e : ty)] — the CLAIM-FREE
grounding: an ascribed bare hole is grounded at [ty] without TYPING the
pending value it stands for ([Typing]'s ascription arm), and the node lowers
to no instruction ([To_wasm]'s [Ascribed] arm). Under an UNEQUAL conditional
annotation the same printed hole reads the polymorphic floor in one
configuration and a residual of any hierarchy in the other, so only a pin
that never types its value is right in both. This is what every "leave the
residual to the branch that consumes it" pin below is spelled with. *)letascribe_to(ty:Ast.valtype)(e:_Ast.instr):_Ast.instr={ewithAst.desc=Ast.Cast(e,Ascribedty);expected=cast_result(Ascribedty);}(* The typed hole [(_ as ty)] the conversions give an absent operand — a pop from
the polymorphic stack of dead code. Both nodes record [ty]: the opcode's
signature states the operand type whether or not a value was there to take. *)lettyped_hole(ty:Ast.valtype)=cast_to(Valtypety)(expectty(Ast.no_loc_instrAst.Hole))(* Drop the expectation recorded on [i] and on everything under it. Used where a
value's width comes from its CONTEXT rather than from its own printed form — a
block/function result, an initialiser, a branch delivery. The conversion leaves
such a value unpinned for exactly that reason, and the typer types it against
the context type instead of merging that type into the value's own cell, so the
type inferred for the node stays flexible and states nothing about the width the
value takes: a claim recorded there would be checked against a defaulted
flexible literal and misfire. It clears as far down as the context type
reaches, no further — a [do f32 { 3 + 4 }] exit is pinned by the block
annotation down to both literals, whereas a comparison's operands keep their
claim, their own printed form still having to carry their width. *)letrecforget_expected(i:_Ast.instr):_Ast.instr=letdesc:_Ast.instr_desc=matchi.Ast.descwith(* An arithmetic operator's result type IS its operands' — a context type
reaching the sum reaches them too, exactly as a pin cast on the sum would
(a comparison's i32 result says nothing about its operands, so it stops
here, and so does a cast, a call or a narrow store, which fix their
operand's type themselves). *)|Ast.BinOp(({Ast.desc=Add|Sub|Mul|Div_|Rem_|And|Or|Xor|Shl|Shr_;_;}asop),a,b)->Ast.BinOp(op,forget_expecteda,forget_expectedb)|Ast.UnOp(({Ast.desc=Neg|Pos;_}asop),a)->Ast.UnOp(op,forget_expecteda)(* A [select]'s arms share its result type; its condition does not. *)|Ast.Select(c,a,b)->Ast.Select(c,forget_expecteda,forget_expectedb)(* A sequence's value is its last element. *)|Ast.Sequence(_::_asl)->letrev=List.revlinAst.Sequence(List.rev(forget_expected(List.hdrev)::List.tlrev))(* A nested block's own exits were cleared by its own [run]. *)|d->din{iwithAst.desc;expected=Contextual}(*** The conversion stack ***)(* An operand tree whose printed form re-parses type-ADAPTIVELY: with no width or
type of its own it re-defaults (a numeric tree to [i32]). A bare hole is the
base case, and an untyped [select] is adaptive when BOTH arms are (its result
type is its arms'). Mirrors the typer's [reparse_adaptive] (kept local to
from_wasm so it carries no dependency on the typer). *)letrecreparse_adaptive(i:_Ast.instr)=matchi.Ast.descwith|Ast.Hole|Ast.Null->true|Ast.Select(_,a,b)->reparse_adaptivea&&reparse_adaptiveb|_->falsemoduleStack=struct(* [width] records the numeric result width the producing opcode states — a
const or arithmetic op tags its own width, everything else is [None]. It is
recorded on the value itself as it is pushed ({!expect}/[push_num]), and that
RECORD is what keeps the width across the round trip: the typer reconciles it
with what the printed Wax would re-infer and pins whatever would resolve
elsewhere (see {!Wax_lang.Typing.f}'s [~width_check]). Nothing here places a
width pin any more — a consumer whose surface erases its operand's width
([drop], [i32.wrap_i64], a comparison, [eqz]) simply pops it.
The tag lives on in the STACK for what it says about a value's flexibility,
which pinning cannot replace:
- a numeric residual is not a candidate backing for a bare hole (see
[effective_backing]: a [ref.is_null]/[ref.eq] operand is a reference);
- an arithmetic result is only width-FLEXIBLE while both its operands are, and
a method-form op inherits its receiver's flexibility — which decides the tag
a consumer sees and, for [drop], whether its [Let] carries a type
annotation. *)typewidth=[`I32|`I64|`F32|`F64]option(* Each entry is [(arity, width, instr)]: [arity] is the number of stack values
the instruction produces. Only a single value ([arity = 1]) can be popped as
an operand; [arity = 0] is a statement (a [nop], a void call, a branch whose
targets carry no value) and [arity >= 2] a multi-value residual, both of which
a pop reads as a hole; [arity = -1] is a value a block-shaped consumer took as
its parameter ([consume]) — it still prints as its own statement, but its
value is spoken for. The distinction matters for [effective_backing]: a
zero-value statement is transparent (a bare hole reconnects THROUGH it to
whatever is below), a value residual is not, and a consumed value cancels
against its consumer's parameter claim. *)typestack=(int*width*Ast.locationAst.instr)listtype'at=stack->stack*'aletreccompletencur=ifn=0thencurelsecomplete(n-1)(bare_hole()::cur)letrecgrab_recnstackcur=ifn=0then(stack,cur)elsematchstackwith|(1,_,instr)::rem->grab_rec(n-1)rem(instr::cur)|_->(stack,completencur)(* Whether a statement carries a conditional annotation: only such a
statement can have consumed — per configuration, in the source's spliced
validation — a value whose printed form the tree-typing then pairs with a
LATER hole. The scan reports it ([`Backing]'s [crossed]) so a caller whose
pin cannot prove the capture sound from the node alone (the [call_ref]
callee type pin) can fall back to the claim-free bottom pin only where the
hazard exists, keeping the annotation-free behaviour untouched. *)letrechas_cond_annotation(i:_Ast.instr)=matchi.Ast.descwith|Ast.If_annotation_->true|_->List.existshas_cond_annotation(Ast_utils.sub_instrsi)(* Mark the top value consumed: it prints as its own statement and the
block-shaped consumer pushed above takes it as its parameter on re-parse
(see [effective_backing]'s [-1] arm). When a conditional annotation stands
on top instead, the pairing is CONFIGURATION-dependent — the annotation's
branch may push the actual parameter — so the value below is not this
block's to mark: the block's re-parse claim would capture it and re-type
it (the backing-scan ScondPush cells grounded an adaptive [select] at the
parameter type, and the lowered module failed its own validation in the
configuration where the branch's push was the parameter). Inject
[param] — a synthetic, already-consumed claim-free bottom value of the
parameter's hierarchy — instead: it prints as a [_ as &?noextern;]
statement, lowers to nothing, satisfies the claim on re-parse, and leaves
the real value where the source's own consumers find it. *)(* The net CLAIMS of the plan-selected branch of a conditional annotation,
keyed physically by the emitted [If_annotation] node (set at emission,
where the plan's selection is read ([ctx.plan]); [hole_claims] reads
it when a scan walks past the annotation entry — the mirror of the
typer's spliced-branch typing, whose branch holes claim the enclosing
pendings positionally). Structural hash with PHYSICAL equality: two
structurally equal annotations at different stream points may select
different branches. *)letannotation_claims:(Obj.t,int)Hashtbl.t=Hashtbl.create16letset_annotation_claims(i:_Ast.instr)n=Hashtbl.replaceannotation_claims(Obj.repri)nletget_annotation_claims(i:_Ast.instr)=matchHashtbl.find_optannotation_claims(Obj.repri)with|Somen->n|None->0letconsumeinputsstack=ifinputs=0then(stack,())else((matchstackwith|(1,w,instr)::rem->(-1,w,instr)::rem(* A GHOST (a plan-selected branch's leftover, arity [-2]): already
printed inside the branch, so nothing is flushed later — the claim
is spent and the entry simply leaves the stack (the typer's block
parameter claims the branch's pending the same way). *)|(-2,_,_)::rem->rem|_->stack),())letgrabnstack=grab_recnstack[]letpusharityistack=((arity,None,i)::stack,())(* Record the tagged width as the value's expected type (see {!expect}): the
tag IS the width the producing opcode states, and recording it is all this
conversion does about width — the typer pins whatever would otherwise default
to another one (see {!Wax_lang.Typing.f}'s [~width_check]). An untagged value
([None]) keeps whatever its producer recorded: the tag says the value is
width-FLEXIBLE, not that its type is unknown (a grounded arithmetic result is
untagged yet has the opcode's width, recorded at the call site). *)letexpect_widthwi=matchwwithSomew->expect(valtype_of_widthw)i|None->i(* Push a numeric value tagged with the width its opcode states. *)letpush_numwidthistack=((1,width,expect_widthwidthi)::stack,())(* An unconditional control-flow instruction ([br]/[br_table]/[return]/[become]/
[unreachable]/[throw]/…) leaves the values still on the stack — below the
operands it consumed — dead: [run] emits them as leftover statements but no
consumer ever pops them. Their width rests on the expectation recorded when
they were pushed — a width-sensitive leftover (an [i64.div_u]/[i64.shr_u] whose
divisor or shift count is load-bearing) would re-default to i32 on re-parse and
trap / mask differently, and the typer pins it from that record. The tags are
dropped because nothing will pop these entries again. *)letpush_polyistack=letstack=List.map(fun(a,_,e)->(a,None,e))stackin((0,None,i)::stack,())(* Pop one operand. Whether the consumer's Wax surface carries the operand's
width or erases it ([drop], [i32.wrap_i64], a comparison, [eqz]) no longer
changes anything here: the value was annotated with its opcode's width when it
was PUSHED, and the typer pins it from that record if the printed form would
resolve elsewhere. An empty/absent stack reads as a hole (dead code). *)letpopstack=matchstackwith(1,_,i)::rem->(rem,i)|_->(stack,bare_hole())(* Pop with the width tag, without pinning — the caller decides. Used by [drop],
which records the tag in its [Let]'s type annotation rather than an identity
cast. A hole reads as an untagged empty pop. (Binops/comparisons/select use
[try_pop_tagged] instead, to tell a hole apart from a real operand.) *)letpop_taggedstack=matchstackwith|(1,w,i)::rem->(rem,(i,w))|_->(stack,(bare_hole(),None))lettry_popstack=matchstackwith(1,_,i)::rem->(rem,Somei)|_->(stack,None)(* The REFERENCE value a bare hole reconnects to, seen THROUGH interposed
entries that cannot be that value:
- a zero-value statement ([arity] 0) — a [nop], a void call, a [br_if] whose
condition was the value just above it;
- a NUMERIC value residual (a [Some] width tag): it cannot be a
[ref.is_null]/[ref.eq] operand (they take a reference), so in valid code it
is a leftover from a consumer whose grab an interposed statement blocked
(e.g. [i32.const c ; atomic.fence ; br_if] leaves [c] stranded, its role as
the [br_if] condition lost), not the operand the ref op actually pops.
[stop] marks the terminator sentinels ([arity] 0 too, but the polymorphic
bottom, so they stop the scan). Returns the first residual that CAN back the
hole — an [arity] >= 1 value that is neither tagged nor recorded — else [None]
(a terminator bottom, a numeric/vector residual only, or empty). A [None] tag
with no record also covers an untyped [select] of holes, correctly treated as a
backing that the caller then pins.
What is RECORDED, and therefore skipped, is the accurate list of what cannot be
a reference: every const and arithmetic result (the width tags); the
method-form ops, which record their opcode's type even when their tag stays
flexible; the loads; a local's or global's numeric or vector type; every SIMD
result (a [v128] record exists for this scan alone — the reconciliation skips
it); the i32-valued reference ops ([ref.test], [ref.eq], [ref.is_null],
[array.len], [i31.get_s/u]); the numeric conversions ([wrap], [promote],
[demote], [extend_i32]); a [Char]; a signed packed field or element read; and a
CALL whose signature returns one non-reference value ([call], [call_ref],
[call_indirect] — the callee's type is reachable at the push site even though it
is not from this scan).
With the aggregate reads and the memory/table sizes recorded, that list is
complete for a valid module: every instruction whose result is a numeric or
vector value now says so on its node, so the only residual this scan can
return is a value that really may be a reference. (The one thing it cannot
see is a value from a producer added later without a record — which is why
fuzz/ref-width.sh enumerates the shape and the round-trip legs
FAITHDRIFT/WIDTHDRIFT watch the rest.) *)(* A statement carrying a HOLE is NOT transparent to the scan below, however
zero-valued it is: on a re-parse its hole claims the first
value above it, so that value cannot also back a later hole. This is where the
scan's model used to diverge from both Wasm and Wax. In
[array.get ; atomic.fence ; drop ; ref.is_null] the WASM [drop] pops the
array element; the fence's zero-value entry only blocks the pop in THIS
conversion's stack, so the element is left as a residual and the [drop] emits
[_ = _]. Reading the element as a backing for the [ref.is_null] hole then
suppressed its pin — but on a re-parse the [_ = _] claims the element (exactly
as the Wasm [drop] did), leaving the bare [!_] to default to i32 and re-lower
as an [i32.eqz]: an opcode-family change. *)(* A hole the re-parse types NUMERICALLY is not such a claimant: it cannot take
the reference residual, so the statement stays transparent. The value a
[set]/[tee] writes to a numeric local or global records its type exactly for
this ([expect_local]/[expect_global]) — without it, [x = _] over a
[ref.null func] blocked the scan, the [ref.is_null] below it was pinned
[(_ as &?any)] as if bottom-sprung, and on re-parse the hole DID reconnect to
the func-hierarchy value: the pin crossed hierarchies and the decompiled Wax
did not type-check (a wat-mutation-fuzzer finding). *)(* How many values a statement claims from THIS stack on a re-parse: one per
UNTYPED hole (a hole whose recorded type is numeric claims no reference, as
above), plus a block's PARAMETERS, which it takes from here whether or not any
hole is involved. A block BODY runs on its own stack, which starts empty, so a
hole inside it claims nothing here — only the operands a block-shaped node
evaluates in the enclosing frame do: an [if]'s or [while]'s condition, a
[match]'s scrutinee. *)letrechole_claims(i:_Ast.instr)=matchi.Ast.descwith(* EVERY printed hole claims one value: the typer's claiming is positional
and type-blind ([count_holes] is syntactic), so a hole whose recorded
type is numeric still takes the next pending value — it merely pairs, in
a valid module, with the numeric residual the scan's value arms absorb
below. (The old model gave a recorded hole zero claims and skipped every
numeric residual unconditionally; the two cancelled only while the
pairing was type-consistent, which an [(@if)] breaks.) *)|Ast.Hole->1(* A conditional annotation claims what the branch the typer's plan selects
claims: that branch is typed SPLICED into the enclosing frame (its holes
take the enclosing values, its leftovers stay pending — see the
[If_annotation] arm of [instruction], which records the count at
emission), so the scan predicts the reconnection in the PRESERVED tree
whose types drive [To_wasm]. *)|Ast.If_annotation_->get_annotation_claimsi|Ast.Block{typ;_}|Ast.Loop{typ;_}|Ast.TryTable{typ;_}|Ast.Try{typ;_}|Ast.TryCatch{typ;_}->Array.lengthtyp.Ast.params|Ast.If{typ;cond;_}->Array.lengthtyp.Ast.params+hole_claimscond|Ast.While{cond;_}->hole_claimscond|Ast.Match{scrutinee;_}->hole_claimsscrutinee|_->List.fold_left(funns->n+hole_claimss)0(Ast_utils.sub_instrsi)(* The claims an entry's OWN tree makes on the stack below it. [ghost] says
they are already charged (a branch leftover: they were counted as the
annotation's claims), so the arm that skips it must not charge them
again. *)letown_claims~ghosti=ifghostthen0elsehole_claimsi(* [claims] counts the values the holes ABOVE are still owed: each takes the next
residual, so the scan skips that many before asking whether what it reaches can
back this hole. Counting them is what makes a hole-bearing statement
TRANSPARENT rather than opaque — [_.f = _] claims the two values sitting above
an extern residual, so the [ref.is_null] hole below it reconnects to that
extern and needs no pin at all. Read as an opaque blocker, the scan stopped one
entry early, pinned [(_ as &?any)] as if the hole were bottom-sprung, and on a
re-parse that pin became an [any.convert_extern]: a hierarchy crossing, and an
opcode the source never had (a wasm-smith FAITHDRIFT).
MAINTENANCE: this scan and [hole_claims] are a hand-rolled simulation of the
Wax re-parser's reconnection behaviour, and [fuzz/backing-scan.sh] enumerates
their input space exhaustively over an alphabet read off these match arms —
one representative per entry class. A new arm (a new entry kind, a new claim
shape) must add its representative there, or the guard degrades back to
fuzzing luck for exactly that arm. *)letreceffective_backingstop~crossed?(ghost=false)claims=function(* A CONSUMED value ([consume] marked it): it prints as its own statement,
and on the re-parse the block-shaped consumer above takes it as its
parameter — the very claim [hole_claims] charged for that consumer. The
two cancel: without this, the parameter charge ate a REAL value further
down and the scan pinned over a residual the hole in fact reconnects to
(the backing-scan grid's Bp1 cluster: the pin materialised as an
[any.convert_extern]). Spoken for, it can back nothing itself. *)(* A GHOST — a plan-selected branch's leftover value, printed inside
the branch: positionally it IS a pending of the enclosing frame (the
typer's spliced branch leaves it pending), so it absorbs a claim, backs
a reconnection, and stops the scan when adaptive, exactly like a
single-value entry — re-dispatched as one. Its own holes are NOT
charged here: they were counted as the annotation's branch claims. *)|(-2,w,i)::rem->effective_backingstop~crossed~ghost:trueclaims((1,w,i)::rem)|(-1,_,i)::rem->(* Its own tree may CARRY holes (a consumed [select] of holes): those
claim from this frame exactly like a statement's — the consumed
value prints as its own statement, whose holes run BEFORE the
consumer's parameter claim — so they are charged like [hole_claims]
of any other statement (a depth-4 grid finding: uncharged, the scan
read the extern the select's arm captures as the reader's backing,
and the bare [!_] re-defaulted to [i32.eqz]). *)effective_backingstop~crossed:(crossed||has_cond_annotationi)(max0(claims-1)+hole_claimsi)rem|(0,_,i)::_whenstopi->`Blocked|(0,_,i)::rem->effective_backingstop~crossed:(crossed||has_cond_annotationi)(claims+hole_claimsi)rem(* Numeric by its width TAG, or by the type its producer RECORDED on it
([Ast.instr]'s [expected], which only ever holds a numeric scalar): either
way it cannot be the reference operand, so keep scanning. The record is what
catches a residual the tag cannot: a method-form op inherits its receiver's
flexibility, so [f32.sqrt] of a hole is UNTAGGED while still being an f32 —
read as a reference backing, it left a dead [ref.eq] unpinned and it
re-parsed as an [i32.eq] (a bottom-fuzz finding). *)|(a,w,i)::remwhena>=1&&(w<>None||matchi.Ast.expectedwith|Ast.Recorded_->true|Ast.Unset|Ast.Contextual->false)->(* If holes above are still owed values, these are the values they take
— ALL of them for a multi-value entry (a record on one means every
result is non-reference, see [functype_value_result]) — and the scan
keeps looking. But a value the claims do NOT absorb is what the
reader's own hole captures (claiming is positional and type-blind),
and it is provably a NON-reference: report [`Value] so the caller
grounds its hole with the claim-free bottom pin — a bare hole would
re-default the op to its numeric family, and a top-of-hierarchy pin
would capture the value and fail to type. Reachable only through an
[(@if)] (whose branches consume the value per configuration); in
plain wasm the validator types the residual into the reference op
and rejects. *)ifclaims>=atheneffective_backingstop~crossed(claims-a+own_claims~ghosti)remelse`Value(* A value entry the holes above fully claim: not this hole's operand, so
keep looking past it — charging its OWN holes, which claim from this
stack exactly as a statement's do. A residual is flushed as a statement
AT its position, so a hole inside it takes a value below it before this
hole gets there: [ref.null any ; atomic.fence ; extern.convert_any]
leaves the convert's pinned hole [(_ : &?any)] claiming the null, and
reading the convert as a free skip let the scan walk on to the null and
call it this hole's backing — the bare [!_] below an [(@if)] that claims
the convert then reconnected to nothing and re-defaulted to [i32.eqz]
(a depth-4 backing-scan finding). The [-1] arm charges the same way. *)|(a,None,i)::remwhena>=1&&claims>=a->effective_backingstop~crossed(claims-a+own_claims~ghosti)rem(* An ADAPTIVE value — an untyped [select] of holes, or a bare hole — is not a
backing: its own printed form carries no hierarchy, so on a re-parse the ref
op's hole reconnects to it and the pair re-defaults to the NUMERIC form (a
[select] of holes becomes the i32 select and [!] on it an [i32.eqz]). It
stops the scan rather than being skipped: the hole reconnects to IT, so
whatever reference lies deeper is not what the printed form would find. The
caller then pins the hole, which grounds the reconnected value through the
same unification (as it already does for such a value popped directly as the
operand). *)|(a,None,i)::_whena>=1&&reparse_adaptivei->`Blocked(* The value the reader's hole captures. Claiming is POSITIONAL: the
[claims] still owed eat this entry's TOPMOST values, so the capture is
the entry's value [claims] positions from its top — 0 for a single-value
entry, and for a partially-claimed multi-value residual a middle result
(the classification indexes the signature accordingly; the old model
instead skipped the whole entry, losing the reference a claimed-past
multi still hands the hole — the VmultiER grid cells). *)|(a,None,i)::_whena>=1->`Backing(i,claims,crossed)(* The two no-backing outcomes are NOT the same: [`Floor] means the scan
walked cleanly to the block's own floor — where the enclosing block's
PARAMETERS are the next stack values, so a reference among them can still
back the hole (see [ctx.block_params]) — while [`Blocked] means a
terminator sentinel or a hole-bearing statement stands between: the printed
hole is bottom-sprung there and reconnects to nothing, so nothing (a block
parameter included) can back it. Conflating them let a block's reference
parameter suppress the pin THROUGH an [unreachable] — [do (&?extern) {
unreachable; !_ }] re-defaulted to [i32.eqz] even though the parameter was
real (a ref-width grid finding). *)|[]->`Floor|_->`Blocked(* [claims] seeds the scan with the hole count of the consulting statement's
SIBLING operands: a receiver is its statement's deepest operand, so its
positional capture sits below the values its shallower siblings' own holes
take first.
Returned alongside the verdict: whether a conditional annotation sits
ANYWHERE in the stack. A [`Backing]'s own [crossed] is path-precise; this
whole-stack flag is for the [`Floor]/[`Blocked] outcomes, where a CLAIMING
pin is unsafe once an annotation is in play — a branch's pushes satisfy
the interposed claims in the spliced configurations, so a value the scan
counted as absorbed is still what the pin would capture there (the
backing-scan ScondPush cells: [(_ as &?any)] captured a funcref an
interposed [drop] released to it and the module no longer type-checked).
Over-approximate (an annotation below the scan's stopping point counts
too): the downgrade it triggers — the claim-free bottom pin — is inert
wherever the claiming pin was. *)leteffective_backing?(claims=0)stopstack=letcrossed_any=List.exists(fun(_,_,i)->has_cond_annotationi)stackin(stack,(effective_backingstop~crossed:falseclaimsstack,crossed_any))(* [try_pop] carrying the width tag — a method-form op tags its result with its
receiver's flexibility, so an erasing consumer pins it (and the pin, cast on
the result, propagates back to the receiver: [((5).clz()) as i64] is
[i64.clz]). *)lettry_pop_taggedstack=matchstackwith|(1,w,i)::rem->(rem,Some(i,w))|_->(stack,None)(* Flush the leftover stack as statements. A value stranded past a conditional
branch ([br_if]/[br_on_null]/[br_on_cast]/…) is popped by neither a
width-erasing consumer nor [push_poly] (which only fires at an
*unconditional* terminator): the branch pushes a statement entry
([present = false]) on top of it, so it can no longer be consumed and
reaches here as a leftover — its width tag would otherwise be lost, and a
load-bearing [i64.shr_u]/[f32.sqrt] leftover would re-default to i32/f64 on
re-parse (masking/precision change). Pin such a stranded value from its
discarded tag.
Only a *present* stranded value is pinned: a present value that a statement
entry sits above (closer to the top) can never be popped again, so it is a
genuine leftover. Two shapes keep the pin off values whose width is already
fixed by context (where a pin would be redundant cast noise): the block's own
RESULTS — the top [results] present entries, fixed by the block/function/
const-initialiser type — and a value [consume] flipped to [present = false]
as a block input, fixed by the block's declared input type. A value consumed
later pops normally and never reaches [run].
[results] is the block's declared output arity: the top [results] present
entries are its results and stay unpinned; a present entry BEYOND that count
(or below a statement) is an excess leftover — a value stranded below the
block's own results with no statement between them (the block-arity gap) is
otherwise mistaken for a result and never pinned, so a width-tagged leftover
([f64.trunc]) narrows on re-parse. Callers that cannot state an arity default
to the old leading-present-run heuristic ([max_int] = every leading present
entry is a result); the control constructs pass their real output count. *)letrun_stack?(results=max_int)st=letrecpin_strandedresults_leftbelow_stmt=function|[]->[](* A ghost is already printed inside its branch — nothing to flush; it
still marks everything deeper as below a statement. *)|(-2,_,_)::rem->pin_strandedresults_lefttruerem|(arity,_,i)::rem->(* Only a single-value entry ([arity = 1]) is a pinnable leftover; a
statement ([0]) or multi-value residual is left as is and, like a
statement below it, marks everything deeper as below-statement. *)letpresent=arity=1inletis_result=present&&(notbelow_stmt)&&results_left>0in(* A leftover keeps the width recorded on it at push time, which is
what the typer pins it from. A RESULT instead has its expectation
CLEARED: its width comes from the enclosing block/function/initialiser
type, and the typer types such a value against that context type
rather than merging it into the value's own cell, so the value's own
inferred type stays flexible and says nothing about the width it will
take. *)leti=ifpresent&&is_resultthenforget_expectedielseiinletresults_left=ifis_resultthenresults_left-1elseresults_leftini::pin_strandedresults_left(below_stmt||notpresent)reminList.rev(pin_strandedresultsfalsest)letrun?resultsf=letst,()=f[]inrun_stack?resultsstendlet(let*)efst=letst,v=estinfvstletreturnvst=(st,v)letsequencel=matchlwith[i]->i|_->Ast.no_loc_instr(Ast.Sequencel)(*** Instruction-conversion helpers ***)letis_integer=letint_re=Re.(compile(whole_string(alt[rep1(alt[rg'0''9';char'_']);seq[str"0x";rep1(alt[rg'0''9';rg'a''f';rg'A''F';char'_']);];])))infuns->Re.execpint_resletis_negativen=n.[0]='-'letremove_signn=ifn.[0]='-'||n.[0]='+'thenString.subn1(String.lengthn-1)elsen(* A Wax operator carries its own source location; reuse the (source or target)
instruction's, which is the best approximation we have when reconstructing
from Wasm. Polymorphic in the carried [desc] so it works for either AST. *)(* A located operator, sharing the span of the node it was recovered from. Takes
the span rather than the node: the node can be an [annotated] or an instruction,
which are no longer the same shape. *)letop_loc(loc:Ast.location)op:(_,Ast.location)Ast.annotated={Ast.desc=op;info=loc}(* [loc] is the span of the instruction the literal was decoded from. *)letinteger(loc:Ast.location)n:_Ast.instr=letatdesc:_Ast.instr={desc;info=loc;hints=Wax_wasm.Hints.none;expected=Unset}inlete=at(Int(remove_signn))in(* The literal under the [Neg] is [Contextual]: the [Neg] node is the value,
and whatever claim its consumer records lands there — the two share one
inference cell, so a claim on the literal itself would be redundant. *)ifis_negativenthenat(UnOp(op_loclocAst.Neg,contextuale))elseeletfloatin=(* Test the magnitude, not the signed string: a negative integer-valued float
(e.g. [-4.0] printed as [-4]) must take the [integer] path too, else it
becomes a [Float] node whose integer-looking text ([-4]) re-lexes as an
integer literal on the round-trip — dropping the block/cast annotation that
pinned it to a float and leaving [.to_bits()] applied to an [i64]. *)ifis_integer(remove_signn)thenintegeri.Src.infonelselete:_Ast.instr={desc=Float(remove_signn);info=i.Src.info;hints=Wax_wasm.Hints.none;expected=Unset;}inifis_negativenthen{(* As in [integer]: the claim carrier is the [Neg] node. *)Ast.desc=UnOp(op_loci.Src.infoAst.Neg,contextuale);info=i.Src.info;hints=Wax_wasm.Hints.none;expected=Unset;}elseeletsequence_optl=matchlwith|[]->None|[i]->Somei|l->Some(Ast.no_loc_instr(Ast.Sequencel))letreasonable_string=Re.(compile(whole_string(rep(alt[diffany(rg'\000''\031');char'\n';char'\r';char'\t']))))letstring_argsnargs=ifn=Uint32.zerothenNoneelseletbyte_of_argarg=matcharg.Ast.descwith|Ast.Intc->((* [int_of_string_opt]: a byte value too large for an [int] (let alone a
byte) is simply not a string byte, not a crash. *)matchint_of_string_optcwith|Somecwhenc>=0&&c<256->Somec|_->None)|Ast.CharcwhenUchar.to_intc<128->Some(Uchar.to_intc)|_->NoneintryifUint32.of_int(List.lengthargs)<>nthenraiseExit;letb=Bytes.create(Uint32.to_intn)inList.iteri(funiarg->matchbyte_of_argargwith|Somec->Bytes.setbi(Char.chrc)|None->raiseExit)args;lets=Bytes.to_stringbinifString.is_valid_utf_8s&&Re.execpreasonable_stringsthenSomeselseNonewithExit->None(* As [string_args], but for an [i16] array: each argument is a UTF-16 code unit
(0..0xffff), decoded back to the source string. Falls back ([None]) on a
value out of range or a lone surrogate, so a genuine numeric array stays one. *)letwide_string_argsnargs=ifn=Uint32.zerothenNoneelseletunit_of_argarg=matcharg.Ast.descwith|Ast.Intc->(matchint_of_string_optcwith|Somecwhenc>=0&&c<0x10000->Somec|_->None)|Ast.CharcwhenUchar.to_intc<0x10000->Some(Uchar.to_intc)|_->NoneintryifUint32.of_int(List.lengthargs)<>nthenraiseExit;letunits=List.map(funarg->matchunit_of_argargwithSomec->c|None->raiseExit)argsinmatchWax_utils.Unicode.utf16_decodeunitswith|SomeswhenRe.execpreasonable_strings->Somes|_->NonewithExit->Noneletinttypety:Ast.valtype=matchtywith|`I32->I32|`I64->I64|`F32->I32|`F64->I64|_->assertfalse(* The scalar type one lane of a SIMD shape holds — the type a lane EXTRACTION
produces (a narrow [i8x16]/[i16x8] lane extends to an i32, as in Wasm). *)letlane_valtype(s:Wax_wasm.Ast.vec_shape):Ast.valtype=matchswith|I8x16|I16x8|I32x4->I32|I64x2->I64|F32x4->F32|F64x2->F64letfloattypety:Ast.valtype=matchtywith|`I32->F32|`I64->F64|`F32->F32|`F64->F64|_->assertfalseletint_un_op~faithfuli0sz(op:Src.int_un_op)=(* A Wax instruction at the source instruction's span. Built fresh rather than
with [{ i0 with desc }]: a Wasm and a Wax instruction differ in the type of
their call-target hints, so one cannot be reinterpreted as the other. *)letwith_loc(i:_Ast.instr_desc):_Ast.instr={desc=i;info=i0.Src.info;hints=Wax_wasm.Hints.none;expected=Unset;}in(* A no-argument instruction method [recv.meth()]. *)letmethod_callrecvmeth=with_loc(Call(with_loc(StructGet(recv,Ast.no_locmeth)),[]))inlet*recv=Stack.try_pop_taggedinlete'=Option.mapfstrecvin(* The operand's own width tag (its flexibility): a method-form op below
([clz]/[ctz]/[popcnt]/[extend8_s]/[extend16_s]) has result width = receiver
width, so it carries the receiver's flexibility to its result — an erasing
consumer then pins it, and the pin (a cast on the result) propagates back to
the receiver. Ops that fix a concrete result width (a cast: [trunc], [eqz]'s
i32) are grounded, [None]. *)letrecv_w=matchrecvwithSome(_,w)->w|None->Noneinletety=matche'withSomee->e|None->typed_holetyin(* Materialise the operand of a TRUNCATION with its float width [ty] cast on,
when the operand is inlined. The reconciliation would place the same cast for
a valid module (the truncation's surface [as int] carries the RESULT width, so
the operand's recorded width is what the typer pins it from — verified
byte-identical over the corpus and by fuzz/drop-width.sh), and this is kept
for the two things it does that the reconciliation cannot:
- a source module the validator would REJECT keeps its ill-typedness visible:
without the cast, [(0 as i32) as i64_s_strict] re-reads as an integer
extend — a different, well-typed program — instead of a truncation whose
operand is not a float (the spec suite asserts that Wax typing mirrors Wasm
validation on such a module, see test/wasm_test_suite.expected);
- it keeps the [eqz] special case below matchable ([sz] is [i32] there, so no
cast is added and the [BinOp] shape shows through).
An [i32] target needs no cast (i32 is the re-parse default) and an absent
operand is already the typed hole [e] builds. *)letpinty=letx=etyinmatch(e',ty)with|Some_,(Ast.I64|F32|F64)->cast_to(Valtypety)x|_->xin(* Width-preserving method-form ops carry the receiver's flexibility; the rest
produce a concrete (grounded) result. *)letresult_w=matchopwith|Clz|Ctz|Popcnt|ExtendS(`_8|`_16)->recv_w|_->Nonein(* The type the opcode's result HAS, whatever the flexibility tag says: [eqz]
yields i32 whatever its operand's width, every other op here the integer
size the opcode names. Recorded so a re-inference at another width is
caught — and, for a width-preserving method ([.clz()] and friends), that
record is what pins an adaptive receiver too: the typer's pin lands on the
call's RESULT and reaches the receiver through it ([i64.clz] on a
select-of-holes would otherwise re-parse as [i32.clz]). *)letresult_ty=matchopwithEqz->Ast.I32|_->inttypeszinStack.push_numresult_w@@expectresult_ty(matchopwith|Clz->method_call(e(inttypesz))"clz"|Ctz->method_call(e(inttypesz))"ctz"|Popcnt->method_call(e(inttypesz))"popcnt"|Eqz->(letoperand=pin(inttypesz)inmatchoperand.Ast.descwith(* [eqz] of an equality is exactly the negated comparison; recover
[i32.eqz (ref.eq a b)] — how [a != b] on references lowers — as
[a != b] rather than [!(a == b)]. ([sz] is [i32] here, so [pin] leaves
the [BinOp] shape untouched.) Under [--faithful] this rewrite is off:
it turns [t.eq; i32.eqz] into a single [t.ne], so keep the [!(...)]
form, which re-lowers to the original [eq; eqz] pair. *)|BinOp({Ast.desc=Ast.Eq;_},e1,e2)whennotfaithful->with_loc(BinOp(op_loci0.infoAst.Ne,e1,e2))|_->with_loc(UnOp(op_loci0.infoAst.Not,operand)))|Trunc(f,signage)->(* The operand is a float of [f]'s width, NOT [floattype sz] ([sz] is the
integer *result* size — wrong for e.g. [i32.trunc_f64], whose operand
is f64 not f32). Unlike the trunc's own [as int] cast (which fixes the
*result* width), nothing here pins the *source* float width, so an
inlined operand must carry it explicitly: a bare float literal
re-defaults to f64 (so an f32 source drifts), and an integer-valued
float const prints as a bare integer that re-defaults to i32 (so even
an f64 source drifts) — both silently changing which inputs trap. Pin
it with a cast, as [Reinterpret] does; [simplify] drops the pin again
when the operand already settles on [fty] (a plain f64 literal). *)letfty:Ast.valtype=matchfwith`F32->F32|`F64->F64incast_to(Signedtype{typ=sz;signage;strict=true})(pinfty)|TruncSat(f,signage)->letfty:Ast.valtype=matchfwith`F32->F32|`F64->F64incast_to(Signedtype{typ=sz;signage;strict=false})(pinfty)|Reinterpret->(* [to_bits]/[from_bits] are the one method pair whose result width is
NOT their receiver's (they cross the int/float divide), so a pin on
the result cannot reach the receiver and the reconciliation has no way
to place this cast. Without it the receiver re-defaults and the method
picks the wrong result type, which does not even type-check. *)method_call(lete=e(floattypesz)inife'=Nonetheneelsecast_to(Valtype(floattypesz))e)"to_bits"|ExtendS`_32->(* i64.extend32_s, rendered [((operand as i64) as i32) as i64_s] so
[to_wasm] re-fuses it: the [as i32] wraps an i64 to i32 and the outer
[as i64_s] sign-extends, and the fusion keys on the inner operand
being typed i64. Pin the i64 source in every case — a bare [i64.const]
source would re-default to i32 (collapsing the wrap and re-emitting the
value-equal but distinct [extend_i32_s]), and a dead-code hole is
polymorphic so [(_ as i64)] pins it i64 and the pair re-fuses to
[extend32_s] rather than dropping the wrap to [extend_i32_s]. A
non-constant i64 operand is already i64-typed, so [pin] is a no-op and
[simplify] leaves the wrap. *)cast_to(Signedtype{typ=sz;signage=Signed;strict=false})(cast_to(Valtype(inttype`I32))(pin(inttype`I64)))|ExtendS`_8->method_call(e(inttypesz))"extend8_s"|ExtendS`_16->method_call(e(inttypesz))"extend16_s")(* Pop an operand for a method-form intrinsic, ascribing it the operator's
scalar type [ty]. A non-inlinable operand becomes a typed hole [(_ as ty)]
rather than a bare [_], so the call type-checks in unreachable code where the
operand stack is polymorphic (mirrors the unary ops [int_un_op]/[float_un_op]).
The arithmetic/comparison operators lower to plain [BinOp]s, which accept a
polymorphic operand for *type-checking*, but for *width fidelity* they pin one
anchor-free hole with the opcode type too (see [int_bin_op]'s [symbol]). *)letpop_typedty=let*o=Stack.try_popinreturn(matchowithSomee->e|None->typed_holety)(* Give a conversion's absent operand — a hole on the polymorphic stack in dead
code — the opcode's source type, [(_ as src)], so the conversion survives the
round trip. A width-narrowing/widening conversion ([wrap]/[extend]/[demote]/
[promote]) whose source width the surface [as] cannot recover from a bare [_]
would otherwise drop entirely ([unreachable; i32.wrap_i64; drop] losing the
wrap): pinning the source makes [(_ as i64) as i32] re-emit the [wrap]. The
same holds for the reference conversions [ref.i31] ([(_ as i32) as &i31]) and
[i31.get_s/u] ([(_ as &?i31) as i32_s]), whose surface [as] erases the source
hierarchy — a bare [_ as &i31] / [_ as i32_s] re-types the hole directly to the
target and drops the op. A [select] operand is grounded for the same reason,
as [convert_src] does below: an untyped [select] of holes re-parses
type-adaptively (a numeric select re-defaults to i32, a reference select loses
its hierarchy), so under the outer [as] a bare [(_?_:_) as i32_s] takes the
target type directly and drops the op ([select; i31.get_s] losing the
[i31.get_s]); pinning the source ([((_?_:_) as &?i31) as i32_s]) keeps it. (The
cross-hierarchy [extern.convert_any] / [any.convert_extern] need the still
wider [convert_src] below, which also grounds a forwarding [br_on_null].) A
present, concrete operand is returned unchanged, so reachable code is untouched
— the redundant pin on a grounded select is pruned by the same reparse-adaptive
mirror in the typer that keeps the load-bearing one. Mirrors the dead-code
numeric-operand pins in [int_bin_op]/[pop_typed]. *)letrectype_hole_srcsrce=matche.Ast.descwith|Ast.Hole|Ast.Select_->cast_to(Valtypesrc)e(* A [ref.as_non_null] FORWARDS the reference, so the source pin belongs on
the reference inside it. Wrapping the [!] instead leaves its own (bottom)
result untyped, and the outer surface [as] then has to cast it: [_! as
i32_s] re-lowers as [ref.cast (ref i31) ; i31.get_s] where the source had
one opcode (the backing-scan [Rnn] cells). *)|Ast.NonNullinner->{ewithAst.desc=Ast.NonNull(type_hole_srcsrcinner)}|_->e(* As [type_hole_src] for the cross-hierarchy converts ([extern.convert_any] /
[any.convert_extern]), but also grounds an operand whose printed form re-parses
type-ADAPTIVELY and would otherwise take the target hierarchy under the outer
[as], collapsing the convert into a plain [ref.null]: a hole, and a [select]
whose arms are adaptive (its result type is its arms'). A bare [null] arrives
already cast ([ref.null any] -> [null as &?any]) so is left alone; a concrete
reference fixes the convert on its own and is left alone. The typer keeps the
pin only when load-bearing (its [restore_inner] mirrors [reparse_adaptive]) and
prunes it for a concrete operand, so reachable non-adaptive code is untouched. *)(* A bare HOLE is pinned NON-NULL ([nullable = false], the default): it stands for
a value off the polymorphic bottom, which is non-null (the bottom reference is a
subtype of every non-nullable type), and the converts propagate that — pinned
nullable, the convert yields [&?extern] where the original yielded [&extern] and
a consumer typed non-null (a [(ref extern)] local) rejects the decompiled Wax
outright, breaking the round trip. Non-null satisfies a nullable consumer too,
by subtyping.
Everywhere else the source's own nullability is kept: a [select]'s arms may be
concrete nullable values (or [null] literals), and a [br_on_null]'s tested ref
and a [ref.as_non_null]'s operand are nullable by construction — narrowing any
of those would be a real cast, not a pin. *)(* A bare hole — the shape [convert_src] pins non-null (see there). *)letis_bare_hole(e:_Ast.instr)=matche.Ast.descwithAst.Hole->true|_->false(* The hierarchy a heaptype's own name settles a value in; [None] where the name
alone does not say (a [Type]/[Exact] reference could name a func, a
struct/array, or a continuation type). *)lethierarchy_top(t:Ast.heaptype)=matchtwith|Any|Eq|I31|Struct|Array|None_->Some`Any|Extern|NoExtern->Some`Extern|Func|NoFunc->Some`Func|Exn|NoExn->Some`Exn|Cont|NoCont->Some`Cont|Type_|Exact_->None(* As [hierarchy_top], resolving a named type through the module's definitions
(a struct/array type is in the [any] hierarchy, a func type in [func], a
continuation type in [cont]); [None] when the name is unknown here (an
implicit type interned for an inline signature). *)letheaptype_hierarchyctx(t:Ast.heaptype)=matchhierarchy_toptwith|Someh->Someh|None->(matchtwith|Typen|Exactn->(matchsrc_typedefctxnwith|Some{Src.typ=Struct_|Array_;_}->Some`Any|Some{Src.typ=Func_;_}->Some`Func|Some{Src.typ=Cont_;_}->Some`Cont|None->None)|_->None)(* The [backing_class] a value of heap type [t] presents: its hierarchy, and
whether it is provably an [eq]-subtype ([Any] itself and the unresolvable
named types are not). *)letheaptype_classctx(t:Ast.heaptype)=matchheaptype_hierarchyctxtwith|None->Unknown_class|Somehier->Ref_class{hier;eq=hier=`Any&&t<>Any}letvaltype_classctx(t:Ast.valtype)=matchtwithAst.Ref{typ;_}->heaptype_classctxtyp|_->Value_class(* The per-result classes of a multi-value signature, in result order. *)letresult_classesctx(results:Src.valtypearray)=Array.map(funt->valtype_classctx(valtypectxt))results(* The class of the result [from_top] positions below a residual's topmost
value — what the residual hands a reconnecting hole once the [from_top]
claims interposed holes are owed have eaten its top. *)letindexed_class(classes:backing_classarray)~from_top=leti=Array.lengthclasses-1-from_topinifi<0thenUnknown_classelseclasses.(i)(* Classify the residual [b] that [Stack.effective_backing] says a bare hole
reconnects to: what [b]'s own printed form re-types it as, when the node
(with the context's tables) can say. A [Get] is a local or global — whose
declared type is its re-parse type — or, when neither table knows the name,
a function reference. A multi-value call residual does not name its results
on the node: a direct call is looked up by its Wax name
([ctx.multi_ref_results], filled at emission); a [call_ref]'s callee cast
names the function type, resolved through the module; a [call_indirect]
through an inline signature carries the (already converted) type itself.
Reconnection is POSITIONAL — the hole takes the residual's topmost value not
yet eaten by interposed claims — so a multi-value residual is indexed by
[from_top] (the scan's leftover claims); a single-value node with
[from_top > 0] cannot occur (the scan absorbs a fully-claimed entry). *)letrecbacking_class_ofctx~from_top(b:_Ast.instr)=matchb.Ast.descwith|Ast.Call({Ast.desc=Ast.Getf;_},_)->(matchHashtbl.find_optctx.multi_ref_resultsf.Ast.descwith|Someclasses->indexed_classclasses~from_top|None->Unknown_class)|Ast.Call({Ast.desc=Ast.Cast(_,(Valtype(Ref{typ=Typetn|Exacttn;_})|Ascribed(Ref{typ=Typetn|Exacttn;_})));_;},_)->(matchsrc_typedefctxtnwith|Some{Src.typ=Func{results;_};_}->indexed_class(result_classesctxresults)~from_top|_->Unknown_class)|Ast.Call({Ast.desc=Ast.Cast(_,Functype{sign;_});_},_)->indexed_class(Array.map(valtype_classctx)sign.Ast.results)~from_top|_whenfrom_top>0->Unknown_class|Ast.Null->Null_class|Ast.NonNulle->backing_class_ofctx~from_tope|Ast.Cast(_,(Valtype(Ref{typ;_})|Ascribed(Ref{typ;_})))->heaptype_classctxtyp|Ast.Cast(_,Functype_)->Ref_class{hier=`Func;eq=false}|Ast.Struct_|Ast.StructDefault_|Ast.StructDesc_|Ast.StructDefaultDesc_|Ast.Array_|Ast.ArrayFixed_|Ast.ArraySegment_|Ast.String_->Ref_class{hier=`Any;eq=true}|Ast.ContNew_->Ref_class{hier=`Cont;eq=false}|Ast.Getn->(matchHashtbl.find_optctx.local_valtypesn.Ast.descwith|Somet->valtype_classctxt|None->(matchHashtbl.find_optctx.global_valtypesn.Ast.descwith|Somet->valtype_classctxt|None->Ref_class{hier=`Func;eq=false}))|_->Unknown_class(* Whether [b] is settled by its own printed form in the hierarchy [src] (a
convert's source), or is a null, which every hierarchy accepts. Only then
may a cross-hierarchy convert leave its absent operand BARE: the hole
reconnects to [b] and the convert's own [as] surface lowers over the real
value, one opcode, exactly the source. Pinned instead, the pin lands on the
reconnected value and materialises as a [ref.cast] the source never had (the
backing-scan grid's founding convert cluster). *)letbacking_in_hierarchyctxsrc~from_top(b:_Ast.instr)=matchbacking_class_ofctx~from_topbwith|Null_class->true|Ref_class{hier;_}->hier=src|Value_class|Unknown_class->false(* The opposite polarity: [b] provably re-types OUTSIDE the hierarchy [src] (a
wrong-hierarchy reference, or a non-reference value). A top-of-hierarchy pin
over such a backing would capture it and materialise as the very
hierarchy-crossing it should not add, so the caller pins the source
hierarchy's BOTTOM instead — the claim-free ascription (see
[is_bottom_heaptype]) that grounds the hole without touching [b]. Only an
[(@if)] can make this reachable: in plain Wasm the validator types the
residual into the consumer and rejects, while an annotation's branches
consume it only per configuration. An UNCLASSIFIABLE backing stays on the
top-of-hierarchy pin: in valid annotation-free input whatever the pin
captures is right-hierarchy (the validator typed it into the consumer), so
the pin is inert after unification — while a claim-free pin would strand
the residual the source consumed. *)(* Whether [b]'s PRINTED form re-parses as a bare, ADAPTIVE null. Exactly one
null type is shed on the way out: [&?any], the type a bare [null]
re-parses to, so the typer prunes that annotation from a standalone
leftover statement as redundant (measured — [&?none], [&?extern], [&?eq],
[&?i31] and [&?func] all survive, since each states something the default
does not). A convert that leaves its hole bare over such a backing loses
its opcode: the hole reconnects to the null and the convert's own [as]
surface types that adaptive null instead of converting it
([null as &?extern] is [ref.null extern]). The hole keeps its SOURCE pin
instead, so the null types at the source hierarchy first and the outer cast
is a genuine crossing — [(null as &?any) as &?extern], the shape [Typing]'s
[restore_inner] exists to preserve. That rule cannot help here: it fires on
a null that is the cast's OWN operand, while this pair is joined only by
the re-parse (a wasm-smith FAITHDRIFT finding: [ref.null any ; block end ;
extern.convert_any], the void block blocking the convert's pop).
The pin over such a backing must be the ASCRIPTION [(_ : &?any)], not the
source CAST: a cast is an instruction, and with [simplify] off under
[--faithful] it survives as a [ref.cast] over the value the hole
reconnects to (measured). The ascription lowers to nothing while still
stating the source type, so the convert alone remains.
Every OTHER null backing keeps its printed type and so is concrete: pinning
over one would land on an already-typed value and, with [simplify] off
under [--faithful], materialise the [ref.cast] the founding convert cluster
exists to avoid ([null as &?none as &?extern], and the plain
[null as &?extern] of the grid's [Rnull.S2c] cell). *)letbacking_adaptive_null(b:_Ast.instr)=matchb.Ast.descwith|Ast.Null->true|Ast.Cast({desc=Ast.Null;_},Ast.Valtype(Ast.Ref{typ=Any;nullable=_}))->true|_->false(* Whether [b] is the backing an EXTERN source pin must ground rather than
capture: the scan named it, it is the hole's own single-value capture, and its
printed form says nothing about its hierarchy — so it defaults to the any
hierarchy on a re-parse and a pin over it would cross (see
[pin_backing_source]). A classifiable backing is already handled: in the
source hierarchy it leaves the hole bare, outside it takes the claim-free
bottom. *)letbacking_needs_groundingctx~from_top(b:_Ast.instr)=from_top=0&&matchbacking_class_ofctx~from_topbwith|Unknown_class->true|Ref_class_|Value_class|Null_class->falseletbacking_wrong_hierarchyctxsrc~from_top(b:_Ast.instr)=matchbacking_class_ofctx~from_topbwith|Ref_class{hier;_}->hier<>src|Value_class->true|Null_class|Unknown_class->false(* As [backing_wrong_hierarchy] for [ref.eq]: [b] provably re-types as
something other than an [eq]-subtype, so a bare [_ == _] hole capturing it
would not type-check (and an [(_ as &?eq)] pin capturing it would be a
hierarchy crossing). *)letbacking_not_eqctx~from_top(b:_Ast.instr)=matchbacking_class_ofctx~from_topbwith|Ref_class{eq;_}->noteq|Value_class->true|Null_class|Unknown_class->false(* As [backing_wrong_hierarchy] for [ref.is_null], which accepts every
reference: only a provable NON-reference re-typing is wrong (the bare [!_]
over it would re-default to [i32.eqz], and an [(_ as &?any)] pin over it
would not type-check). *)letbacking_not_refctx~from_top(b:_Ast.instr)=matchbacking_class_ofctx~from_topbwith|Value_class->true|Ref_class_|Null_class|Unknown_class->falseletrecconvert_src?(nullable=true)srce=matche.Ast.descwith|Ast.Hole->cast_to(Valtype(matchsrcwith|Ast.Refrwhennotnullable->Ast.Ref{rwithnullable=false}|t->t))e|Ast.Select_->cast_to(Valtypesrc)e(* [br_on_null] forwards its operand's value on the fall-through (its non-null
version), so the source cast must pin that operand INSIDE the branch, not wrap
the branch result: wrapping would cast the branch's already-[any]-defaulted
result and insert a spurious [extern.convert_any]. Recurse to the innermost
hole. ([br_on_non_null] does NOT forward — its fall-through consumes the
operand and yields nothing — so it never appears as a convert's value operand
and needs no case here.) *)|Ast.Br_on_null(l,inner)->{ewithAst.desc=Ast.Br_on_null(l,convert_srcsrcinner)}(* A [br_on_null] whose label carries values delivers them THEN the tested ref;
the operand is a [Sequence] of [branch-values…; tested-ref], and its
fall-through non-null ref — the value a following convert consumes — takes
the LAST element's (the tested ref's) hierarchy, so pin that last element.
([ref.as_non_null] on the tested ref shows as a [NonNull] wrapper; recurse
through it to the hole so the pin lands on the reference itself.) *)|Ast.Sequence(_::_asl)->letrev=List.revlinletlast=convert_srcsrc(List.hdrev)in{ewithAst.desc=Ast.Sequence(List.rev(last::List.tlrev))}|Ast.NonNullinner->{ewithAst.desc=Ast.NonNull(convert_srcsrcinner)}|_->e(* Ground the tested-ref of a forwarding [br_on_null] residual sitting on top of
the stack, for a following cross-hierarchy convert. A [br_on_null] into a block
with a ref result pushes an arity >= 2 residual (the delivered branch values
plus the fall-through non-null ref); that residual cannot be split, so the
convert's own pop reads a fresh hole which, on re-parse, reconnects to the
fall-through ref — typed by the block's declared ref result (e.g. [(ref null
any)]). The convert's source pin on that hole would then cross hierarchies and
materialise a spurious extra opcode (an [extern.convert_any] ahead of the
[any.convert_extern]). Pinning the residual's tested-ref operand to the convert
SOURCE instead grounds the fall-through ref at the source hierarchy, so the
hole reconnects there and the convert lowers to exactly one opcode. A no-op
unless the top is such a residual; the arity-1 (no-result-block) case is a
directly-popped [br_on_null] operand already handled by [convert_src].
Called AFTER the convert's own pop (which leaves an unsplittable residual in
place), so an interposed hole-valued consumer — a [ref.as_non_null] whose own
pop read a fresh hole off the residual, leaving [NonNull (hole)] on top — is
popped out of the way first and its inner hole reconnects to the pinned
tested ref just the same; [hole_reconnects] is the gate that says the popped
operand IS such a reconnecting tree (a real value in between means no
stranded hole, so nothing to ground). *)letpin_forwarding_sourcesrcstack=matchstackwith|(a,w,({Ast.desc=Ast.Br_on_null(l,inner);_}asnode))::remwhena>=2->((a,w,{nodewithAst.desc=Ast.Br_on_null(l,convert_srcsrcinner)})::rem,())|_->(stack,())(* Ground the residual [b] that [Stack.effective_backing] named as a bare
hole's backing, for a consumer whose source hierarchy is EXTERN. It is the
deeper twin of [pin_forwarding_source], which only reaches a residual still
on top of the stack; the scan sees through interposed zero-value statements,
so the residual to ground is generally not the head, and it is found here by
physical identity with the node the scan returned.
Why the extern hierarchy alone needs it. Every other top-of-hierarchy pin is
same-hierarchy and so inert after unification, which is why an UNCLASSIFIABLE
backing is otherwise left to the pin on the hole ("whatever it captures is
right-hierarchy — the validator typed it there"). An extern source pin
CROSSES: land it on a residual whose own printed form defaults to the any
hierarchy and it becomes [extern.convert_any], the opcode the pin exists to
prevent. The residual must take the source hierarchy instead, so the hole
reconnects there and the consumer lowers to its one opcode — [(_ as &?extern)!]
rather than [_!] plus a pinned hole (a wasm-smith FAITHDRIFT on
[br 'l ; ref.as_non_null ; nop ; ref.cast (ref extern)], and its
[any.convert_extern] mirror). Only a SINGLE-value capture is grounded
([from_top = 0]): a claimed-past multi-value residual hands the hole a middle
result, which this node-level pin cannot address. *)letpin_backing_sourcesrcbstack=letrecgo=function|[]->[]|((a,w,i)asentry)::rem->ifi==bthen(a,w,convert_srcsrci)::remelseentry::goremin(gostack,())(* Whether a popped operand's value slot is an unclaimed hole — the shapes
[convert_src] recurses through — so its re-parse claims the next pending
value and [pin_forwarding_source]'s grounding matters. *)letrechole_reconnects(e:_Ast.instr)=matche.Ast.descwith|Ast.Hole->true|Ast.NonNullinner->hole_reconnectsinner(* The two forwarding shapes [convert_src] also recurses through: the value the
consumer takes is the tested ref's, so an unclaimed hole there reconnects
and must be grounded exactly as a directly-popped one. Left out, a pin
[pin_hierarchy] placed INSIDE the [br_on_null] landed on the residual the
inner hole reconnects to and manufactured the crossing
([br 'l ; ref.as_non_null ; nop ; br_on_null 'l ; ref.cast (ref extern)]). *)|Ast.Br_on_null(_,inner)->hole_reconnectsinner|Ast.Sequence(_::_asl)->hole_reconnects(List.hd(List.revl))|_->falseletrecpin_hierarchypin(e:_Ast.instr)=matche.Ast.descwith(* A hole, or an untyped [select] of holes: both re-parse type-adaptively (the
select's result type is its arms'), so both take the target hierarchy under the
outer cast and absorb it. Same shapes [type_hole_src]/[convert_src] pin. *)|Ast.Hole|Ast.Select_->Some(cast_topine)|Ast.NonNullinner->Option.map(funinner->{ewithAst.desc=Ast.NonNullinner})(pin_hierarchypininner)(* A forwarding [br_on_null], and a [br_on_null] whose label carries values
(the [Sequence] of delivered values then the tested ref): the value the
outer cast consumes is the tested ref's non-null version, so the pin must
land on that ref INSIDE the branch. Wrapping the branch instead pins its
already-[any]-defaulted result and materialises the spurious
[extern.convert_any] the pin exists to prevent — the same reason
[convert_src] recurses through these two shapes, which this function's
header claims to match (a wasm-smith FAITHDRIFT on
[br 'l ; br_on_null 'l ; ref.cast (ref extern)]). *)|Ast.Br_on_null(l,inner)->Option.map(funinner->{ewithAst.desc=Ast.Br_on_null(l,inner)})(pin_hierarchypininner)|Ast.Sequence(_::_asl)->letrev=List.revlinOption.map(funlast->{ewithAst.desc=Ast.Sequence(List.rev(last::List.tlrev))})(pin_hierarchypin(List.hdrev))|_->None(* [pop_typed] carrying the receiver's width tag, for a method-form op that
inherits its receiver's flexibility (a rotate, a float method). A hole is
grounded ([None]). *)letpop_typed_taggedty=let*o=Stack.try_pop_taggedinreturn(matchowithSome(e,w)->(e,w)|None->(typed_holety,None))(* Whether a popped operand ANCHORS its own type — its printed form fixes it, so a
consumer need not ascribe one: it is present ([Some]), carries no flexible width
tag ([None]), and is not an adaptive tree. An untyped [select] of holes is pushed
with a [None] tag (its arms carry no width) but re-parses like a bare hole, so it
anchors nothing. Only the TYPED [select] consumer still asks: its arms must carry
the select's declared type, which for a REFERENCE type the width reconciliation
cannot supply (it covers the numeric scalars only), and pinning an arm the typer
already placed in another hierarchy would be a static error. The numeric
operators no longer ask — each operand carries its own recorded width and the
typer places whatever pin is needed. *)letis_anchor=function|Some(e,None)->not(reparse_adaptivee)|_->falseletfloat_un_opi0sz(op:Src.float_un_op)=(* A Wax instruction at the source instruction's span. Built fresh rather than
with [{ i0 with desc }]: a Wasm and a Wax instruction differ in the type of
their call-target hints, so one cannot be reinterpreted as the other. *)letwith_loc(i:_Ast.instr_desc):_Ast.instr={desc=i;info=i0.Src.info;hints=Wax_wasm.Hints.none;expected=Unset;}in(* A no-argument instruction method [recv.meth()]. *)letmethod_callrecvmeth=with_loc(Call(with_loc(StructGet(recv,Ast.no_locmeth)),[]))inlet*recv=Stack.try_pop_taggedinlete'=Option.mapfstrecvinletrecv_w=matchrecvwithSome(_,w)->w|None->Noneinletety=matche'withSomee->e|None->typed_holetyin(* As [int_un_op]'s [pin], for a CONVERT's integer source: its surface
([8 as f32_s]) carries the result width, not the source's. The reconciliation
would place the same cast for a valid module; it is kept so that an ill-typed
source module ([f32.convert_i64_s] of an [i32.const]) still decompiles to Wax
the typer rejects, rather than to a different well-typed conversion. *)(* [i32] is normally the re-parse default, so pinning a convert's i32 source
would be noise — EXCEPT over an operand that re-parses ADAPTIVELY (a hole, or
an untyped [select] of them). Such an operand does not default: under the
convert's own [as f32_u] it takes the TARGET type instead, and the conversion
collapses to nothing — [f32.convert_i32_u] of a dead-code select vanished
across the round trip (a wasm-smith width/faithful finding). Pin it there, so
the source width is stated and the convert survives. *)letpin_srctyx=match(e',ty)with|Some_,(Ast.I64|F32|F64)->cast_to(Valtypety)x|Somee,Ast.I32whenreparse_adaptivee->cast_to(Valtypety)x|_->xin(* [neg]/[abs]/…/[sqrt] have result width = operand width, so they carry the
operand's flexibility (like [clz]); [convert]/[reinterpret] fix a concrete
result width via a cast, so they are grounded ([None]). *)letresult_w=matchopwith|Neg|Abs|Ceil|Floor|Trunc|Nearest|Sqrt->recv_w|Convert_|Reinterpret->Nonein(* Every float unary op's result has the float width the opcode names — the
conversions ([convert]/[reinterpret]) too, whose operand is an integer. That
record is also what grounds an ADAPTIVE receiver (an untyped dead-code
[select] of holes, whose printed form would re-default to i32 under [-] or to
f64 under [.floor()]): the ops whose result width IS their receiver's carry
the pin back to it through the call, so nothing has to be inserted on the
receiver itself. *)Stack.push_numresult_w@@expect(floattypesz)(matchopwith|Neg->with_loc(UnOp(op_loci0.infoAst.Neg,e(floattypesz)))|Abs->method_call(e(floattypesz))"abs"|Ceil->method_call(e(floattypesz))"ceil"|Floor->method_call(e(floattypesz))"floor"|Trunc->method_call(e(floattypesz))"trunc"|Nearest->method_call(e(floattypesz))"nearest"|Sqrt->method_call(e(floattypesz))"sqrt"|Convert(sz',signage)->letity=inttype(sz':>[`I32|`I64|`F32|`F64])incast_to(Signedtype{typ=sz;signage;strict=false})(pin_srcity(eity))|Reinterpret->(* As [int_un_op]'s [Reinterpret]: the bits methods cross the int/float
divide, so no pin on the result can reach the receiver. *)method_call(lete=e(inttypesz)inife'=Nonetheneelsecast_to(Valtype(inttypesz))e)"from_bits")letblocktypectx(typ:Src.blocktypeoption)=matchtypwith|None->{Ast.params=[||];results=[||]}|Some(Valtypety)->{Ast.params=[||];results=[|valtypectxty|]}|Some(Typeuse(ty_idx,sign))->let{Src.params;results}=match(ty_idx,sign)with|_,Somesign->sign|Someidx,_->((* A numeric [(type N)] may name an implicit type synthesised from an
inline signature, which lives in [ctx.implicit_types], not the
declared [types] sequence — check it first, as [type_arity] does,
before [lookup_type]. *)matchimplicit_functypectxidxwith|Somesign->sign|None->(letty=lookup_typectxTypeidxinmatchty.typwith|Struct_|Array_|Cont_->assertfalse|Funcsign->sign))|None,None->assertfalsein{Ast.params=Array.map(funp->(* A *Wasm* parameter entry, so not [Ast.param_type] (which reads a
Wax one). *)annotatedp.Wax_utils.Ast.infoNone(valtypectx(sndp.Wax_utils.Ast.desc)))params;results=Array.map(funt->valtypectxt)results;}letlabel_name(label:Src.nameoption)=Option.map(fun(l:Src.name)->l.Wax_utils.Ast.desc)labelletlabel_targeted?self(instrs:_Src.instrlist)=(* [self] is the block's own source label name, if any. A symbolic [br $self]
targets this block regardless of nesting depth (unlike a numeric [br N],
whose depth is tracked), so match an [Id] reference against it. This keeps
a block reachable only by a name that [sanitize_identifier] later rejects
(so it renders under the fallback "l") counted as targeted, which reserves
that fallback name and prevents an inner block from colliding with it. *)lethitdepth(idx:Src.idx)=matchidx.descwith|Numn->Uint32.to_intn=depth|Idname->self=Somenamein(* Explicit recursion rather than [List.exists (one depth)]: [any] is called
once per (nested) block, so a partial-application closure here allocated on
every block — the hottest allocation in [modulefield]. *)letrecanydepth=function|[]->false|i::rest->onedepthi||anydepthrestandonedepth(i:_Src.instr)=matchi.descwith|Bri|Br_ifi|Br_on_nulli|Br_on_non_nulli|Br_on_cast(i,_,_)|Br_on_cast_fail(i,_,_)|Br_on_cast_desc_eq(i,_,_)|Br_on_cast_desc_eq_fail(i,_,_)->hitdepthi|Br_table(labels,lab)->List.exists(hitdepth)(lab::labels)|Block{block;_}|Loop{block;_}->any(depth+1)block.desc|If{if_block;else_block;_}->any(depth+1)if_block.desc||any(depth+1)else_block.desc|TryTable{block;catches;_}->any(depth+1)block.desc||List.exists(fun(c:Src.catch)->matchcwith|Catch(_,l)|CatchRef(_,l)|CatchAlll|CatchAllRefl->hitdepthl)catches|Try{block;catches;catch_all;_}->(any(depth+1)block.desc||List.exists(fun(_,b)->any(depth+1)b.Wax_utils.Ast.desc)catches||matchcatch_allwith|Someb->any(depth+1)b.Ast.desc|None->false)|Resume(_,handlers)|ResumeThrowRef(_,handlers)|ResumeThrow(_,_,handlers)->List.exists(fun(c:Src.on_clause)->matchcwithOnLabel(_,l)->hitdepthl|OnSwitch_->false)handlers(* Folded WAT form: the operands [l] and the head [i] run at this same
depth (the wrapper opens no block scope), mirroring how [instruction]
flattens it. *)|Folded(i,l)->onedepthi||anydepthl|_->falseinany0instrsletpush_labelctx~loop~targetedlabeltyp=letarity=blocktype_arityctxtypinleti=ifloopthenfstarityelsesndarityinletlabel_arities=(Option.map(fun(l:Src.name)->l.Wax_utils.Ast.desc)label,i)::ctx.label_aritiesinletlabel,labels=LabelStack.push~diagnostics:ctx.diagnostics~targetedctx.labelslabelin(label,{ctxwithlabels;label_arities;block_params=blocktype_paramsctxtyp})(*
let bottom_heap_type ctx (t : Src.heaptype) : Ast.heaptype =
match t with
| Any | Eq | I31 | Struct | Array | None_ -> None_
| Func | NoFunc -> NoFunc
| Exn | NoExn -> NoExn
| Extern | NoExtern -> NoExtern
| Type ty -> (
match (lookup_type ctx Type ty).typ with
| Struct _ | Array _ -> None_
| Func _ -> NoFunc)
*)(* A labelled immediate argument [name: v] of a memory access. Both the label
node and its payload are [Contextual]: an immediate's type is fixed by its
position in the call, not by its printed form. *)letlabelledwith_locnamev=contextual(with_loc(Ast.Labelled(Ast.no_locname,contextualv)))(* Trailing labelled [offset]/[align] arguments of a memory access: [offset]
only when non-zero, [align] only when it differs from the natural
alignment. *)letmem_extrawith_loc(memarg:Src.memarg)nat=letlitv=with_loc(Ast.Int(Wax_utils.Uint64.to_stringv))inletnat=Wax_utils.Uint64.of_intnatin(ifWax_utils.Uint64.comparememarg.offsetWax_utils.Uint64.zero<>0then[labelledwith_loc"offset"(litmemarg.offset)]else[])@ifWax_utils.Uint64.comparememarg.alignnat<>0then[labelledwith_loc"align"(litmemarg.align)]else[](* The callee of an indirect call: [tab[index]] narrowed to the call's function
type, i.e. [tab[index] as &$ft] (named type) or [tab[index] as &fn(..)] (an
inline type, with no named type to reference). The cast is always emitted;
[to_wasm] re-fuses the whole pattern back to [call_indirect]. *)letindirect_calleectxwith_loctab((tyidx,sign):Src.typeuse)index=lettabget=with_loc(Ast.ArrayGet(with_loc(Ast.Get(idxctx`Tabletab)),index))inletinline_functype(s:Src.functype):Ast.casttype=letsign:Ast.functype={params=functype_paramsctxs.params;results=Array.map(funt->valtypectxt)s.results;}inAst.Functype{nullable=true;sign}inletcast_type:Ast.casttypeoption=matchOption.bindtyidx(implicit_functypectx)with|Someft->(* Anonymous implicit type: no named type to reference, render inline. *)Some(inline_functypeft)|None->(matchtyidxwith|Someti->Some(Ast.Valtype(Ast.Ref{nullable=true;typ=Ast.Type(idxctx`Typeti)}))|None->Option.mapinline_functypesign)inmatchcast_typewith|Somect->with_loc(Ast.Cast(tabget,ct))|None->tabget(* A bottom descriptor operand carries no descriptor type of its own — a hole
(dead code, popped from an empty stack) or a [ref.null none]-style null (a cast
to a bottom heap type) — so the typer cannot recover the target from it. Pin it
to the descriptor type of the target [x] ([exact] matching the target's
exactness); a concrete operand keeps its own type. An existing bottom cast's
target is rewritten in place, so [simplify] cannot fold the pin back to bottom. *)letpin_descriptorctx~exactxd=match(lookup_typectxTypex).descriptorwith|None->d|Somey->(lety=idxctx`Typeyinletpin=Ast.Valtype(Ast.Ref{nullable=true;typ=(ifexactthenAst.ExactyelseAst.Typey);})inletis_bottom(t:Ast.heaptype)=matchtwith|None_|NoFunc|NoExtern|NoExn|NoCont->true|_->falseinmatchd.Ast.descwith|Ast.Hole|Ast.Null->cast_topind(* Both bottom spellings: a null literal's own [Valtype] cast (the
[RefNull] emission, [null as &?none]) and the claim-free [Ascribed]
hole pin. *)|Ast.Cast(inner,(Ast.Valtype(Ast.Ref{typ;_})|Ast.Ascribed(Ast.Ref{typ;_})))whenis_bottomtyp->(* Rebuilt on [d] to keep the outer node's span; its expectation is the
new cast's target, not the replaced cast's operand. *){dwithAst.desc=Ast.Cast(inner,pin);expected=cast_resultpin;}|_->d)(* As [pin_descriptor], taking the target as the [reftype] the branch/cast
immediate carries (an abstract target has no descriptor — leave the hole). *)letpin_descriptor_reftypectx(t:Src.reftype)d=matcht.typwith|Typex->pin_descriptorctx~exact:falsexd|Exactx->pin_descriptorctx~exact:truexd|_->d(*** The instruction converter ***)(* Only value-producing arithmetic and bitwise operators have a compound-
assignment form; comparisons do not. *)lethas_compound_form:Ast.binop->bool=function|Add|Sub|Mul|Div_|Rem_|And|Or|Xor|Shl|Shr_->true|Eq|Ne|Lt_|Gt_|Le_|Ge_->false(* Build the assignment [target = e], collapsing [x = x op e] back into the
compound assignment [x op= e] — the inverse of the lowering in {!To_wasm}.
The variable must be the operator's left operand. *)letset_desctargete=matche.Ast.descwith|Ast.BinOp(op,{desc=Gety;_},rhs)whenhas_compound_formop.desc&&String.equaly.desctarget.Wax_utils.Ast.desc->Ast.Set(target,Someop,rhs)|_->Ast.Set(target,None,e)(* A decompiled struct-literal field. When the value is a plain [Get] of the
like-named local/global/function, use the punning shorthand [{x}] ([None])
rather than the redundant [{x: x}]; re-parsing resolves the pun to that same
[Get], so the output round-trips. *)letstruct_fieldnm(v:_Ast.instr)=matchv.descwith|Ast.GetxwhenString.equalx.descnm->(Ast.no_locnm,None)|_->(Ast.no_locnm,Somev)(* A diverging instruction leaves the stack polymorphic: everything below it is
dead and a later pop from the empty region springs from a polymorphic bottom
(an [Unknown]-typed hole). These are exactly the instructions lowered through
[Stack.push_poly]. Used by the reference comparisons to tell such a bottom
from a real dead value producer still on the stack (see [RefEq]/[RefIsNull]). *)letis_poly_terminator(i:_Ast.instr)=matchi.Ast.descwith|Ast.Unreachable|Ast.Br_|Ast.Br_table_|Ast.TailCall_|Ast.Return_|Ast.Throw_|Ast.ThrowRef_->true|_->false(* Whether two source signatures convert to the same Wax signature — compared
on the printed converted types, locations aside (the [collapse_splices]
trick). Used by [pin_callee]: a captured function value of the SAME
signature satisfies the callee pin as written (the pin drops as redundant
and the call reads the value, exactly as the source instruction did).
Deliberately equality, not subtyping (which this module cannot decide): a
proper-subtype capture behind an annotation falls back to the claim-free
bottom pin, which is inert there. *)letsame_signaturectx(a:Src.functype)(b:Src.functype)=letprint(ft:Src.functype)=Wax_utils.Printer.run_string(funpp->Array.iter(funp->Wax_lang.Output.valtypepp(valtypectx(sndp.Wax_utils.Ast.desc));Wax_utils.Printer.stringpp"->")ft.Src.params;Array.iter(funt->Wax_lang.Output.valtypepp(valtypectxt);Wax_utils.Printer.stringpp",")ft.Src.results)inString.equal(printa)(printb)(* The [call_ref]/[return_call_ref] callee type pin [(_ as &?t)] for an ABSENT
callee (a pop off the polymorphic stack). The cast names the instruction's
type immediate, so it must survive — but its hole claims positionally, and
behind a conditional annotation (the scan's [crossed]) the captured value
may be anything the branches consume per configuration: a wrong-hierarchy or
non-reference capture poisons the cast, and a func capture of a DIFFERENT
signature materialises the pin as a [ref.cast] the source never had. Ground
such a hole with the claim-free ASCRIPTION of the type itself — [(_ : &?t)]
claims nothing, still names [t], and lowers to no instruction — and keep
the plain pin everywhere else: with no annotation in between, a captured
value is the very callee the source popped (the validator typed it there),
so the claim is load-bearing and sound. *)letpin_calleectxt(f:_Ast.instr)=lettarget:Ast.valtype=Ref{nullable=true;typ=Type(idxctx`Typet)}inletpininner={fwithAst.desc=Ast.Cast(inner,Valtypetarget)}inmatchf.Ast.descwith|Ast.Hole->let*backing,crossed_any=Stack.effective_backingis_poly_terminatorinletwrong=matchbackingwith|`Value->true|`Backing(b,from_top,crossed)->(crossed&&matchbacking_class_ofctx~from_topbwith|Null_class->false|Value_class->true|Unknown_class->false|Ref_class{hier;_}->(hier<>`Func||matchb.Ast.descwith|Ast.Getg->(match((trySome(CondTbl.findctx.function_typesctx.cond_asmg.Ast.desc)withNot_found->None),typeuse_functypectx(Somet,None))with|Somegtu,Sometft->(matchtypeuse_functypectxgtuwith|Somegft->not(same_signaturectxgfttft)|None->true)|_->true)|_->true))(* With an annotation in the stack, a claiming pin can capture what a
branch's pushes released to it per configuration (see
[effective_backing]'s [crossed_any]): go claim-free. *)|`Floor|`Blocked->crossed_anyin(* An UNCLASSIFIABLE residual, as in the convert arms: the pin would land
on it and materialise a [ref.cast] the source never had
([ref.as_non_null ; atomic.fence ; call_ref] re-lowering with a cast
between the fence and the call). The residual IS the callee the source
popped, so grounding it at the callee type is what the hole then
reconnects to, and the hole stays bare. *)let*grounded=matchbackingwith|`Backing(b,from_top,_)whenbacking_needs_groundingctx~from_topb->let*()=pin_backing_sourcetargetbinreturntrue|_->returnfalseinreturn(ifgroundedthenfelseifwrongthenascribe_totargetfelsepinf)(* A FORWARDING operand — a [ref.as_non_null] over the callee, the shapes
[pin_hierarchy] recurses through — carries the reference inside it, so the
pin belongs there. Wrapping the forwarder instead pins its own (bottom,
non-null) result and materialises a [ref.cast] the source never had:
[(_! as &?t)()] re-lowers as [ref.as_non_null ; ref.cast ; call_ref] where
the source had two opcodes (the backing-scan [Rnn.VmultiRC] cells). Inside,
the pin sits on the hole and is absorbed, exactly as [convert_src] places a
convert's source pin. *)|_->(matchpin_hierarchy(Ast.Valtypetarget)fwith|Somef'->returnf'|None->return(pinf))(* Whether the residual's own printed form names exactly the type [type_name] —
the one capture a [(_ as &?type_name)] receiver pin provably absorbs as
written (the pin drops as redundant and the access reads the value, exactly
as the source instruction did). *)letbacking_names_type~from_top(b:_Ast.instr)(type_name:Ast.ident)=from_top=0&&matchb.Ast.descwith|Ast.Cast(_,(Valtype(Ref{typ=Typen|Exactn;_})|Ascribed(Ref{typ=Typen|Exactn;_})))->String.equaln.Ast.desctype_name.Ast.desc|_->false(* The member-access receiver type pin [(recv as &?t)] (a struct/array read,
write, fill/copy/init receiver) for an ABSENT receiver, as [pin_callee] for
a callee: behind a conditional annotation ([crossed]) a positional capture
the pin cannot absorb — a value outside [t]'s hierarchy, a non-reference, or
a reference whose printed form names a DIFFERENT type — either poisons the
access (and the lowering, which reads the struct/array type off the
receiver, has nothing to emit) or materialises the pin as a [ref.cast] the
source never had. Ground such a hole with the claim-free bottom of [t]'s
hierarchy inside the type pin — [((_ as &?none) as &?t)] still names [t] —
and keep the plain pin everywhere else (with no annotation in between a
captured value is the receiver the source instruction read, validator-typed
there). [siblings] are the statement's shallower operands, whose own hole
claims sit between this receiver's hole and its capture. *)(* The bottom heap type of a hierarchy — the claim-free pin's spelling. *)lethierarchy_bottom:_->Ast.heaptype=function|Some`Func->NoFunc|Some`Extern->NoExtern|Some`Exn->NoExn|Some`Cont->NoCont|Some`Any|None->None_letpin_receiverctxtype_name~siblings(recv:_Ast.instr)=lettarget:Ast.valtype=Ref{nullable=true;typ=Typetype_name}inletpininner={recvwithAst.desc=Ast.Cast(inner,Valtypetarget)}inmatchrecv.Ast.descwith|Ast.Hole->letclaims=List.fold_left(funne->n+Stack.hole_claimse)0siblingsinlet*backing,crossed_any=Stack.effective_backing~claimsis_poly_terminatorinletwrong=matchbackingwith|`Value->true|`Backing(b,from_top,crossed)->(crossed&&(not(backing_names_type~from_topbtype_name))&&matchbacking_class_ofctx~from_topbwith|Null_class->false(* An UNCLASSIFIABLE backing cannot be shown absorbable, and under a
crossed annotation the claiming pin is a [ref.cast] EMITTED
before the access's other operands: its hole then claims the
annotation's push rather than the residual, which is the very
mis-capture the pin exists to avoid, and the typer rejects the
spelling outright ("This expression occurs before a hole '_'" —
the backing-scan [Rnn.ScondPush*] cells). The claim-free
ascription lowers to nothing, so it states the type without
taking a value and the operands keep their source order. *)|Unknown_class->true|Value_class|Ref_class_->true)(* As [pin_callee]: annotation in play, claiming pin unsafe. *)|`Floor|`Blocked->crossed_anyinreturn(ifwrongthenascribe_totargetrecvelsepinrecv)(* As [pin_callee]: a forwarding receiver carries the reference inside it, so
the pin goes there — [(_! as &?s).f] re-lowers with a [ref.cast] the source
never had. *)|_->(matchpin_hierarchy(Ast.Valtypetarget)recvwith|Somerecv'->returnrecv'|None->return(pinrecv))(* Pin the reference HIERARCHY of an operand that leaves it open: a hole is
polymorphic, and [!e] ([ref.as_non_null]) only forwards its operand's type. The
pin is pushed down to the hole ITSELF rather than wrapped around the [!] —
around it the pin would be a cross-hierarchy cast of an any-typed operand, i.e.
an [extern.convert_any], the very instruction being avoided, whereas on a bare
hole it merely types the hole and lowers to nothing. [None] when the operand
pins a hierarchy of its own (a named value, a construction, an expression
already cast) and so needs no pin.
An unannotated [select] of holes is deliberately NOT descended into, unlike in
[RefIsNull]: an unannotated select's value operands must be numeric (or, in
dead code, bottom), so one feeding a cast into the extern hierarchy always has
two bottom arms — the expected type then flows into them and the cast is
dropped as redundant rather than turning into a convert. That is the
documented best-effort cast fidelity, and pinning an arm would trade it for a
typed-[select] immediate, itself a documented residual. *)(* An absent numeric-operator operand whose positional claim would capture a
REFERENCE (or null) residual across a conditional annotation — whose branch
consumes it per configuration; the depth-4 grid's [R*.Scond*.Rnum.*] cells:
the record alone cannot help there (the mis-typed tree resolves the cell as
the reference and the width machinery skips it), so the hole gets the
SYNTACTIC pin [(_ as i64)] too. The capture still mis-types in the
discarded-diagnostics tree pass, but the printed target survives the
re-parse and the lowering's poisoned-operand fallback reads it, keeping the
opcode at its source width. [bare] everywhere else (no output churn):
without an annotation, a capturable reference here means invalid input. *)letpin_crossed_ref_holectxty~bareo=matchowith|Some(e,_)->returne|None->let*backing,crossed_any=Stack.effective_backingis_poly_terminatorinreturn(ifcrossed_any&&matchbackingwith|`Backing(b,from_top,_)->(matchbacking_class_ofctx~from_topbwith|Ref_class_|Null_class->true|Value_class|Unknown_class->false)|`Value|`Floor|`Blocked->falsethentyped_holetyelsebare())letfloat_bin_opctx(i0:_Src.instr)sz(op:Src.float_bin_op)=(* A Wax instruction at the source instruction's span. Built fresh rather than
with [{ i0 with desc }]: a Wasm and a Wax instruction differ in the type of
their call-target hints, so one cannot be reinterpreted as the other. *)letwith_loc(i:_Ast.instr_desc):_Ast.instr={desc=i;info=i0.Src.info;hints=Wax_wasm.Hints.none;expected=Unset;}in(* As for [int_bin_op]: an arithmetic operator preserves the operand width and
its result stays flexible only when both operands are; an absent operand is a
hole carrying the operator's type as its record, and nothing else is
inserted — the typer grounds whatever would resolve elsewhere. *)letbare()=expect(floattypesz)(Ast.no_loc_instrAst.Hole)inletarith=Some(sz:>[`I32|`I64|`F32|`F64])inletsymbolwidthop=let*o2=Stack.try_pop_taggedinlet*o1=Stack.try_pop_taggedinlet*e1=pin_crossed_ref_holectx(floattypesz)~bareo1inlet*e2=pin_crossed_ref_holectx(floattypesz)~bareo2inletboth_flexible=match(o1,o2)with|Some(_,Some_),Some(_,Some_)->true|_->falseinletwidth=ifboth_flexiblethenwidthelseNoneinStack.push_numwidth(expect(floattypesz)(with_loc(BinOp(op_loci0.infoop,e1,e2))))inletcompareop=let*o2=Stack.try_pop_taggedinlet*o1=Stack.try_pop_taggedinlet*e1=pin_crossed_ref_holectx(floattypesz)~bareo1inlet*e2=pin_crossed_ref_holectx(floattypesz)~bareo2in(* The i32 result carries no width TAG (it is not flexible), but the opcode
states it, so record it. *)Stack.push1(expectI32(with_loc(BinOp(op_loci0.infoop,e1,e2))))in(* [min]/[max]/[copysign]: result width = receiver width (as [rotl]). *)letmethname=let*e2=pop_typed(floattypesz)inlet*e1,w1=pop_typed_tagged(floattypesz)inStack.push_numw1(expect(floattypesz)(with_loc(Call(with_loc(StructGet(e1,Ast.no_locname)),[e2]))))inmatchopwith|Add->symbolarithAdd|Sub->symbolarithSub|Mul->symbolarithMul|Div->symbolarith(DivNone)|Min->meth"min"|Max->meth"max"|CopySign->meth"copysign"|Eq->compareEq|Ne->compareNe|Lt->compare(LtNone)|Gt->compare(GtNone)|Le->compare(LeNone)|Ge->compare(GeNone)letint_bin_opctx(i0:_Src.instr)sz(op:Src.int_bin_op)=(* A Wax instruction at the source instruction's span. Built fresh rather than
with [{ i0 with desc }]: a Wasm and a Wax instruction differ in the type of
their call-target hints, so one cannot be reinterpreted as the other. *)letwith_loc(i:_Ast.instr_desc):_Ast.instr={desc=i;info=i0.Src.info;hints=Wax_wasm.Hints.none;expected=Unset;}in(* An absent operand — a pop from the empty/absent stack, i.e. dead code — is a
hole carrying the operator's operand type as its recorded width, which is what
the typer grounds it from if the printed form would resolve elsewhere. A
PRESENT operand already carries its own record from where it was pushed, so
nothing distinguishes the two here any more (no anchor analysis, no pin
placement: the reconciliation decides all of that from the records). *)letbare()=expect(inttypesz)(Ast.no_loc_instrAst.Hole)inletarith=Some(sz:>[`I32|`I64|`F32|`F64])in(* An arithmetic operator yields the operand width, so [a + b] round-trips to
that width via the sum's own type. Its result stays a flexible literal tree
(tagged) only when BOTH operands are; if either is grounded the sum is
grounded too ([x + 1] re-parses to [x]'s width on its own) and takes no tag,
so a downstream eraser does not read it as flexible. *)letsymbolwidthop=let*o2=Stack.try_pop_taggedinlet*o1=Stack.try_pop_taggedinlet*e1=pin_crossed_ref_holectx(inttypesz)~bareo1inlet*e2=pin_crossed_ref_holectx(inttypesz)~bareo2inletboth_flexible=match(o1,o2)with|Some(_,Some_),Some(_,Some_)->true|_->falseinletwidth=ifboth_flexiblethenwidthelseNonein(* The sum's own type is the operand width whether or not the tag above keeps
it flexible. *)Stack.push_numwidth(expect(inttypesz)(with_loc(BinOp(op_loci0.infoop,e1,e2))))in(* A comparison yields i32 whatever its operands' width, so its surface *erases*
that width ([(4096 >>u 40) == 0] would re-default the shift to i32 and flip
true->false). Nothing is inserted for it: each operand carries its own
recorded width, and the typer pins whichever one would resolve elsewhere. The
i32 result carries no tag. *)letcompareop=let*o2=Stack.try_pop_taggedinlet*o1=Stack.try_pop_taggedinlet*e1=pin_crossed_ref_holectx(inttypesz)~bareo1inlet*e2=pin_crossed_ref_holectx(inttypesz)~bareo2in(* The i32 result carries no width TAG (it is not flexible), but the opcode
states it, so record it. *)Stack.push1(expectI32(with_loc(BinOp(op_loci0.infoop,e1,e2))))in(* [rotl]/[rotr]: result width = receiver width, so it carries the receiver's
flexibility (the count arg is pinned by the method once the receiver fixes
it). Like [clz], an erasing consumer then pins it back to the receiver. *)letmethname=let*e2=pop_typed(inttypesz)inlet*e1,w1=pop_typed_tagged(inttypesz)inStack.push_numw1(expect(inttypesz)(with_loc(Call(with_loc(StructGet(e1,Ast.no_locname)),[e2]))))inmatchopwith|Add->symbolarithAdd|Sub->symbolarithSub|Mul->symbolarithMul|Divs->symbolarith(Div(Somes))|Rems->symbolarith(Rems)|And->symbolarithAnd|Or->symbolarithOr|Xor->symbolarithXor|Shl->symbolarithShl|Shrs->symbolarith(Shrs)|Rotl->meth"rotl"|Rotr->meth"rotr"|Eq->compareEq|Ne->compareNe|Lts->compare(Lt(Somes))|Gts->compare(Gt(Somes))|Les->compare(Le(Somes))|Ges->compare(Ge(Somes))(* Branch-hinting / compilation-hints proposals: carry a Wasm instruction's hints
onto the Wax instruction it decompiles to. A Wasm instruction contributes one
entry to the stack of Wax expressions being built, so the hints go on whatever
[instruction_desc] left on top. *)(* Record a local's / global's NUMERIC type on a node that carries its value —
the [Get] that reads it, and the value a [set]/[tee] writes to it. A reference
local records nothing: the channel holds numeric scalars only (and [v128], as a
not-a-reference marker), which is exactly what its readers ask about. *)letexpect_localctx(name:(string,_)Ast.annotated)e=matchHashtbl.find_optctx.local_valtypesname.Ast.descwith|Somet->expectte|None->eletexpect_globalctx(name:(string,_)Ast.annotated)e=matchHashtbl.find_optctx.global_valtypesname.Ast.descwith|Somet->expectte|None->eletrecinstructionctx(i:_Src.instr):unitStack.t=let*()=instruction_descctxiinifWax_wasm.Hints.is_emptyi.hintsthenreturn()elselethints=Wax_wasm.Hints.map_targets(funf->idxctx`Funcf)i.hintsinfunstack->matchstackwith|(arity,w,top)::rem->((arity,w,{topwithAst.hints})::rem,())|[]->([],())andinstruction_descctx(i:_Src.instr):unitStack.t=letwith_loc(i':_Ast.instr_desc):_Ast.instr={Ast.desc=i';info=i.info;hints=Wax_wasm.Hints.none;expected=Unset;}in(* A block-shaped value node ([do]/[loop]/[if]/[try]): its result type is
stated by its own annotation, or — when [simplify] drops a redundant one —
re-imposed by the context that made it redundant, so the node needs no
claim of its own ([Contextual]; the values INSIDE feeding its exits are
cleared by [forget_expected] for the same reason). *)letblock_node(i':_Ast.instr_desc):_Ast.instr=contextual(with_loci')inletmem_callmmethargs=with_loc(Ast.Call(with_loc(Ast.StructGet(with_loc(Ast.Get(idxctx`Memm)),Ast.no_locmeth)),args))inlettable_calltmethargs=with_loc(Ast.Call(with_loc(Ast.StructGet(with_loc(Ast.Get(idxctx`Tablet)),Ast.no_locmeth)),args))in(* [seg.drop()] on a data or element segment. *)letdrop_callkindseg=with_loc(Ast.Call(with_loc(Ast.StructGet(with_loc(Ast.Get(idxctxkindseg)),Ast.no_loc"drop")),[]))in(* [recv.meth(args)] method call and [f(args)] free-function call, used for
SIMD intrinsics. *)letmeth_callrecvmethargs=with_loc(Ast.Call(with_loc(Ast.StructGet(recv,Ast.no_locmeth)),args))in(* [ns::name(args)] qualified-path intrinsic call (SIMD free functions, wide
arithmetic). *)letpath_callnsnameargs=with_loc(Ast.Call(with_loc(Ast.Path(Ast.no_locns,Ast.no_locname)),args))in(* Ascribe a (struct/array) method receiver its reference type, so the method
resolves even when the receiver is a hole on a polymorphic stack (unreachable
code); a redundant cast on a concrete receiver is dropped by [simplify]. *)(* As [pin_receiver] / [pin_callee]: over a FORWARDING operand the pin goes on
the reference inside it, so it is absorbed rather than casting the
forwarder's own bottom result ([(_! as &?array).length()] re-lowered with a
[ref.cast] the source never had). A concrete operand takes the plain
wrapper. *)letcast_refrecvtyp=letpin=Ast.Valtype(Ast.Ref{nullable=true;typ})inmatchpin_hierarchypinrecvwith|Somerecv'->recv'|None->{recvwithAst.desc=Ast.Cast(recv,pin)}in(* Ascribe the continuation operand — the last of [args] — with the
instruction's type immediate, [(c as &?ct)], so a resume/switch/bind
through a supertype signature keeps its exact immediate on the round
trip. The ascription lowers to no instruction; re-typing drops it again
when it merely names the operand's own type. *)letascribe_contctargs=matchList.revargswith|c::rest->List.rev(cast_refc(Typect)::rest)|[]->[]inmatchi.descwith|Block{label;typ;block}->letlabel,ctx=push_labelctx~loop:false~targeted:(label_targeted?self:(label_namelabel)block.desc)labeltypinletinputs,outputs=blocktype_arityctxtypinletblock=Stack.run~results:outputs(instructionsctxblock.desc)inlet*()=Stack.consumeinputsinStack.push(ifinputs>0then0elseoutputs)(block_node(Block{label=label();typ=blocktypectxtyp;block=Ast.no_locblock;}))|Loop{label;typ;block}->letlabel,ctx=push_labelctx~loop:true~targeted:(label_targeted?self:(label_namelabel)block.desc)labeltypinletinputs,outputs=blocktype_arityctxtypinletblock=Stack.run~results:outputs(instructionsctxblock.desc)inlet*()=Stack.consumeinputsinStack.push(ifinputs>0then0elseoutputs)(block_node(Loop{label=label();typ=blocktypectxtyp;block=Ast.no_locblock;}))|If{label;typ;if_block;else_block}->letlabel,ctx=letself=label_namelabelinpush_labelctx~loop:false~targeted:(label_targeted?selfif_block.desc||label_targeted?selfelse_block.desc)labeltypinletinputs,outputs=blocktype_arityctxtypin(* Keep the (then ...)/(else ...) clause locations on the Wax blocks so a
comment opening a clause attaches to the block rather than the
condition or the previous clause's last instruction. *)letif_block={if_blockwithAst.desc=Stack.run~results:outputs(instructionsctxif_block.desc);}inletelse_block=ifelse_block.desc=[]thenNoneelseSome{else_blockwithAst.desc=Stack.run~results:outputs(instructionsctxelse_block.desc);}inlet*cond=Stack.popinlet*()=Stack.consumeinputsinStack.push(ifinputs>0then0elseoutputs)(block_node(If{label=label();typ=blocktypectxtyp;cond;if_block;else_block;}))|TryTable{label=labl;typ;block;catches}->letlabl,block_ctx=push_labelctx~loop:false~targeted:(label_targeted?self:(label_namelabl)block.desc)labltypinletinputs,outputs=blocktype_arityctxtypinletblock=Stack.run~results:outputs(instructionsblock_ctxblock.desc)inletcatches=List.map(fun(catch:Src.catch):Ast.catch->matchcatchwith|Catch(t,l)->Catch(idxctx`Tagt,labelctxl)|CatchRef(t,l)->CatchRef(idxctx`Tagt,labelctxl)|CatchAlll->CatchAll(labelctxl)|CatchAllRefl->CatchAllRef(labelctxl))catchesinlet*()=Stack.consumeinputsinStack.push(ifinputs>0then0elseoutputs)(block_node(TryTable{label=labl();typ=blocktypectxtyp;block=Ast.no_locblock;catches;}))|Try{label;typ;block;catches;catch_all}->(* A [br] out of the try's body or any of its handler blocks targets the
one try scope, so all of them bear on whether this label renders. *)lettargeted=letself=label_namelabelinlabel_targeted?selfblock.desc||List.exists(fun(_,b)->label_targeted?selfb.Wax_utils.Ast.desc)catches||matchcatch_allwith|Someb->label_targeted?selfb.Ast.desc|None->falseinletlabel,ctx=push_labelctx~loop:false~targetedlabeltypinletinputs,outputs=blocktype_arityctxtypinletblock=Stack.run~results:outputs(instructionsctxblock.desc)inletcatches=List.map(fun(t,block)->(idxctx`Tagt,Ast.no_loc(Stack.run~results:outputs(instructionsctxblock.Wax_utils.Ast.desc))))catchesinletcatch_all=Option.map(funblock->Ast.no_loc(Stack.run~results:outputs(instructionsctxblock.Wax_utils.Ast.desc)))catch_allinlet*()=Stack.consumeinputsinStack.push(ifinputs>0then0elseoutputs)(block_node(Try{label=label();typ=blocktypectxtyp;block=Ast.no_locblock;catches;catch_all;}))|Unreachable->Stack.push_poly(with_locUnreachable)|Nop->Stack.push0(with_locNop)|Drop->(* A dropped value supplies no expected type: a width eraser (see [Stack]).
[i64.div_u (2147483648 + 2147483648)] would re-default its divisor to a
trapping [0]. The drop is an anonymous [Let] ([_ = e]); a non-default
flexible width is pinned in its type annotation ([_: i64 = e]) rather
than by an identity cast on the value, so the reader is never left to
disambiguate a genuine [as] conversion from a width pin. The keep/drop
of that annotation then reuses the ordinary [Let] machinery. *)let*e,w=Stack.pop_taggedinletannot:Ast.valtypeoption=matchwwith|Some`I64->SomeI64|Some`F32->SomeF32|Some`F64->SomeF64|Some`I32|None->NoneinStack.push0(with_loc(Let([(None,annot)],Somee)))|Bri->letinput=label_arityctxiinlet*args=Stack.grabinputinStack.push_poly(with_loc(Br(labelctxi,sequence_optargs)))|Br_ifi->letinput=label_arityctxiinlet*args=Stack.grab(input+1)inStack.pushinput(contextual(with_loc(Br_if(labelctxi,sequenceargs))))|Br_table(labels,lab)->letinput=label_arityctxlabinlet*args=Stack.grab(input+1)inStack.push_poly(with_loc(Br_table(List.map(funi->labelctxi)(labels@[lab]),sequenceargs)))|Br_on_nulli->letinput=label_arityctxiinlet*args=Stack.grab(input+1)inStack.push(input+1)(contextual(with_loc(Br_on_null(labelctxi,sequenceargs))))|Br_on_non_nulli->letinput=label_arityctxiinlet*args=Stack.grabinputinStack.push(input-1)(contextual(with_loc(Br_on_non_null(labelctxi,sequenceargs))))|Br_on_cast(i,_,t)->letinput=label_arityctxiinlet*args=Stack.grabinputinStack.pushinput(contextual(with_loc(Br_on_cast(labelctxi,reftypectxt,sequenceargs))))|Br_on_cast_fail(i,_,t)->letinput=label_arityctxiinlet*args=Stack.grabinputinStack.pushinput(contextual(with_loc(Br_on_cast_fail(labelctxi,reftypectxt,sequenceargs))))|Br_on_cast_desc_eq(i,_,t)->(* The descriptor operand is on top of the branch operands. The target type
and its exactness are recovered from the descriptor, so only the result
nullability of [t] is kept. *)letinput=label_arityctxiinlet*d=Stack.popinletd=pin_descriptor_reftypectxtdinlet*args=Stack.grabinputinStack.pushinput(contextual(with_loc(Br_on_cast_desc_eq(labelctxi,t.nullable,sequenceargs,d))))|Br_on_cast_desc_eq_fail(i,_,t)->letinput=label_arityctxiinlet*d=Stack.popinletd=pin_descriptor_reftypectxtdinlet*args=Stack.grabinputinStack.pushinput(contextual(with_loc(Br_on_cast_desc_eq_fail(labelctxi,t.nullable,sequenceargs,d))))|Folded(head,l)->(* Carry the folded expression's full span (the [(…)]'s [$sloc]) onto its
head instruction, so the resulting Wax node encloses its operands rather
than covering just the opcode keyword. Otherwise a comment trailing the
[)] attaches to the last-ending operand — which, for [select]'s
value/condition reorder, is the ternary's *first* element (the
condition), not its last, breaking the wax→wat→wax round-trip. *)let*()=instructionsctxlininstructionctx{headwithSrc.info=i.Src.info}|LocalGetx->(* Record the local's numeric type on the node (see [local_valtypes]). *)letname=idxctx`LocalxinStack.push1(expect_localctxname(with_loc(Getname)))|GlobalGetx->(* As for [LocalGet]: record the global's numeric type on the node. *)letname=idxctx`GlobalxinStack.push1(expect_globalctxname(with_loc(Getname)))|LocalSetx->(* Record the target's numeric type on the assigned VALUE too, not only on a
[Get]: a hole assigned to a numeric local is a hole the re-parse types
numerically, which is what [Stack.effective_backing] needs to know to see
past the statement (see [hole_claims]). *)letname=idxctx`Localxinlet*e=Stack.popinStack.push0(with_loc(set_descname(expect_localctxnamee)))|GlobalSetx->letname=idxctx`Globalxinlet*e=Stack.popinStack.push0(with_loc(set_descname(expect_globalctxnamee)))|LocalTeex->letname=idxctx`Localxinlet*e=Stack.popin(* The tee's own result has the local's type too, so record it on the
node as well as on the assigned value. *)Stack.push1(expect_localctxname(with_loc(Tee(name,expect_localctxnamee))))|BinOp(I32op)->int_bin_opctxi`I32op|BinOp(I64op)->int_bin_opctxi`I64op|BinOp(F32op)->float_bin_opctxi`F32op|BinOp(F64op)->float_bin_opctxi`F64op|Add128|Sub128|MulWide_->(* Wide arithmetic decompiles to the [i64::...] path intrinsics, whose two
i64 results are consumed by a multi-value [let]. *)letname,input=matchi.descwith|Add128->("add128",4)|Sub128->("sub128",4)|MulWideSigned->("mul_wide_s",2)|MulWideUnsigned->("mul_wide_u",2)|_->assertfalseinlet*args=Stack.grabinputin(* Both results are i64: record it (the all-numeric multi-value mark the
backing scan reads — see [functype_value_result]). *)Stack.push2(expectI64(path_call"i64"nameargs))|UnOp(I64op)->int_un_op~faithful:ctx.faithfuli`I64op|UnOp(I32op)->int_un_op~faithful:ctx.faithfuli`I32op|UnOp(F64op)->float_un_opi`F64op|UnOp(F32op)->float_un_opi`F32op|StructNewi->lettype_name=idxctx`Typeiinletfields=snd(struct_fieldsctxtype_name)inlet*args=Stack.grab(List.lengthfields)inStack.push1(with_loc(Struct(Some(idxctx`Typei),List.map2struct_fieldfieldsargs)))|StructNewDefaulti->Stack.push1(with_loc(StructDefault(Some(idxctx`Typei))))|StructNewDesci->lettype_name=idxctx`Typeiinletfields=snd(struct_fieldsctxtype_name)in(* The descriptor operand is on top of the field values. The struct type is
recovered from the descriptor, so it is not written. *)let*d=Stack.popinletd=pin_descriptorctx~exact:trueidinlet*args=Stack.grab(List.lengthfields)inStack.push1(with_loc(StructDesc(d,List.map2struct_fieldfieldsargs)))|StructNewDefaultDesci->let*d=Stack.popinletd=pin_descriptorctx~exact:trueidinStack.push1(with_loc(StructDefaultDescd))|StructGet(s,t,f)->lettype_name=idxctx`Typetinletname=Sequence.get(fst(struct_fieldsctxtype_name))finlet*arg=Stack.popinlet*arg=pin_receiverctxtype_name~siblings:[]arginlete=with_loc(StructGet(arg,name))inStack.push1(matchswith(* A packed field read with a sign ([struct.get_s/u] of an [i8]/[i16])
yields an i32 whatever the field's width; an unsigned read of a
non-packed field yields the field's own type. Record either. *)|None->expect_value_result(struct_field_value_typectxtype_namename)e|Somesignage->expectI32(with_loc(Cast(e,Signedtype{typ=`I32;signage;strict=false}))))|StructSet(t,f)->lettype_name=idxctx`Typetinletname=Sequence.get(fst(struct_fieldsctxtype_name))finlet*e2=Stack.popinlet*e1=Stack.popinlet*e1=pin_receiverctxtype_name~siblings:[e2]e1inStack.push0(with_loc(StructSet(e1,name,e2)))|ArrayNewt->let*len=Stack.popinlet*v=Stack.popinStack.push1(with_loc(Array(Some(idxctx`Typet),v,len)))|ArrayNewDefaultt->let*len=Stack.popinStack.push1(with_loc(ArrayDefault(Some(idxctx`Typet),len)))|ArrayNewFixed(t,n)->(* [n] is a u32 immediate and each element becomes an argument node, so a
faithful decompilation of a huge [n] is inherently that large (the
operands not on the stack are filled with holes). No validation runs
on this path, so an adversarial [n] (e.g. 2^31) makes conversion slow /
memory-hungry. Left unguarded by design: capping [n] would silently
mis-convert valid dead code that legitimately needs holes, and any real
module's count is small. Validation itself is O(operands present) --
see [pop_repeat] in validation.ml. *)let*args=Stack.grab(Uint32.to_intn)in(* A string only builds an [i8] (raw bytes) or [i16] (UTF-16) array, so
only those decode back to a string literal; any other element type
stays an array literal. *)letstr=match(lookup_typectxTypet).typwith|Array{typ=PackedI8;_}->string_argsnargs|Array{typ=PackedI16;_}->wide_string_argsnargs|_->NoneinStack.push1(matchstrwith|Somes->with_loc(String(Some(idxctx`Typet),s))|None->with_loc(ArrayFixed(Some(idxctx`Typet),args)))|ArrayGet(s,t)->let*e2=Stack.popinlet*e1=Stack.popinlet*e1=pin_receiverctx(idxctx`Typet)~siblings:[e2]e1inlete=with_loc(ArrayGet(e1,e2))inStack.push1(matchswith(* As [StructGet], for the array's element type. *)|None->expect_value_result(array_element_value_typectx(idxctx`Typet))e|Somesignage->expectI32(with_loc(Cast(e,Signedtype{typ=`I32;signage;strict=false}))))|ArraySett->let*e3=Stack.popinlet*e2=Stack.popinlet*e1=Stack.popinlet*e1=pin_receiverctx(idxctx`Typet)~siblings:[e2;e3]e1inStack.push0(with_loc(ArraySet(e1,e2,e3)))|Callf->letinput,output=function_arityctxfinlet*args=Stack.grabinputinletname=idxctx`Funcfinlettu=lookup_typectxFuncfin(* A multi-value signature with reference results cannot record its
composition on the node (the expectation channel is single-valued);
remember the reference hierarchies under the Wax name instead, for
the converts' backing test (see [ctx.multi_ref_results]). *)(ifoutput>=2thenmatchtypeuse_functypectxtuwith|Some{Src.results;_}->Hashtbl.replacectx.multi_ref_resultsname.Ast.desc(result_classesctxresults)|None->());Stack.pushoutput(expect_value_result(typeuse_value_resultctxtu)(with_loc(Call(with_loc(Getname),args))))|CallReft->letinput,output=type_arityctxtinletresult_ty=type_value_resultctxtinlet*f=Stack.popinlet*f=pin_calleectxtfinlet*args=Stack.grabinputinStack.pushoutput(expect_value_resultresult_ty(with_loc(Call(f,args))))|ReturnCallf->letinput,_=function_arityctxfinlet*args=Stack.grabinputinStack.push_poly(with_loc(TailCall(with_loc(Get(idxctx`Funcf)),args)))|ReturnCallReft->letinput,_=type_arityctxtinlet*f=Stack.popinlet*f=pin_calleectxtfinlet*args=Stack.grabinputinStack.push_poly(with_loc(TailCall(f,args)))|Return->let*args=Stack.grabctx.return_arityinStack.push_poly(with_loc(Return(sequence_optargs)))|Constc->letlit,ty,width=matchcwith|I32n->(integeri.Src.infon,Ast.I32,`I32)|I64n->(integeri.Src.infon,Ast.I64,`I64)|F32f->(floatif,Ast.F32,`F32)|F64f->(floatif,Ast.F64,`F64)in(* [push_num] records on the pushed node; under [strict_constants] that is
the pin cast, so the literal beneath it carries the claim [Contextual]ly
(the cast ascribes its type). *)Stack.push_num(Somewidth)(ifctx.strict_constantsthenwith_loc(Cast(contextuallit,Valtypety))elselit)|RefI31->(* Source is i32; a dead-code hole is pinned [(_ as i32)] so [ref.i31]
survives (a bare [_ as &i31] re-types the hole to a null i31 and drops the
conversion). *)let*e=Stack.popinStack.push1(with_loc(Cast(type_hole_srcI32e,Valtype(Ref{nullable=false;typ=I31}))))|I31Getsignage->(* Source is [&?i31]; a dead-code hole is pinned [(_ as &?i31)] so [i31.get]
survives (a bare [_ as i32_s] re-types the hole to i32 and drops it). *)let*e=Stack.popinStack.push1(expectI32(with_loc(Cast(type_hole_src(Ref{nullable=true;typ=I31})e,Signedtype{typ=`I32;signage;strict=false}))))|I64ExtendI32signage->(* Source is i32; a dead-code hole is pinned [(_ as i32)] so the widen
survives (a bare [_ as i64_s] drops the [extend_i32_s]). *)let*e=Stack.popinStack.push1(expectI64(with_loc(Cast(type_hole_srcI32e,Signedtype{typ=`I64;signage;strict=false}))))|I32WrapI64->(* Width eraser: [i32.wrap_i64 (4096 >>u 40)] is 0, but a bare [4096 >>u 40]
re-defaults to i32 and the shift count masks to 8, yielding 16 (a LIVE
miscompilation). Pin the i64 operand; a dead-code hole is pinned
[(_ as i64)] so [(_ as i64) as i32] re-emits the [wrap] (else it drops). *)let*e=Stack.popinStack.push1(expectI32(with_loc(Cast(type_hole_srcI64e,ValtypeI32))))|F64PromoteF32->(* Width eraser: the f32 source is not carried by [e as f64]; a bare float
literal re-defaults to f64, dropping the promote (and its f32 rounding).
A dead-code hole is pinned [(_ as f32)] so the promote survives. *)let*e=Stack.popinStack.push1(expectF64(with_loc(Cast(type_hole_srcF32e,ValtypeF64))))|F32DemoteF64->(* Width eraser: an integer-valued f64 source prints as a bare integer that
re-defaults to i32, turning the demote into an i32->f32 convert. A
dead-code hole is pinned [(_ as f64)] so the demote survives. *)let*e=Stack.popinStack.push1(expectF32(with_loc(Cast(type_hole_srcF64e,ValtypeF32))))|ExternConvertAny->(* Source is [&?any]; a dead-code hole is pinned [(_ as &?any)] so the
conversion survives (a bare [_ as &?extern] re-types the hole and drops
it). A forwarding [br_on_null] residual on top has its tested ref pinned
to the source too, so a stranded fall-through hole reconnects there.
EXCEPT when a source-hierarchy residual (or a null) backs the hole:
the printed hole reconnects to it and the convert's own [as] surface
lowers over the real value — the pin would land on that reconnected
value instead and materialise as a [ref.cast] the source never had
(the backing-scan grid's founding convert cluster). Left bare, the
result stays NULLABLE: the reconnected value may be a null.
And over a backing provably OUTSIDE the source hierarchy — reachable
only through an [(@if)], whose branches consume it per configuration —
neither works: bare, the hole takes the wrong-hierarchy value and the
convert collapses into (or compounds with) a crossing the source never
had; pinned at the source TOP, the pin captures that value and
materialises the same crossing. The source hierarchy's BOTTOM is the
claim-free pin that grounds the hole and leaves the residual to the
branch that consumes it (see [backing_wrong_hierarchy]). *)letsrc:Ast.valtype=Ref{nullable=true;typ=Any}inlet*e=Stack.popinlet*()=ifhole_reconnectsethenpin_forwarding_sourcesrcelsereturn()inlet*backing,crossed_any=Stack.effective_backingis_poly_terminatorinletadaptive_null=is_bare_holee&&matchbackingwith|`Backing(b,_,_)->backing_adaptive_nullb|`Value|`Floor|`Blocked->falsein(* An UNCLASSIFIABLE residual is the third outcome: its printed form says
nothing about its hierarchy, so it is neither left to a bare hole nor
safe to pin over — the pin lands on it and materialises exactly the
[ref.cast] this arm's rule is about. Ground the residual at the source
instead ([pin_backing_source]); it is then IN the source hierarchy, so
[backed] leaves the hole bare and the convert keeps its one opcode. *)let*grounded=matchbackingwith|`Backing(b,from_top,_)whenhole_reconnectse&&(notadaptive_null)&&backing_needs_groundingctx~from_topb->let*()=pin_backing_sourcesrcbinreturntrue|_->returnfalseinletbacked=is_bare_holee&&(notadaptive_null)&&(grounded||matchbackingwith|`Backing(b,from_top,_)->backing_in_hierarchyctx`Any~from_topb|`Value|`Floor|`Blocked->false)inletwrong=is_bare_holee&&matchbackingwith|`Backing(b,from_top,_)->backing_wrong_hierarchyctx`Any~from_topb|`Value->true(* Annotation in play: the claiming source pin can capture what a
branch's pushes released; the source-hierarchy BOTTOM pin is the
claim-free spelling and the convert lowers over it identically. *)|`Floor|`Blocked->crossed_anyin(* A bare hole is pinned NON-NULL and the convert preserves non-nullness, so
the RESULT is non-null too. State that here rather than leaving it to the
typer's refinement, which is gated on [simplify] and so does not happen
under [--faithful] — there the nullable target made the decompiled Wax
ill-typed against a non-null consumer. *)(* An adaptive-null backing keeps the pin NULLABLE: the value the printed
hole reconnects to is a null, which a non-null pin could not type. *)letnullable=backed||adaptive_null||not(is_bare_holee)inletoperand=ifbackedtheneelseifadaptive_nullthenascribe_tosrceelseifwrongthenascribe_to(Ref{nullable=false;typ=Any})eelseconvert_src~nullablesrceinStack.push1(with_loc(Cast(operand,Valtype(Ref{nullable;typ=Extern}))))|AnyConvertExtern->(* Source is [&?extern]; a dead-code hole is pinned [(_ as &?extern)] so the
conversion survives — except over an extern-hierarchy backing, and with
the wrong-hierarchy bottom pin [(_ as &noextern)], exactly as
[ExternConvertAny] above. A forwarding [br_on_null] residual on top has
its tested ref pinned to the source. *)letsrc:Ast.valtype=Ref{nullable=true;typ=Extern}inlet*e=Stack.popinlet*()=ifhole_reconnectsethenpin_forwarding_sourcesrcelsereturn()inlet*backing,crossed_any=Stack.effective_backingis_poly_terminatorinletadaptive_null=is_bare_holee&&matchbackingwith|`Backing(b,_,_)->backing_adaptive_nullb|`Value|`Floor|`Blocked->falsein(* As [RefCast]: this source pin crosses hierarchies, so an unclassifiable
residual is grounded at the source rather than captured by a pin on the
hole (see [pin_backing_source]). Grounded, it IS in the source
hierarchy, so [backed] below leaves the hole bare. *)let*grounded=matchbackingwith|`Backing(b,from_top,_)whenhole_reconnectse&&(notadaptive_null)&&backing_needs_groundingctx~from_topb->let*()=pin_backing_sourcesrcbinreturntrue|_->returnfalseinletbacked=is_bare_holee&&(notadaptive_null)&&(grounded||matchbackingwith|`Backing(b,from_top,_)->backing_in_hierarchyctx`Extern~from_topb|`Value|`Floor|`Blocked->false)inletwrong=is_bare_holee&&matchbackingwith|`Backing(b,from_top,_)->backing_wrong_hierarchyctx`Extern~from_topb|`Value->true(* As [ExternConvertAny]: claim-free under an annotation. *)|`Floor|`Blocked->crossed_anyin(* As [ExternConvertAny]: a non-null pin gives a non-null result. *)(* An adaptive-null backing keeps the pin NULLABLE: the value the printed
hole reconnects to is a null, which a non-null pin could not type. *)letnullable=backed||adaptive_null||not(is_bare_holee)inletoperand=ifbackedtheneelseifadaptive_nullthenascribe_tosrceelseifwrongthenascribe_to(Ref{nullable=false;typ=Extern})eelseconvert_src~nullablesrceinStack.push1(with_loc(Cast(operand,Valtype(Ref{nullable;typ=Any}))))|ArrayNewData(t,d)->let*len=Stack.popinlet*off=Stack.popinStack.push1(with_loc(ArraySegment(Some(idxctx`Typet),idxctx`Datad,off,len)))|ArrayNewElem(t,e)->let*len=Stack.popinlet*off=Stack.popinStack.push1(with_loc(ArraySegment(Some(idxctx`Typet),idxctx`Eleme,off,len)))|TableGett->let*index=Stack.popinStack.push1(with_loc(ArrayGet(with_loc(Get(idxctx`Tablet)),index)))|TableSett->let*value=Stack.popinlet*index=Stack.popinStack.push0(with_loc(ArraySet(with_loc(Get(idxctx`Tablet)),index,value)))(* call_indirect desugars to [(tab[i] as &$functype)(args)] (a call_ref);
[to_wasm] re-fuses this back to call_indirect. *)|CallIndirect(tab,tu)->letinput,output=typeuse_arityctxtuinlet*index=Stack.popinlet*args=Stack.grabinputinletf=indirect_calleectxwith_loctabtuindexinStack.pushoutput(expect_value_result(typeuse_value_resultctxtu)(with_loc(Call(f,args))))|ReturnCallIndirect(tab,tu)->letinput,_=typeuse_arityctxtuinlet*index=Stack.popinlet*args=Stack.grabinputinletf=indirect_calleectxwith_loctabtuindexinStack.push_poly(with_loc(TailCall(f,args)))|ArrayLen->let*e=Stack.popinlete=cast_refeArrayinStack.push1(expectI32(with_loc(Call(with_loc(StructGet(e,Ast.no_loc"length")),[]))))|RefCastt->let*e=Stack.popin(* A cast into the EXTERN hierarchy shares the Wax [as &extern] surface
with the cross-hierarchy conversion [extern.convert_any]. An operand that
fixes no hierarchy of its own — a dead-code hole, or one forwarded
through [!] ([ref.as_non_null]) — types in the ANY hierarchy on a
re-parse, so the cast re-lowers to [extern.convert_any]: an
opcode-family change, not a width drift. Pin such an operand with
[(_ as &?extern)], the reference analogue of the numeric width pins (and
of [ref.is_null]'s [(_ as &?any)]). Only this direction needs it: a hole
cast to the any/func hierarchy already re-lowers to [ref.cast], and a
real extern operand fixes the hierarchy itself and is left bare.
But that pin may only capture NOTHING. Alone among the
top-of-hierarchy pins it CROSSES hierarchies, so it is not inert over
whatever it lands on: [(_ as &?extern)] over an any-hierarchy value IS
[extern.convert_any], the very opcode-family change it exists to
prevent. So it is applied only to a hole springing from the polymorphic
bottom with no residual to reconnect to ([`Floor] / [`Blocked]) — there
the bare cast would otherwise be absorbed and lost entirely. Over a
real [`Backing] the hole stays bare and reconnects: an ADAPTIVE
residual (a forwarding [br_on_null], an untyped [select]) then takes
the extern hierarchy from this cast's own [as] surface, one opcode,
exactly the source, while a pin would land on it and manufacture the
convert (a wasm-smith FAITHDRIFT on
[br 'l ; br_on_null 'l ; nop ; ref.cast (ref noextern)], the [nop]
splitting the cast off the residual it would otherwise have consumed
directly). A residual that is NOT adaptive is grounded at the source
instead ([pin_backing_source]), and a [`Value] backing — or one
provably in another hierarchy — takes the claim-free bottom below. *)lettarget=reftypectxtinlet*backing,crossed_any=Stack.effective_backingis_poly_terminatorinletbottom_sprung=matchbackingwith|`Floor|`Blocked->true|`Backing_|`Value->falseinletextern_target=matchtarget.typwithExtern|NoExtern->true|_->falseinletextern_src:Ast.valtype=Ref{nullable=true;typ=Extern}in(* An unclassifiable residual takes the source hierarchy itself, so the
hole below it reconnects there and stays bare (see
[pin_backing_source]). *)let*()=matchbackingwith|`Backing(b,from_top,_)whenextern_target&&hole_reconnectse&&backing_needs_groundingctx~from_topb->pin_backing_sourceextern_srcb|_->return()in(* And over a backing provably outside the TARGET's hierarchy — reachable
only through an [(@if)], whose branches consume it per configuration —
a bare hole would capture it, and typing the capture compounds this
cast with the hierarchy crossing the re-parse needs (the backing-scan
[+cast] cells: an extern residual under [ref.cast (ref null $s)]
re-lowered with an [any.convert_extern] the source never had). Ground
the hole with the claim-free bottom of the target's hierarchy instead;
the [((_ as &?none) as &?s)] chain lowers to nothing, which is the
absorbed spelling a dead cast of the polymorphic bottom already
round-trips to. A SAME-hierarchy capture stays bare: it is either the
cast's own operand (no annotation in between — the validator typed it
there) or re-lowers as at most this cast's own [ref.cast]. *)lettarget_hier=heaptype_hierarchyctxtarget.typinletwrong=matche.Ast.descwith|Ast.Hole->(matchbackingwith|`Value->true|`Backing(b,from_top,crossed)->(crossed&&matchbacking_class_ofctx~from_topbwith|Ref_class{hier;_}->Somehier<>target_hier|Value_class->true|Null_class|Unknown_class->false)(* As [pin_callee]: annotation in play, claiming pin unsafe. *)|`Floor|`Blocked->crossed_any)|_->falsein(* The claim-free grounding is decided BEFORE the extern pin and wins over
it. Both target the same bare hole, and the pin — being a cast — claims
a pending value, which is exactly what an annotation in play makes
unsafe; applied first it also replaced the [Hole] this test matches on,
so the grounding could never fire for an extern target at all (the
backing-scan [Rnn.ScondPushR.Bp1] cell: the claiming pin captured a
value a branch released and re-lowered as an [extern.convert_any]). *)lete=ifwrongthenascribe_to(Ref{nullable=true;typ=hierarchy_bottomtarget_hier})eelseifextern_target&&bottom_sprungthenOption.value~default:e(pin_hierarchy(Ast.Valtypeextern_src)e)elseeinStack.push1(with_loc(Cast(e,Valtype(Reftarget))))|RefCastDescEqt->(* The descriptor operand is on top of the value. The target type and its
exactness are recovered from the descriptor, so only [t]'s result
nullability is kept. *)let*d=Stack.popinletd=pin_descriptor_reftypectxtdinlet*e=Stack.popinStack.push1(with_loc(CastDesc(e,t.nullable,d)))|RefGetDesct->lettype_name=idxctx`Typetinlet*arg=Stack.popin(* [ref.get_desc $t] requires its operand to be [<: (ref null (exact? $t))],
so a concrete operand already carries a descriptor-bearing type and
[e.descriptor] resolves directly — casting it to [&?$t] would only strip
its exactness (the result's exactness mirrors the operand's). Cast only
an operand with no descriptor of its own: a bottom reference (a hole in
dead code, or a [ref.null none]-style null cast to a bottom heap type)
or a bare null. *)(* A bottom operand fits the *exact* operand type, and [ref.get_desc]'s
result is then exact (validation takes the most precise), so pin the
exact descriptor type. Rewrite the target of an existing bottom cast
([ref.null none] → [null as &?none]) in place rather than wrapping it —
a nested [(null as &?none) as &?!t] is folded back to the bottom by
[simplify], undoing the pin. *)letexact_pin=Ast.Valtype(Ref{nullable=true;typ=Exacttype_name})inletis_bottom(t:Ast.heaptype)=matchtwith|None_|NoFunc|NoExtern|NoExn|NoCont->true|_->falseinletarg=matcharg.Ast.descwith|Ast.Hole|Ast.Null->cast_toexact_pinarg(* Both bottom spellings, as above. *)|Ast.Cast(inner,(Valtype(Ref{typ;_})|Ascribed(Ref{typ;_})))whenis_bottomtyp->{argwithdesc=Ast.Cast(inner,exact_pin);expected=cast_resultexact_pin;}|_->arginStack.push1(with_loc(GetDescriptorarg))|RefTestt->let*e=Stack.popinlettarget=reftypectxtin(* As [RefCast]: over a backing provably outside the target's hierarchy,
or with an annotation anywhere in the stack (whose branches shuffle
what a bare hole would capture per configuration), ground the hole
with the claim-free bottom of the target's hierarchy — [ref.test]
carries its type immediate, so the pinned [(_ as &?none) is &?s]
still lowers to exactly the source opcode. *)let*e=matche.Ast.descwith|Ast.Hole->let*backing,crossed_any=Stack.effective_backingis_poly_terminatorinlettarget_hier=heaptype_hierarchyctxtarget.typinletwrong=matchbackingwith|`Value->true|`Backing(b,from_top,crossed)->(crossed&&matchbacking_class_ofctx~from_topbwith|Ref_class{hier;_}->Somehier<>target_hier|Value_class->true|Null_class|Unknown_class->false)|`Floor|`Blocked->crossed_anyinreturn(ifwrongthenascribe_to(Ref{nullable=true;typ=hierarchy_bottomtarget_hier})eelsee)|_->returneinStack.push1(expectI32(with_loc(Test(e,target))))|RefEq->(* [ref.eq] shares the Wax [==] surface with the numeric comparisons, but
the numeric width pin ([(_ as i64)]) is an [as t] cast and cannot spell
a ref type: with no anchor two bare holes re-parse as the numeric
[i32.eq] — a family change, not just a width drift. Leave the holes bare
whenever a real ref value backs the comparison (the typer unifies them
to it): a present anchor (a ref value, whose printed form carries its
type), or a value residual [effective_backing] finds below any interposed
zero-value statements (a ref by validity, so the bare [_ == _] recovers
[ref.eq]). Only when both operands spring from the polymorphic bottom (a
terminator sentinel / empty, reached through interposed dead statements)
pin one hole with a nullable eq-ref cast [(_ as &?eq)]. (A ref pin cannot
be blindly applied like a numeric one: a numeric [as] is always valid, but
casting a value the typer already typed in another reference hierarchy to
[&?eq] is a static error — hence it is only used over the polymorphic
bottom, where it is a valid convert, never over a value residual, which is
detected and left bare regardless of its hierarchy.) *)let*o2=Stack.try_popinlet*o1=Stack.try_popinlet*backing,crossed_any=Stack.effective_backingis_poly_terminatorinletbare=bare_hole()inleteq_pine=Ast.no_loc_instr(Ast.Cast(e,Valtype(Ref{nullable=true;typ=Eq})))in(* An UNANNOTATED [select] operand is pinned, exactly as [RefIsNull] pins the
same shape: only a numeric select is unannotated, so in dead code it is a
polymorphic select of holes that re-parses to the numeric form — and then
[==] on it is an [i32.eq], an opcode-family change. It backs no concrete
reference either, so it does not count towards [backed]. *)letis_selecto=matchowithSome{Ast.desc=Ast.Select_;_}->true|_->falseinletbackso=Option.is_someo&¬(is_selecto)inletbacked=backso1||backso2||matchbackingwith|`Backing_->true|`Value|`Floor|`Blocked->falsein(* A backing that provably re-types as something OTHER than an
[eq]-subtype — an extern/func-hierarchy reference or a non-reference
multi-value residual, reachable only through an [(@if)] whose branches
consume it per configuration: a bare hole would capture it and the
[==] would not type-check (or re-default numeric). Ground each hole
with the claim-free bottom [(_ as &?none)] instead, leaving the value
to the branch that consumes it (see [backing_not_eq]). *)letwrong=matchbackingwith|`Backing(b,from_top,_)->backing_not_eqctx~from_topb|`Value->true(* Annotation in play: the claiming [(_ as &?eq)] pin can capture what
a branch's pushes released (a funcref made it a hierarchy crossing);
the bottom pins are the claim-free spelling. *)|`Floor|`Blocked->crossed_anyinletbottom_pine=ascribe_to(Ref{nullable=true;typ=None_})einlete1=matcho1with|Some({Ast.desc=Ast.Select_;_}ase)->eq_pine|Somee->e|None->ifwrongthenbottom_pinbareelseifbackedthenbareelseeq_pinbareinlete2=matcho2with|Some({Ast.desc=Ast.Select_;_}ase)->eq_pine|Somee->e|None->ifwrongthenbottom_pin(bare_hole())elsebareinStack.push1(expectI32(with_loc(BinOp(op_loci.infoAst.Eq,e1,e2))))|RefFuncf->Stack.push1(with_loc(Get(idxctx`Funcf)))|RefNulltyp->Stack.push1(with_loc(Cast(with_locNull,Valtype(Ref{nullable=true;typ=heaptypectxtyp}))))|RefIsNull->(* [ref.is_null] lowers to the Wax [!e] surface, which it shares with
[i32.eqz]; with no ref backing it a bare hole re-parses as [i32.eqz] (a
family change). Leave the hole bare whenever a real ref backs it (a
present anchor, or a value residual [effective_backing] finds below any
interposed zero-value statements — a ref by validity, so the typer types
[!_] to [ref.is_null]). Pin with a nullable any-ref cast [(_ as &?any)]
when the hole springs from the polymorphic bottom instead: a terminator
sentinel or empty stack, INCLUDING one reached through interposed dead
statements (a [br_if] whose condition consumed the value just above, so
its own arity-0 entry no longer backs anything — the shape [effective_
backing] skips to reach the sentinel). [ref.is_null] carries no immediate,
so any nullable ref top recovers it and [&?any] is canonical.
A present operand that is an UNANNOTATED [select] is pinned too: only a
numeric [select] is unannotated, so in dead code it is a polymorphic
[select] of holes that re-parses to the numeric [i32] select unless
pinned; it backs no concrete ref. (Every pin is kept only when
load-bearing: [simplify]/[--faithful] drop it for a concrete ref operand,
where [ty' <: &?any] holds, and keep it otherwise.) *)let*o=Stack.try_popinlet*backing,crossed_any=Stack.effective_backingis_poly_terminatorinletany_pine=Ast.no_loc_instr(Ast.Cast(e,Valtype(Ref{nullable=true;typ=Any})))in(* The claim-free bottom pin, for a hole whose positional capture would
be a provable NON-reference (see [backing_not_ref] and the scan's
[`Value] verdict): [(_ as &?none)] grounds the hole without taking a
pending value, so the numeric residual stays for the [(@if)] branch
that consumes it. *)letref_bottom_pin()=ascribe_to(Ref{nullable=true;typ=None_})(bare_hole())in(* The innermost enclosing block's own parameters are its first stack
values, so a REFERENCE among them backs this hole exactly as a value
residual does: the hole reconnects to the parameter on re-parse and takes
its type, and pinning [&?any] over it would cross hierarchies instead —
an [(&?noextern)] block parameter had the pin materialise an
[any.convert_extern] (a wasm-smith finding). Leave it bare and let the
parameter type it — but ONLY when the scan reached the block floor
cleanly ([`Floor]): past a terminator ([`Blocked]) the printed hole is
bottom-sprung and reconnects to nothing, so the parameter cannot type it
and the bare [!_] would re-default to [i32.eqz] (a ref-width grid
finding: [do (&?extern) { unreachable; !_ }]). *)letbacked_by_block_param=matchctx.block_paramswith|[||]->false|params->(matchparams.(Array.lengthparams-1)with|Src.Ref_->true|_->false)in(* A backing that provably re-types as a NON-reference (a multi-value
residual whose last result is numeric — reachable as a hole's backing
only through an [(@if)] whose branches consume it per configuration):
a bare [!_] capturing it re-defaults to [i32.eqz], and the [(_ as
&?any)] pin capturing it does not type-check. Ground the hole with the
claim-free bottom [(_ as &?none)] instead (see [backing_not_ref]). *)lete=matchowith|Some({Ast.desc=Ast.Select_;_}ase)->any_pine|Somee->e|None->(matchbackingwith|`Value->ref_bottom_pin()|`Backing(b,from_top,_)whenbacking_not_refctx~from_topb->ref_bottom_pin()|`Backing_->bare_hole()|`Floorwhenbacked_by_block_param->bare_hole()(* Annotation in play: the claiming [(_ as &?any)] pin can capture
what a branch's pushes released (a funcref/cont capture no
longer type-checked); the bottom pin is claim-free. *)|(`Floor|`Blocked)whencrossed_any->ref_bottom_pin()|`Floor|`Blocked->any_pin(bare_hole()))inStack.push1(expectI32(with_loc(UnOp(op_loci.infoAst.Not,e))))|Selecttys->((* The Wax [?:] carries no result type, so resolve the annotation (if any)
both to catch an out-of-range type reference and to recover the single
result type: a typed [select (result t)] with [t] one of [i64]/[f32]/[f64]
or a REFERENCE type must survive re-parse, but with no type on the [?:] an
anchor-free numeric arm re-defaults to i32 and an anchor-free reference
arm re-defaults to the numeric [i32] select — dropping the ref type, which
a downstream [ref.is_null] then reads as [i32.eqz] (an opcode-family
change). ([i32] itself is the re-parse default and needs no pin; an
untyped select is genuinely typeless.) A ref pin is only safe with no
anchored operand (guarded below by [not any_anchor]): casting an operand
the typer already placed in another hierarchy to [t] would be a static
error, but a bare hole is polymorphic. *)letsel_ty=matchtyswith|Some[t]->(matchvaltypectxtwithI32->None|t->Somet)|Somets->List.iter(funt->ignore(valtypectxt:Ast.valtype))ts;None|None->Noneinlet*cond=Stack.popinlet*o2=Stack.try_pop_taggedinlet*o1=Stack.try_pop_taggedinletis_hole=functionNone->true|_->falseinletany_anchor=is_anchoro1||is_anchoro2inmatchsel_tywith|Sometwhennotany_anchor->(* Pin exactly one arm to the select's type: a hole if there is one
(cast on the hole), else result-cast the first flexible arm. That
grounds the whole select, so it takes no tag. Kept for the REFERENCE
case, which the width reconciliation does not cover (numeric scalars
only) and where a bare [?:] would lose the hierarchy; the numeric
widths ride along on the same path. *)letpin1_hole=is_holeo1inletpin2_hole=is_holeo2&¬pin1_holeinletpin1_flex=(notpin1_hole)&¬pin2_holeinletpinned=typed_holetinletcaste=cast_to(Valtypet)einlete1=matcho1with|Some(e,_)->ifpin1_flexthencasteelsee|None->ifpin1_holethenpinnedelsebare_hole()inlete2=matcho2with|Some(e,_)->e|None->ifpin2_holethenpinnedelsebare_hole()inStack.push_numNone(expectt(with_loc(Select(cond,e1,e2))))|_->(* Untyped/i32/reference select, or a typed select an arm anchors: the
result width is that of the arms (both share the select's type),
flexible only when BOTH arms are flexible literal trees; if either is
grounded it fixes the width and re-parse resolves the other to it (as
in [int_bin_op]'s [symbol]). Carrying the combined tag lets a
downstream eraser ([i32.wrap_i64]) pin the arms so a flexible i64
select doesn't re-default to i32.
When only ONE arm is present (a dead-code hole in the other), the hole
cannot anchor the present arm's width, and the untyped select carries
no result type to pin it either, so a present arm with a non-default
width tag ([f32.const], a small [i64.const]) is pinned directly
([_ ? (0x0 as f32) : _]) — otherwise the bare literal re-defaults
(f32 -> f64, i64 -> i32) on re-parse. *)lethole=bare_hole()inlete1,e2,width=match(o1,o2)with|Some(a,wa),Some(b,wb)->letwidth=match(wa,wb)withSome_,Some_->wa|_->Nonein(a,b,width)|Some(a,_),None->(a,hole,None)|None,Some(b,_)->(hole,b,None)|None,None->(hole,hole,None)in(* With no width tag ([None]) the untyped select is deliberately
ADAPTIVE — its arms are — so it is [Contextual], not a gap; a
tagged one gets the tag recorded by [push_num] itself. *)Stack.push_numwidth(contextual(with_loc(Select(cond,e1,e2)))))|Throwt->letinput,_=tag_arityctxtinlet*args=Stack.grabinputinStack.push_poly(with_loc(Throw(idxctx`Tagt,args)))|ThrowRef->let*e=Stack.popinStack.push_poly(with_loc(ThrowRefe))|ContNewct->let*f=Stack.popinStack.push1(with_loc(ContNew(idxctx`Typect,f)))|ContBind(src,dst)->letsp,_=cont_arityctxsrcinletdp,_=cont_arityctxdstinlet*args=Stack.grab(sp-dp+1)inletsrc=idxctx`TypesrcinStack.push1(with_loc(ContBind(src,idxctx`Typedst,ascribe_contsrcargs)))(* The stack-switching results ([suspend]/[resume]/[switch]) are [Contextual]:
their types come from the DECLARED tag/continuation signature the printed
form still names (the tag or the [ct] immediate), so a re-parse re-derives
them from the declarations — these arms only know the result arity, not the
types, and need no claim of their own. *)|Suspendt->letinput,output=tag_arityctxtinlet*args=Stack.grabinputinStack.pushoutput(contextual(with_loc(Suspend(idxctx`Tagt,args))))|Resume(ct,handlers)->letinput,output=cont_arityctxctinlet*args=Stack.grab(input+1)inletct=idxctx`TypectinStack.pushoutput(contextual(with_loc(Resume(ct,List.map(on_clausectx)handlers,ascribe_contctargs))))|ResumeThrow(ct,tag,handlers)->lettinput,_=tag_arityctxtaginlet_,output=cont_arityctxctinlet*args=Stack.grab(tinput+1)inletct=idxctx`TypectinStack.pushoutput(contextual(with_loc(ResumeThrow(ct,idxctx`Tagtag,List.map(on_clausectx)handlers,ascribe_contctargs))))|ResumeThrowRef(ct,handlers)->let_,output=cont_arityctxctinlet*args=Stack.grab2inletct=idxctx`TypectinStack.pushoutput(contextual(with_loc(ResumeThrowRef(ct,List.map(on_clausectx)handlers,ascribe_contctargs))))|Switch(ct,tag)->letinput,_=cont_arityctxctinletoutput=switch_outputctxctinlet*args=Stack.grabinputinletct=idxctx`TypectinStack.pushoutput(contextual(with_loc(Switch(ct,idxctx`Tagtag,ascribe_contctargs))))|RefAsNonNull->let*e=Stack.popinStack.push1(with_loc(NonNulle))|ArrayFillt->let*n=Stack.popinlet*v=Stack.popinlet*i=Stack.popinlet*a=Stack.popinlet*a=pin_receiverctx(idxctx`Typet)~siblings:[i;v;n]ainStack.push0(with_loc(Call(with_loc(StructGet(a,Ast.no_loc"fill")),[i;v;n])))|ArrayCopy(t1,t2)->let*n=Stack.popinlet*i2=Stack.popinlet*a2=Stack.popinlet*i1=Stack.popinlet*a1=Stack.popinlet*a2=pin_receiverctx(idxctx`Typet2)~siblings:[i2;n]a2inlet*a1=pin_receiverctx(idxctx`Typet1)~siblings:[i1;a2;i2;n]a1inStack.push0(with_loc(Call(with_loc(StructGet(a1,Ast.no_loc"copy")),[i1;a2;i2;n])))|Load(m,memarg,nt)->let*addr=Stack.popinletmeth,nat=matchntwith|NumI32->("load32",4)|NumI64->("load64",8)|NumF32->("loadf32",4)|NumF64->("loadf64",8)in(* Record the type the method name states ([m.load64] is an i64), so a dead
load residual is recognised as numeric by [Stack.effective_backing] rather
than mistaken for a reference backing. *)Stack.push1(expect(matchntwith|NumI32->I32|NumI64->I64|NumF32->F32|NumF64->F64)(mem_callmmeth(addr::mem_extrawith_locmemargnat)))|LoadS(m,memarg,result_ty,size,signage)->let*addr=Stack.popinletmeth,nat=matchsizewith|`I8->("load8",1)|`I16->("load16",2)|`I32->("load32",4)inletcall=mem_callmmeth(addr::mem_extrawith_locmemargnat)in(* The operand is [Contextual]: in the fused spelling ([m.load8(x) as
i64_s] = [i64.load8_s]) the cast is part of the load's own surface —
the record for the whole sits on the cast node below. *)letcasttype=with_loc(Ast.Cast(contextuale,Signedtype{typ;signage;strict=false}))inletresult=match(size,result_ty)with|_,`I32->cast`I32call(* A genuinely fused i64 narrow load ([i64.load8_s]) is a single cast
[m.load8(x) as i64_s], which [to_wasm]'s single-cast arm re-fuses to
the same instruction. Only a *pair* ([i32.load8_s ; i64.extend_i32_s])
decompiles as the two casts [(m.load8(x) as i32_s) as i64_s], which
re-lowers to that honest pair (the two spellings are distinct now that
[to_wasm] no longer fuses the double cast); so both round-trip
opcode-for-opcode under [--faithful], and the default path coalesces
the pair's two casts to the single-cast spelling via [simplify]. *)|(`I8|`I16|`I32),`I64->cast`I64callin(* As for the plain load: the cast states the result type, so record it. *)Stack.push1(expect(matchresult_tywith`I32->I32|`I64->I64)result)|Store(m,memarg,nt)->let*value=Stack.popinlet*addr=Stack.popinletmeth,nat=matchntwith|NumI32->("store32",4)|NumI64->("store64",8)|NumF32->("storef32",4)|NumF64->("storef64",8)inStack.push0(mem_callmmeth(addr::value::mem_extrawith_locmemargnat))|StoreS(m,memarg,result_ty,size)->let*value=Stack.popinlet*addr=Stack.popin(* A narrow store ([store8/16/32]) picks its i32/i64 type from the value
operand's type ([To_wasm]), which is the one place a Wax surface names no
type for its operand at all — [m.store16] is both the i32 and the i64
form. Record the store's own type on the value, and the typer states it in
the printed form wherever the value would not carry it: a width-flexible
expression that would re-default to i32, or (the [Unknown]-cell case) a
hole on the polymorphic dead-code stack, which the lowering reads as i32.
See {!Wax_lang.Typing.f}'s [~width_check]. *)letvalue=Stack.expect_width(Some(result_ty:>[`I32|`I64|`F32|`F64]))valueinletmeth,nat=matchsizewith|`I8->("store8",1)|`I16->("store16",2)|`I32->("store32",4)inStack.push0(mem_callmmeth(addr::value::mem_extrawith_locmemargnat))|Atomic(m,op,memarg)->(letoperands,results=Atomics.signatureopinlet*ops=Stack.grab(List.lengthoperands)inlet*addr=Stack.popinletnat=1lslAtomics.natural_align_log2opin(* A narrow store/RMW ([store8/16/32], [rmw*8/16/32]) picks its i32/i64 type
from the value operand's type on re-parse ([To_wasm.atomic_op]): its method
name carries only the access width, which is ambiguous ([atomic_store16] is
both [i32.atomic.store16] and [i64.atomic.store16]). A width-flexible i64
value operand (an [i64.const], a hole on the dead-code stack) re-defaults
to i32 and narrows the op, so pin it to its signature type, exactly as the
plain narrow store [StoreS] above (a redundant i32/already-i64 pin is
dropped by [simplify]; [to_wasm] re-fuses). The full-width forms
([store64]/[rmw.…]/[store]) carry an unambiguous name and need no pin. *)letnarrow=matchopwith|AtomicStore(_,Some_)|AtomicRmw(_,_,Some_)->true|_->falseinletops=ifnarrowthenList.map2(funet->Stack.expect_width(Some(t:>[`I32|`I64|`F32|`F64]))e)opsoperandselseopsinletcall=mem_callm(Atomics.method_name(Atomics.familyop))((addr::ops)@mem_extrawith_locmemargnat)in(* The method name carries the access width only; a narrow load resolves
its i32/i64 type with a trailing [as iN_u] cast, following the plain
narrow-load decompile above: a fused i64 form ([i64.atomic.load8_u]) is
the single cast [m.atomic_load8(x) as i64_u] (re-fused by [to_wasm]),
only a genuine pair ([i32.atomic.load8_u ; i64.extend_i32_u]) is two
casts. Stores and RMWs re-resolve from their value operand's type. *)letresult=matchopwith|AtomicLoad(t,Somew)->((* As for [LoadS]: in the fused spelling the cast is part of the
load's surface, so the operand is [Contextual] — the record for
the whole lands on the cast node ([push_num] below). *)letcasttype=with_loc(Ast.Cast(contextuale,Signedtype{typ;signage=Unsigned;strict=false}))inmatch(w,t)with|_,`I32->cast`I32call|(`I8|`I16|`I32),`I64->cast`I64call)|_->callin(* An atomic load / RMW / notify / wait produces a single numeric ([i32]/
[i64]) result; tag it with that width (like every arithmetic result) so a
downstream width eraser pins it, and so a dead leftover of it is recognised
as numeric — a value a [ref.is_null]/[ref.eq] can never take, hence not a
backing (see [effective_backing]). A store has no result. *)matchresultswith|[t]->Stack.push_num(Some(t:>[`I32|`I64|`F32|`F64]))result|_->Stack.push(List.lengthresults)result)|AtomicFence->Stack.push0(path_call"atomic""fence"[])|Charc->Stack.push1(expectI32(with_loc(Charc)))|String(t,s)->lets=Wax_utils.Ast.concat_descsinStack.push1(with_loc(String(Option.map(idxctx`Type)t,s)))|If_annotation{cond;then_body;else_body}->(* Each branch body runs on a fresh stack, and the annotation declares NO
results: a value a branch leaves is an ENCLOSING-frame value per
configuration (the spliced validation hands it to whatever consumer
follows the annotation), so only its own printed form carries its
width — [~results:0] keeps the leftover's recorded expectation, where
the default would clear it as a context-typed block result and a
branch-pushed [i64.const 1] re-lowered at the i32 default (a
backing-scan ScondPush grid finding: the lowered module failed its own
validation in the configuration that feeds the value to an i64
consumer).
The typer types the branch its configuration plan selects SPLICED
against the enclosing pending stack ([Typing]'s [toplevel_instruction]
arm); mirror that world here so the scan predicts its claims: the
selection is read off the same plan ([ctx.plan]), the selected
branch's printed statements' hole count is recorded as the
annotation's claims (charged when a scan walks past the entry,
absorbed by the value entries below — positionally, as the typer pairs
them), and each of its leftover VALUES is pushed above the annotation
as a GHOST entry (arity [-2]: already printed inside the branch, so
never flushed or folded, but claim-absorbing, reconnection-backing,
and classifiable). *)letsel_then=Wax_wasm.Cond_plan.select_ownedctx.plani.infoinletconvertpositive(body:_list)=with_condctx~location:i.infocondpositive(fun()->letst,()=instructionsctxbody[]in(Stack.run_stack~results:0st,st))inletthen_stmts,then_st=converttruethen_body.descinletthen_body={then_bodywithAst.desc=then_stmts}inletelse_body,else_st=matchelse_bodywith|Some(b:(_Src.instrlist,Ast.location)Ast.Annot.annotated)->letstmts,st=convertfalseb.descin(Some{bwithAst.desc=stmts},st)|None->(None,[])inletsel_stmts,sel_st=ifsel_thenthen(then_body.Ast.desc,then_st)else((matchelse_bodywithSomeb->b.Ast.desc|None->[]),ifsel_thenthenthen_stelseelse_st)inletnode=with_loc(If_annotation{cond;then_body;else_body})inStack.set_annotation_claimsnode(List.fold_left(funns->n+Stack.hole_claimss)0sel_stmts);let*()=Stack.push0nodein(* The selected branch's leftover values, bottom-most first so the stack
order matches the branch's own. *)letghosts=List.rev(List.filter_map(fun(a,w,t)->ifa>=1thenSome(w,t)elseNone)sel_st)infunst->(List.fold_left(funst(w,t)->(-2,w,t)::st)stghosts,())(* [size]/[grow] return the memory's ADDRESS type (i64 under memory64); record
it (see [ctx.address_types]). *)|MemorySizem->Stack.push1(expect_address_typectx(idxctx`Memm)(mem_callm"size"[]))|MemoryGrowm->let*d=Stack.popinStack.push1(expect_address_typectx(idxctx`Memm)(mem_callm"grow"[d]))|MemoryFillm->let*n=Stack.popinlet*v=Stack.popinlet*d=Stack.popinStack.push0(mem_callm"fill"[d;v;n])|MemoryCopy(m,m')->let*n=Stack.popinlet*s=Stack.popinlet*d=Stack.popin(* A copy between two different memories names the source explicitly. *)letargs=if(idxctx`Memm).desc=(idxctx`Memm').descthen[d;s;n]elsewith_loc(Ast.Get(idxctx`Memm'))::[d;s;n]inStack.push0(mem_callm"copy"args)|MemoryInit(m,data)->let*n=Stack.popinlet*s=Stack.popinlet*d=Stack.popinletseg=with_loc(Ast.Get(idxctx`Datadata))inStack.push0(mem_callm"init"[seg;d;s;n])|DataDropdata->Stack.push0(drop_call`Datadata)(* As for a memory: a table's [size]/[grow] return its address type. *)|TableSizet->Stack.push1(expect_address_typectx(idxctx`Tablet)(table_callt"size"[]))|TableGrowt->let*n=Stack.popinlet*v=Stack.popinStack.push1(expect_address_typectx(idxctx`Tablet)(table_callt"grow"[v;n]))|TableFillt->let*n=Stack.popinlet*v=Stack.popinlet*d=Stack.popinStack.push0(table_callt"fill"[d;v;n])|TableCopy(t,t')->let*n=Stack.popinlet*s=Stack.popinlet*d=Stack.popinletargs=if(idxctx`Tablet).desc=(idxctx`Tablet').descthen[d;s;n]elsewith_loc(Ast.Get(idxctx`Tablet'))::[d;s;n]inStack.push0(table_callt"copy"args)|TableInit(t,elem)->let*n=Stack.popinlet*s=Stack.popinlet*d=Stack.popinletseg=with_loc(Ast.Get(idxctx`Elemelem))inStack.push0(table_callt"init"[seg;d;s;n])|ElemDropelem->Stack.push0(drop_call`Elemelem)|ArrayInitData(t,data)->let*n=Stack.popinlet*s=Stack.popinlet*d=Stack.popinlet*a=Stack.popinlet*a=pin_receiverctx(idxctx`Typet)~siblings:[d;s;n]ainletseg=with_loc(Ast.Get(idxctx`Datadata))inStack.push0(with_loc(Call(with_loc(StructGet(a,Ast.no_loc"init")),[seg;d;s;n])))|ArrayInitElem(t,elem)->let*n=Stack.popinlet*s=Stack.popinlet*d=Stack.popinlet*a=Stack.popinlet*a=pin_receiverctx(idxctx`Typet)~siblings:[d;s;n]ainletseg=with_loc(Ast.Get(idxctx`Elemelem))inStack.push0(with_loc(Call(with_loc(StructGet(a,Ast.no_loc"init")),[seg;d;s;n])))(* Every SIMD result is recorded, so a dead residual of one is known not to be a
reference (see {!recorded_expectation}): a vector op produces [v128], the
tests and bitmasks an [i32], and a lane extraction its shape's scalar. *)|VecUnOpop->let*v=Stack.popinStack.push1(expectV128(meth_callv(Simd.unop_nameop)[]))|VecBinOpop->let*e2=Stack.popinlet*e1=Stack.popinStack.push1(expectV128(meth_calle1(Simd.binop_nameop)[e2]))|VecTernOpop->let*e3=Stack.popinlet*e2=Stack.popinlet*e1=Stack.popinStack.push1(expectV128(meth_calle1(Simd.ternop_nameop)[e2;e3]))|VecShiftop->let*count=Stack.popinlet*v=Stack.popinStack.push1(expectV128(meth_callv(Simd.shift_nameop)[count]))|VecTestop->let*v=Stack.popin(* [any_true]/[all_true] yield an i32. *)Stack.push1(expectI32(meth_callv(Simd.test_nameop)[]))|VecBitmaskop->let*v=Stack.popinStack.push1(expectI32(meth_callv(Simd.bitmask_nameop)[]))|VecSplats->let*x=Stack.popinStack.push1(expectV128(meth_callx(Simd.splat_names)[]))|VecBitselect->let*e3=Stack.popinlet*e2=Stack.popinlet*e1=Stack.popinStack.push1(expectV128(path_callSimd.free_namespace(Simd.free_memberSimd.bitselect_name)[e1;e2;e3]))|VecExtract(s,sign,lane)->let*v=Stack.popinStack.push1(expect(lane_valtypes)(meth_callv(Simd.extract_namessign)[contextual(integeri.Src.info(Int.to_stringlane))]))|VecReplace(s,lane)->let*value=Stack.popinlet*v=Stack.popinStack.push1(expectV128(meth_callv(Simd.replace_names)[contextual(integeri.Src.info(Int.to_stringlane));value]))|VecShufflelanes->let*e2=Stack.popinlet*e1=Stack.popinletimms=List.init16(funk->contextual(integeri.Src.info(Int.to_string(Char.codelanes.[k]))))inStack.push1(expectV128(meth_calle1Simd.shuffle_name(imms@[e2])))|VecConstv->letlit=matchv.Wax_utils.V128.shapewith|F32x4|F64x2->floati|I8x16|I16x8|I32x4|I64x2->integeri.Src.infoinStack.push1(expectV128(path_callSimd.free_namespace(Simd.free_member(Simd.const_namev.shape))(List.map(func->contextual(litc))v.components)))|VecLoad(m,op,memarg)->let*addr=Stack.popinletnat=Simd.vec_load_nat_alignopin(* The loads were the gap in "every SIMD result is recorded" (found by the
first [--debug width-record] census run): a dead residual of one read as
a reference backing in {!Stack.effective_backing}, exactly the class the
[v128] record exists for. *)Stack.push1(expectV128(mem_callm(Simd.vec_load_nameop)(addr::mem_extrawith_locmemargnat)))|VecStore(m,memarg)->let*value=Stack.popinlet*addr=Stack.popinStack.push0(mem_callmSimd.store_name(addr::value::mem_extrawith_locmemarg16))|VecLoadSplat(m,w,memarg)->let*addr=Stack.popinletnat=Simd.lane_nat_alignwinStack.push1(expectV128(mem_callm(Simd.load_splat_namew)(addr::mem_extrawith_locmemargnat)))|VecLoadLane(m,w,memarg,lane)->let*v=Stack.popinlet*addr=Stack.popinletnat=Simd.lane_nat_alignwinStack.push1(expectV128(mem_callm(Simd.load_lane_namew)(addr::v::labelledwith_loc"lane"(integeri.Src.info(Int.to_stringlane))::mem_extrawith_locmemargnat)))|VecStoreLane(m,w,memarg,lane)->let*v=Stack.popinlet*addr=Stack.popinletnat=Simd.lane_nat_alignwinStack.push0(mem_callm(Simd.store_lane_namew)(addr::v::labelledwith_loc"lane"(integeri.Src.info(Int.to_stringlane))::mem_extrawith_locmemargnat))andinstructionsctxl=matchlwith|[]->return()|i::rem->let*()=instructionctxiininstructionsctxrem(*** Module-field conversion ***)letbind_localsstl=List.map(fune->let_,t=e.Wax_utils.Ast.descinletname=Sequence.get_currentst.localsinlett=valtypesttinHashtbl.replacest.local_valtypesname.Ast.desct;Ast.no_loc_instr(Ast.Let([(Somename,Somet)],None)))llettypeusectx((typ,sign):Src.typeuse)=letsignature({params;results}:Src.functype):Ast.functype={params=functype_paramsctxparams;results=Array.map(funt->valtypectxt)results;}inmatchOption.bindtyp(implicit_functypectx)with|Someft->(* The reference points at an anonymous implicit type; there is no named
type to refer to, so render it inline. *)(None,Some(signature(matchsignwithSomes->s|None->ft)))|None->(Option.map(funi->idxctx`Typei)typ,Option.mapsignaturesign)letstring_of_name(nm:Src.name):Ast.locationAst.instr={desc=Ast.String(None,nm.Wax_utils.Ast.desc);info=nm.Wax_utils.Ast.info;hints=Wax_wasm.Hints.none;expected=Unset;}(* Reserve, in a function's fresh local namespace, the Wax names of the
module-level entities its body references by a bare identifier: globals (via
[global.get]/[global.set]), functions (via [call]/[return_call]/[ref.func]),
the memories/tables a memory/table access names as its receiver
([mem.load(..)], [tab[..]], [tab.size()], …), and the data/element segments
named by [seg.drop()] / [mem.init] / [tab.init] / array segment ops. Without
this an auto-named local could be assigned a colliding name and shadow the
reference, since Wax resolves a bare name to a local before anything else. *)letrecreserve_module_names_in_instrctxns(i:_Src.instr)=matchi.descwith|Block{block;_}|Loop{block;_}|TryTable{block;_}->reserve_module_names_in_instrsctxnsblock.desc|If{if_block;else_block;_}->reserve_module_names_in_instrsctxnsif_block.desc;reserve_module_names_in_instrsctxnselse_block.desc|Try{block;catches;catch_all;_}->reserve_module_names_in_instrsctxnsblock.desc;List.iter(fun(_,block)->reserve_module_names_in_instrsctxnsblock.Wax_utils.Ast.desc)catches;Option.iter(funblock->reserve_module_names_in_instrsctxnsblock.Wax_utils.Ast.desc)catch_all|If_annotation{then_body;else_body;_}->reserve_module_names_in_instrsctxnsthen_body.desc;Option.iter(funb->reserve_module_names_in_instrsctxnsb.Wax_utils.Ast.desc)else_body|Folded(i,l)->reserve_module_names_in_instrsctxnsl;reserve_module_names_in_instrctxnsi|GlobalGetx|GlobalSetx->Namespace.reservens(idxctx`Globalx).desc|Callf|ReturnCallf|RefFuncf->Namespace.reservens(idxctx`Funcf).desc|Load(m,_,_)|LoadS(m,_,_,_,_)|Store(m,_,_)|StoreS(m,_,_,_)|Atomic(m,_,_)|MemorySizem|MemoryGrowm|MemoryFillm|VecLoad(m,_,_)|VecStore(m,_)|VecLoadSplat(m,_,_)|VecLoadLane(m,_,_,_)|VecStoreLane(m,_,_,_)->Namespace.reservens(idxctx`Memm).desc|MemoryCopy(m,m')->Namespace.reservens(idxctx`Memm).desc;Namespace.reservens(idxctx`Memm').desc|MemoryInit(m,d)->Namespace.reservens(idxctx`Memm).desc;Namespace.reservens(idxctx`Datad).desc|TableGett|TableSett|TableSizet|TableGrowt|TableFillt|CallIndirect(t,_)|ReturnCallIndirect(t,_)->Namespace.reservens(idxctx`Tablet).desc|TableCopy(t,t')->Namespace.reservens(idxctx`Tablet).desc;Namespace.reservens(idxctx`Tablet').desc|TableInit(t,e)->Namespace.reservens(idxctx`Tablet).desc;Namespace.reservens(idxctx`Eleme).desc|DataDropd|ArrayNewData(_,d)|ArrayInitData(_,d)->Namespace.reservens(idxctx`Datad).desc|ElemDrope|ArrayNewElem(_,e)|ArrayInitElem(_,e)->Namespace.reservens(idxctx`Eleme).desc|_->()andreserve_module_names_in_instrsctxnsl=List.iter(reserve_module_names_in_instrctxns)l(* Collect the Wax names of element segments referenced by table.init /
elem.drop / array.new_elem / array.init_elem, so a declarative segment used
this way is emitted explicitly rather than dropped. *)letreccollect_elem_refsctxacc(i:_Src.instr)=matchi.descwith|Block{block;_}|Loop{block;_}|TryTable{block;_}->collect_elem_refs_instrsctxaccblock.desc|If{if_block;else_block;_}->collect_elem_refs_instrsctxaccif_block.desc;collect_elem_refs_instrsctxaccelse_block.desc|If_annotation{then_body;else_body;_}->collect_elem_refs_instrsctxaccthen_body.desc;Option.iter(funb->collect_elem_refs_instrsctxaccb.Wax_utils.Ast.desc)else_body|Try{block;catches;catch_all;_}->collect_elem_refs_instrsctxaccblock.desc;List.iter(fun(_,b)->collect_elem_refs_instrsctxaccb.Wax_utils.Ast.desc)catches;Option.iter(funb->collect_elem_refs_instrsctxaccb.Wax_utils.Ast.desc)catch_all|Folded(i,l)->collect_elem_refs_instrsctxaccl;collect_elem_refsctxacci|TableInit(_,e)|ElemDrope|ArrayNewElem(_,e)|ArrayInitElem(_,e)->(tryHashtbl.replaceacc(idxctx`Eleme).desc()with_->())|_->()andcollect_elem_refs_instrsctxaccl=List.iter(collect_elem_refsctxacc)l(* Collect the wasm indices of locals referenced by a function body. A parameter
that is both unnamed in the source and absent here needs no Wax name: it can
be rendered anonymously instead of inventing one. Only numeric references
matter, since an unnamed parameter has no [$id] to be referenced by. *)letreccollect_local_refsacc(i:_Src.instr)=matchi.descwith|Block{block;_}|Loop{block;_}|TryTable{block;_}->collect_local_refs_instrsaccblock.desc|If{if_block;else_block;_}->collect_local_refs_instrsaccif_block.desc;collect_local_refs_instrsaccelse_block.desc|If_annotation{then_body;else_body;_}->collect_local_refs_instrsaccthen_body.desc;Option.iter(funb->collect_local_refs_instrsaccb.Wax_utils.Ast.desc)else_body|Try{block;catches;catch_all;_}->collect_local_refs_instrsaccblock.desc;List.iter(fun(_,b)->collect_local_refs_instrsaccb.Wax_utils.Ast.desc)catches;Option.iter(funb->collect_local_refs_instrsaccb.Wax_utils.Ast.desc)catch_all|Folded(i,l)->collect_local_refs_instrsaccl;collect_local_refsacci|LocalGetx|LocalSetx|LocalTeex->(matchx.Ast.descwithNumn->Hashtbl.replaceaccn()|Id_->())|_->()andcollect_local_refs_instrsaccl=List.iter(collect_local_refsacc)l(* The guard printed on a folded attribute (an [export]/[start] moved onto a
definition) is its branch condition with the conjuncts already entailed by
the target's own position ([ctx.cond_asm]) dropped: the target is emitted
inside those enclosing conditionals, so repeating them would be redundant
(and, worse, would re-accumulate on every round-trip). [location] anchors the
condition, which has no source of its own in the binary. *)letsimplify_guardctx~location(syn:Wax_wasm.Ast.cond):(Wax_wasm.Ast.cond,Ast.location)Ast.annotated=letrecconjuncts(c:Wax_wasm.Ast.cond)=matchcwithCond_andl->List.concat_mapconjunctsl|c->[c]inletkept=List.filter(func->not(Cond.logical_impliesctx.cond_asm(Cond.of_condctx.cond_envctx.cond_diag~locationc)))(conjunctssyn)in{Ast.desc=(matchkeptwith[]->syn|[c]->c|l->Cond_andl);info=location;}(* Fold the branch conditions [entries] of some attribute that a [(…)] field
attaches to a definition (an export, a start) into attribute guards on a
target at [ctx.cond_asm]: drop the attribute where its branch is unreachable,
keep it plain where the target's position already entails the branch, and
otherwise guard it with the branch condition simplified against the position.
[make guard nm] builds the attribute for one entry, [guard] being [None] for
a plain attribute. *)(* A synthesized attribute: one the conversion invents rather than reads, so it
has no source span of its own and takes the entity's. *)letsynth_attr~locationattr_nameattr_valueattr_guard:Ast.attribute={attr_name;attr_value;attr_guard;attr_span=location}letfolded_attrsctx~locationentriesmake=List.filter_map(fun(c,syn,nm)->ifnot(Cond.is_satisfiable(Cond.and_ctx.cond_asmc))thenNoneelseifCond.logical_impliesctx.cond_asmcthenSome(makeNonenm)elseSome(make(Some(simplify_guardctx~locationsyn))nm))entriesletexportsctxkindnamee:Ast.attributes=(* Reuse the bare [#[export]] short form when the export name matches the
field's own Wax name; only a differing name needs to be spelled out.
[guard] makes just this export conditional. *)letattrguard(nm:Src.name)=letvalue=ifnm.Wax_utils.Ast.desc=(name:Src.name).Wax_utils.Ast.descthenNoneelseSome(string_of_namenm)insynth_attr~location:name.Ast.info"export"valueguardin(* [e] are the inline exports declared on this field (already in the right
branch), so they inherit the field's reachability unconditionally. *)letinline=List.map(funnm->attrNonenm)ein(* The table holds standalone exports; each is kept only when its branch is
reachable here, plain or guarded per [folded_attrs]. *)letstandalone=matchHashtbl.find_optctx.exports(kind,name.Ast.desc)with|None->[]|Someentries->folded_attrsctx~location:name.Ast.infoentriesattrin(* When a field carries several exports, put the unnamed [#[export]] (the one
reusing the field's own Wax name) first; [partition] is stable, so the rest
keep their order. *)letunnamed,named=List.partition(fun(a:Ast.attribute)->Option.is_nonea.attr_value)(inline@standalone)inunnamed@named(* The [#[start]] attribute(s) on function [name]: a [(start …)] whose branch is
reachable here, plain or guarded like a standalone export. *)letstart_attributectxname:Ast.attributes=matchHashtbl.find_optctx.startsname.Wax_utils.Ast.descwith|None->[]|Someentries->folded_attrsctx~location:name.Ast.info(List.map(fun(c,syn)->(c,syn,()))entries)(funguard()->synth_attr~location:name.Wax_utils.Ast.info"start"Noneguard)(* Compilation-hints proposal: the function's [metadata.code.compilation_priority]
entry, back as the attributes it is written with. [#[priority]] always comes
first, since it is what makes the entry exist; the reserved optimization value
prints as [#[run_once]] rather than the number. *)letpriority_attributes~location(p:Wax_wasm.Hints.priorityoption):Ast.attributes=matchpwith|None->[]|Some{compilation;optimization}->letnumn=Some(Ast.no_loc_instr(Ast.Int(string_of_intn)))inletint_attrkn=synth_attr~locationk(numn)Noneinint_attr"priority"compilation::Option.to_list(Option.map(funo->ifo=Wax_wasm.Hints.run_oncethensynth_attr~location"run_once"NoneNoneelseint_attr"optimization"o)optimization)letsingle_expressionctx~locationl=matchlwith|[e]->e|_->conversion_errorctx~location(Wax_utils.Message.text"A constant expression must produce a single value.")letrecmodulefieldctxexport_tbl(f:(_Src.modulefield,_)Ast.annotated)=(* Sibling fields synthesised alongside [f] (e.g. an element segment for an
inline table initializer), emitted right after it. *)letextra=ref[]inletdesc:_Ast.modulefieldoption=matchf.descwith|Typest->Some(Type(collapse_splicesctx(rectypectxt)))|Import_group1_|Import_group2_->(* Wax has no compact-import concept: flatten the group into individual
imports (each converted as usual), which [group_imports] later
re-forms as a Wax [import "m" { … }] block. *)extra:=List.concat_map(modulefieldctxexport_tbl)(Wax_wasm.Ast_utils.expand_import_groupf);None|Func{locals;instrs;typ;exports=e;priority;_}->letlabel,labels=LabelStack.push~targeted:(label_targetedinstrs)(LabelStack.make())Noneinletctx=letreturn_arity=snd(typeuse_arityctxtyp)inletlocal_namespace=letns=Namespace.make()inreserve_module_names_in_instrsctxnsinstrs;nsin{ctxwithlocals=Sequence.make~diagnostics:ctx.diagnosticslocal_namespace"x";local_valtypes=Hashtbl.create16;labels;label_arities=[(None,return_arity)];block_params=[||];return_arity;}inletused_locals=letacc=Hashtbl.create16incollect_local_refs_instrsaccinstrs;accin(* Name a parameter, unless it is unnamed in the source and never
referenced by the body, in which case it is rendered anonymously. Its
index slot is still consumed so later locals stay correctly aligned.
[i] is the parameter's position, i.e. its wasm local index. *)letconvert_params~claimedparams=Array.mapi(funip->letid,t=p.Wax_utils.Ast.descinletpat=ifOption.is_noneid&¬(Hashtbl.memused_locals(Uint32.of_inti))then(Sequence.skipctx.locals;None)elseletname=Sequence.register'~claimedctx.localsexport_tblNoneid[]inSome(matchidwith|None->(* Unnamed in the source but referenced by the body, so
it cannot be rendered anonymously: warn that a name
was invented, pointing at the parameter. *)Wax_utils.Diagnostic.reportctx.diagnostics~location:p.Ast.info~severity:Warning~warning:Wax_utils.Warning.Generated_name~message:(Wax_utils.Message.text(Printf.sprintf"An unnamed parameter is used; generating \
the name '%s' for it."name))();Ast.no_locname|Someid->{idwithAst.desc=name})inlett=valtypectxtinOption.iter(fun(nm:Ast.ident)->Hashtbl.replacectx.local_valtypesnm.Ast.desct)pat;annotatedp.Ast.infopatt)paramsinletparam_arr,result_arr=matchtypwith|_,Some{params;results}->(params,results)|Somei,None->(letfunctype=matchimplicit_functypectxiwith|Someft->Someft|None->(match(lookup_typectxTypei).typwith|Funcft->Someft|Struct_|Array_|Cont_->None)inmatchfunctypewith|Some{params;results}->(params,results)|None->assertfalse)|None,None->assertfalse(* Should not happen *)in(* Priority pass: claim every source name (params, then locals) before
any unnamed entity is registered, so the generated default never
displaces a real source name (a user local [$x] keeps [x], the
unnamed one becomes [x_2], not the reverse). Renames are reported here
once; [register']/[register] then take the claimed name as-is. *)letclaimed=Hashtbl.create16inletclaimid=matchidwith|SomenmwhenLexer.is_valid_identifiernm.Wax_utils.Ast.desc&¬(Hashtbl.memclaimednm.Ast.desc)->Hashtbl.replaceclaimednm.Ast.desc(Sequence.claim_namectx.locals~loc:nm.Ast.infonm.Ast.desc)|_->()inArray.iter(funp->claim(fstp.Wax_utils.Ast.desc))param_arr;List.iter(fune->claim(fste.Wax_utils.Ast.desc))locals;letsign=letparams=convert_params~claimedparam_arrinSequence.consume_currentsctx.locals;{Ast.params;results=Array.map(funt->valtypectxt)result_arr;}in(* An anonymous implicit type has no name to reference; the inline [sign]
above already carries its signature, so drop the named reference. *)lettyp=matchfsttypwith|SomeiwhenOption.is_some(implicit_functypectxi)->None|t->Option.map(funi->idxctx`Typei)tinList.iter(fune->Sequence.register~claimedctx.localsexport_tblNone(fste.Wax_utils.Ast.desc)[])locals;letlocals=bind_localsctxlocalsinletname=Sequence.get_currentctx.functionsinSome(Func{name;typ;sign=Somesign;body=(label(),locals@Stack.run(instructionsctxinstrs));attributes=priority_attributes~location:name.Ast.infopriority@start_attributectxname@exportsctxFuncnamee;})|Import{module_;name=nm;desc;exports=e;_}->((* Build a single [import "module" <decl>;]. A name-only
[#[import = "name"]] is emitted only when the imported name differs
from the Wax name; consecutive same-module imports are grouped into
blocks in a later pass. *)letbuild?(start=[])idkindexport_kind=letattributes=start@(ifnm.Ast.desc=id.Wax_utils.Ast.descthen[]else[synth_attr~location:id.Wax_utils.Ast.info"import"(Some(string_of_namenm))None;])@exportsctxexport_kindideinSome(Ast.Import{module_;decl={Ast.desc={Ast.id;kind;attributes};info=f.info};})inmatchdescwith|Func{exact;typ}->lettyp,sign=typeusectxtypinletid=Sequence.get_currentctx.functionsin(* An imported function named by [(start …)] carries a [#[start]]
attribute, like a defined start function. *)build~start:(start_attributectxid)id(Import_func{typ;sign;exact})Func|Tagtyp->lettyp,sign=typeusectxtypinbuild(Sequence.get_currentctx.tags)(Import_tag{typ;sign})Tag|Globaltyp->lettyp'=globaltypectxtypinbuild(Sequence.get_currentctx.globals)(Import_global{mut=typ'.mut;typ=typ'.typ})Global|Memorylim->letl=lim.Ast.descinbuild(Sequence.get_currentctx.memories)(Import_memory{address_type=l.address_type;limits=Some(l.mi,l.ma);page_size_log2=l.page_size_log2;shared=l.shared;})Memory|Tablett->letl=tt.Src.limits.Ast.descinbuild(Sequence.get_currentctx.tables)(Import_table{address_type=l.address_type;reftype=reftypectxtt.Src.reftype;limits=Some(l.mi,l.ma);})Table)|Global{typ;init;exports=e;_}->lettyp'=globaltypectxtypinletname=Sequence.get_currentctx.globalsinSome(Global{name;mut=typ'.mut;typ=Sometyp'.typ;def=single_expressionctx~location:f.info(Stack.run(instructionsctxinit));attributes=exportsctxGlobalnamee;})|Tag{typ;exports=e;_}->lettyp,sign=typeusectxtypinletname=Sequence.get_currentctx.tagsinSome(Tag{name;typ;sign;attributes=exportsctxTagnamee})|Memory{limits=lim;init;exports=e;_}->letl=lim.Ast.descinletname=Sequence.get_currentctx.memoriesinletdata=matchinitwith|None->[]|Somebytes->[{Ast.data_name=None;offset=Ast.no_loc_instr(Ast.Int"0");init=data_init_to_waxctxbytes;};]inSome(Memory{name;address_type=l.address_type;limits=Some(l.mi,l.ma);page_size_log2=l.page_size_log2;shared=l.shared;data;attributes=exportsctxMemorynamee;})|Data{init;mode;_}->letname=Sequence.get_currentctx.datasinletinit=data_init_to_waxctxinitinletmode':_Ast.datamode=matchmodewith|Passive->Passive|Active(memidx,off)->Active(idxctx`Memmemidx,single_expressionctx~location:f.info(Stack.run(instructionsctxoff)))inSome(Data{name=Somename;mode=mode';init;attributes=[]})|Table{typ=tt;init;exports=e;_}->letname=Sequence.get_currentctx.tablesinletl=tt.Src.limits.Ast.descinletinit=matchinitwith|Init_default->None|Init_exprex->Some(single_expressionctx~location:f.info(Stack.run(instructionsctxex)))|Init_segmentsegs->(* A per-element initializer is not expressible on the table
itself; desugar it into a separate active element segment
filling the table from offset 0. *)letelem_init=List.map(funex->single_expressionctx~location:f.info(Stack.run(instructionsctxex)))segsinletelem:_Ast.modulefield=Elem{name=Sequence.fresh_namectx.elems;reftype=reftypectxtt.Src.reftype;mode=EActive(name,Ast.no_loc_instr(Ast.Int"0"));init=elem_init;attributes=[];}inextra:=[{fwithdesc=elem}];NoneinSome(Table{name;address_type=l.address_type;reftype=reftypectxtt.Src.reftype;limits=Some(l.mi,l.ma);init;attributes=exportsctxTablenamee;})|Elem{typ;init;mode;_}->((* Declare elems are regenerated by [to_wasm] from [call_ref] usage, so
they are normally dropped. One referenced by table.init / elem.drop /
array.*_elem still needs a binding: emit it as an empty passive
segment, which is runtime-equivalent (a declarative segment is a
dropped passive one — table.init traps, elem.drop is a no-op). *)matchmodewith|Declare->letname=Sequence.get_currentctx.elemsinifHashtbl.memctx.referenced_elemsname.Ast.descthenSome(Elem{name;reftype=reftypectxtyp;mode=EPassive;init=[];attributes=[];})elseNone|Passive|Active_->letname=Sequence.get_currentctx.elemsinletinit=List.map(fune->single_expressionctx~location:f.info(Stack.run(instructionsctxe)))initinletmode':_Ast.elemmode=matchmodewith|Passive->EPassive|Active(tab,off)->EActive(idxctx`Tabletab,single_expressionctx~location:f.info(Stack.run(instructionsctxoff)))|Declare->assertfalseinSome(Elem{name;reftype=reftypectxtyp;mode=mode';init;attributes=[];}))|Start_|Export_->None(* A [(@feature "name")] annotation becomes a [#![feature = "name"]] inner
attribute. *)|Feature_annotationname->Some(Module_annotation[synth_attr~location:name.Wax_utils.Ast.info"feature"(Some(string_of_namename))None;])|String_global{typ;init;_}->letname=Sequence.get_currentctx.globalsinSome(Global{name;mut=false;typ=None;def={desc=String(Option.map(idxctx`Type)typ,Wax_utils.Ast.concat_descinit);info=f.Ast.info;hints=Wax_wasm.Hints.none;expected=Unset;};attributes=[];})|Module_if_annotation{cond;then_fields;else_fields}->(* Convert [then] before [else]: positional naming via [get_current]
must consume names in the same order [register_names] registered
them (then-branch first). A record literal would leave the field
evaluation order unspecified (OCaml evaluates right-to-left), which
would consume the names swapped and scramble them across branches.
[with_cond] sets the branch assumption so per-branch declarations
(e.g. an import with a branch-dependent signature) resolve correctly
in the branch's bodies. *)letthen_fields={then_fieldswithAst.desc=with_condctx~location:f.infocondtrue(fun()->List.concat_map(modulefieldctxexport_tbl)then_fields.desc);}inletelse_fields=Option.map(fun(e:((Ast.locationSrc.modulefield,Ast.location)Ast.Annot.annotatedlist,Ast.location)Ast.Annot.annotated)->{ewithAst.desc=with_condctx~location:f.infocondfalse(fun()->List.concat_map(modulefieldctxexport_tbl)e.desc);})else_fieldsin(* An [@else] emptied by pulling out its standalone exports carries no
fields, so drop it; a conditional left empty in both branches -- e.g.
one that held only a standalone [(export …)] now re-emitted as a
guard on its target -- is a no-op and is dropped entirely. *)letelse_fields=matchelse_fieldswithSomeewhene.Ast.desc=[]->None|e->einifthen_fields.Ast.desc=[]&&else_fields=NonethenNoneelseSome(Conditional{cond;then_fields;else_fields})inOption.to_list(Option.map(fundesc->{fwithdesc})desc)@!extra(*** Implicit type elaboration and name registration ***)letempty_functype:Src.functype={params=[||];results=[||]}(* A hashable key identifying a function type up to the structural equality the
WAT type-use abbreviation needs — parameter names and source locations
ignored — so [elaborate_implicit_types] can dedup inline signatures against
the known types with a hashtable set rather than an O(n) scan (which, run per
inline signature, was quadratic and re-resolved references on every compare).
Each parameter and result value type is keyed structurally, except a
[(type …)] reference, which is resolved to its canonical type name first: a
numeric [(type N)] and a symbolic [(type $s)] naming the *same* declared type
must key equal, or a duplicate inline signature would mint a spurious implicit
type and shift every later numeric type reference. A reference that does not
resolve to a declared name (e.g. one pointing at an implicit type, which
carries no name here) falls back to its raw index form. *)letfunctype_keyctx(ft:Src.functype)=letheaptype_key(h:Src.heaptype)=matchhwith|Typei->(matchSequence.getctx.typesiwith|{Ast.desc=name;_}->`Namedname|exception(Unresolved_reference_|Numeric_ref_in_conditional_)->`Rawi.Ast.desc)|h->`Otherhinletvaltype_key(v:Src.valtype)=matchvwith|Ref{nullable;typ}->`Ref(nullable,heaptype_keytyp)|v->`Scalarvin(Array.map(funp->valtype_key(sndp.Wax_utils.Ast.desc))ft.params,Array.mapvaltype_keyft.results)(* Populate [ctx.implicit_types] with the function types the WAT text format
synthesises from inline [(param)]/[(result)] signatures. Explicit type
definitions occupy the low indices in source order; each inline signature
then reuses the lowest-indexed identical type, or appends a new one at the
end of the index space. This mirrors the spec's elaboration so that a numeric
[(type N)] referring to such a type resolves to the right signature.
Only called for modules without conditional annotations (where numeric
references are allowed); there the index space is unambiguous. *)letelaborate_implicit_typesctxfields=letnext=ref0in(* Keys of the function types seen so far — explicit ones first, then minted
implicit ones — used to decide whether an inline signature duplicates one
already known (in which case it mints no fresh index). A set of keys, not a
list scanned with [functype_eq], keeps this linear. *)letseen:(_,unit)Hashtbl.t=Hashtbl.create64inletrecordft=Hashtbl.replaceseen(functype_keyctxft)()in(* Phase 1: explicit type definitions, in source order. *)List.iter(fun(field:(_Src.modulefield,_)Ast.annotated)->matchfield.descwith|Typesrectype->Array.iter(fune->(match(snde.Wax_utils.Ast.desc:Src.subtype).typwith|Funcft->recordft|Struct_|Array_|Cont_->());incrnext)rectype|_->())fields;(* Phase 2: every inline signature, in source order, appended after the
explicit types. *)letconsider((typ,sign):Src.typeuse)=matchtypwith|Some_->()(* references an existing type; mints nothing *)|None->letft=Option.valuesign~default:empty_functypeinletkey=functype_keyctxftinifnot(Hashtbl.memseenkey)then(Hashtbl.replacectx.implicit_types(Uint32.of_int!next)ft;Hashtbl.replaceseenkey();incrnext)inletblocktype=functionSome(Src.Typeusetu)->considertu|_->()inletrecinstr(i:_Src.instr)=matchi.descwith|CallIndirect(_,tu)|ReturnCallIndirect(_,tu)->considertu|Block{typ;block;_}|Loop{typ;block;_}->blocktypetyp;instrsblock.desc|If{typ;if_block;else_block;_}->blocktypetyp;instrsif_block.Ast.desc;instrselse_block.Ast.desc|TryTable{typ;block;_}->blocktypetyp;instrsblock.desc|Try{typ;block;catches;catch_all;_}->blocktypetyp;instrsblock.desc;List.iter(fun(_,b)->instrsb.Wax_utils.Ast.desc)catches;Option.iter(funb->instrsb.Wax_utils.Ast.desc)catch_all|If_annotation{then_body;else_body;_}->instrsthen_body.desc;Option.iter(funb->instrsb.Wax_utils.Ast.desc)else_body|Folded(i,l)->instri;instrsl|_->()andinstrsl=List.iterinstrlinList.iter(fun(field:(_Src.modulefield,_)Ast.annotated)->matchfield.descwith|Func{typ;instrs=body;_}->considertyp;instrsbody|Import{desc=Func{typ=tu;_};_}|Import{desc=Tagtu;_}->considertu|Tag{typ;_}->considertyp|Global{init;_}->instrsinit|Elem{init;_}->List.iterinstrsinit|Table{init=Init_expre;_}->instrse|Table{init=Init_segmentl;_}->List.iterinstrsl(* Groups are flattened below, so only their [Import] members reach here. *)|Import_group1_|Import_group2_|Types_|Import_|Memory_|Table_|Export_|Start_|Data_|String_global_|Feature_annotation_|Module_if_annotation_->())(List.concat_mapWax_wasm.Ast_utils.expand_import_groupfields)letregister_namesctxexport_tblfields=(* Both passes recurse into the branches of a conditional, in the same order
the converter visits them, so positional naming stays aligned. *)letrecpass1fields=List.iter(fun(field:(_Src.modulefield,_)Ast.annotated)->matchfield.descwith|Import{id;name;desc;exports;_}->((* Failing an explicit [$id] and an export name, borrow the imported
name as the Wax name (like an export name), so an imported
[malloc] is named [malloc] rather than the generic default. *)lethint=ifLexer.is_valid_identifiername.Ast.descthenSomename.Ast.descelseNoneinmatchdescwith|Func_->()|Memorylimits->(* Record the address type exactly as for a module-defined
memory: an import's [size]/[grow] results state it too (an
unrecorded one was a recording gap the [--debug width-record]
census found). *)record_address_typectx(Sequence.register'?hintctx.memoriesexport_tbl(Some(Memory:Src.exportable))idexports)limits.Ast.desc.address_type|Tabletyp->record_address_typectx(Sequence.register'?hintctx.tablesexport_tbl(SomeTable)idexports)typ.Src.limits.Ast.desc.address_type|Globaltyp->record_global_valtypectxtyp(Sequence.register'?hintctx.globalsexport_tbl(SomeGlobal)idexports)|Tagty->register_type?hintctxexport_tblTagidexportsty)|Typesrectype->Array.iter(fune->letid,ty=e.Wax_utils.Ast.descinletname=Sequence.register'ctx.typesexport_tblNoneid[]inCondTbl.addctx.type_defsctx.cond_asmnamety;match(ty:Src.subtype).typwith|Func_|Array_|Cont_->()|Structl->letseq=Sequence.make~diagnostics:ctx.diagnostics(Namespace.make())"f"in(* A struct subtype inherits its supertype's fields by
position, so an unnamed field can borrow the name the
parent gave that slot rather than the generic "f". *)letparent_fields=matchty.supertypewith|None->[||]|Somesup->(matchSequence.getctx.typessupwith|exception(Unresolved_reference_|Numeric_ref_in_conditional_)->[||]|{desc=parent;_}->(matchHashtbl.find_optctx.struct_fieldsparentwith|Some(_,names)->Array.of_listnames|None->[||]))inletfields=Array.mapi(funit->lethint=ifi<Array.lengthparent_fieldsthenSomeparent_fields.(i)elseNoneinSequence.register'?hintseqexport_tblNone(get_annott)[])linHashtbl.replacectx.struct_fieldsname(seq,Array.to_listfields))rectype|Global{id;exports;typ;_}->record_global_valtypectxtyp(Sequence.register'ctx.globalsexport_tbl(SomeGlobal)idexports)(* Groups are flattened below, so only their [Import] members reach here. *)|Func_|Export_|Start_|Import_group1_|Import_group2_|Feature_annotation_->()|Elem{id;_}->Sequence.registerctx.elemsexport_tblNoneid[]|Data{id;_}->Sequence.registerctx.datasexport_tblNoneid[]|Memory{id;exports;limits;_}->(* [register'] rather than [register]: the Wax name it returns is the
key the address type is remembered under. ([get_current] must not be
used here — it advances the conversion pass's positional cursor.) *)record_address_typectx(Sequence.register'ctx.memoriesexport_tbl(SomeMemory)idexports)limits.Ast.desc.address_type|Table{id;exports;typ;_}->record_address_typectx(Sequence.register'ctx.tablesexport_tbl(SomeTable)idexports)typ.Src.limits.Ast.desc.address_type|Tag{id;exports;typ;_}->register_typectxexport_tblTagidexportstyp|String_global{id;_}->Sequence.registerctx.globalsexport_tbl(SomeGlobal)(Someid)[]|Module_if_annotation{then_fields;else_fields;cond}->with_condctx~location:field.infocondtrue(fun()->pass1then_fields.desc);Option.iter(fune->with_condctx~location:field.infocondfalse(fun()->pass1e.Wax_utils.Ast.desc))else_fields)(List.concat_mapWax_wasm.Ast_utils.expand_import_groupfields)inletrecpass2fields=List.iter(fun(field:(_Src.modulefield,_)Ast.annotated)->matchfield.descwith|Import{id;name;desc;exports;_}->(matchdescwith|Func{typ;_}->lethint=ifLexer.is_valid_identifiername.Ast.descthenSomename.Ast.descelseNoneinregister_type?hintctxexport_tblFuncidexportstyp|Memory_|Table_|Global_|Tag_->())|Func{id;exports;typ;_}->register_typectxexport_tblFuncidexportstyp|Module_if_annotation{then_fields;else_fields;cond}->with_condctx~location:field.infocondtrue(fun()->pass2then_fields.desc);Option.iter(fune->with_condctx~location:field.infocondfalse(fun()->pass2e.Wax_utils.Ast.desc))else_fields|Types_|Global_|Export_|Start_|Elem_|Data_|Memory_|Table_|Tag_|String_global_|Import_group1_|Import_group2_|Feature_annotation_->())(List.concat_mapWax_wasm.Ast_utils.expand_import_groupfields)inpass1fields;pass2fieldsletcollect_exportscond_envdiagnosticsfields=lettbl=Hashtbl.create16inletlst=ref[]inletstart_lst=ref[]in(* Combine the accumulated branch conditions ([syn], a list of conjuncts, each
already negated for an [@else]) into one syntactic condition, kept alongside
the solved [asm] so a standalone export narrower than its target can be
re-emitted as a [#[export …, if <cond>]] guard. *)letcombinesyn:Wax_wasm.Ast.cond=matchsynwith[c]->c|l->Cond_andlin(* [asm]/[syn] are the branch assumption under which the fields being walked
appear (solved / syntactic), so each standalone export is recorded with the
condition that guards it. *)letrecgoasmsynfields=List.iter(fun(field:(_Src.modulefield,_)Ast.annotated)->matchfield.descwith|Export{name;kind;index}->(* Don't keep a meaningless location *)lst:=(kind,index,Ast.no_locname.desc,asm,combinesyn)::!lst;letk=(kind,index.Ast.desc)inHashtbl.replacetblk(name::(tryHashtbl.findtblkwithNot_found->[]))|Startindex->start_lst:=(index,asm,combinesyn)::!start_lst|Module_if_annotation{cond;then_fields;else_fields}->letc=Cond.of_condcond_envdiagnostics~location:field.infocondingo(Cond.and_asmc)(syn@[cond])then_fields.desc;Option.iter(fune->go(Cond.and_asm(Cond.not_c))(syn@[Cond_notcond])e.Wax_utils.Ast.desc)else_fields|_->())fieldsingoCond.true_[]fields;(tbl,!lst,!start_lst)(*** Module conversion ***)letrecmodule_has_conditionalfields=List.exists(fun(f:(_Src.modulefield,_)Ast.annotated)->matchf.descwith|Module_if_annotation{then_fields;else_fields;_}->module_has_conditionalthen_fields.desc||Option.fold~none:false~some:(fune->module_has_conditionale.Wax_utils.Ast.desc)else_fields||true|_->false)fields(* A compact import group ([Import_group1]/[Import_group2]) can hold several
memory/table imports, so expand it into individual imports before counting —
as every other module walk does. Miscounting leaves [forbid_numeric_memory]/
[forbid_numeric_table] off under conditionals, degrading a later numeric-
reference diagnostic. *)letreccount_memoriesfields=List.fold_left(funn(f:(_Src.modulefield,_)Ast.annotated)->matchf.descwith|Memory_|Import{desc=Memory_;_}->n+1|Module_if_annotation{then_fields;else_fields;_}->n+max(count_memoriesthen_fields.desc)(Option.fold~none:0~some:(fune->count_memoriese.Wax_utils.Ast.desc)else_fields)|_->n)0(List.concat_mapWax_wasm.Ast_utils.expand_import_groupfields)letreccount_tablesfields=List.fold_left(funn(f:(_Src.modulefield,_)Ast.annotated)->matchf.descwith|Table_|Import{desc=Table_;_}->n+1|Module_if_annotation{then_fields;else_fields;_}->n+max(count_tablesthen_fields.desc)(Option.fold~none:0~some:(fune->count_tablese.Wax_utils.Ast.desc)else_fields)|_->n)0(List.concat_mapWax_wasm.Ast_utils.expand_import_groupfields)(* The [type <name> = fn(..)] declarations for implicit function types that were
named because a ref-type referenced them ([type_ref_name]). Converting a
signature can itself name further implicit types (a nested ref-type use), so
drain [named_implicit] to a fixpoint; a dependency named while converting an
earlier one is emitted before it. *)letextra_type_declsctx=letrecloopacc=matchctx.named_implicitwith|[]->acc|pending->ctx.named_implicit<-[];letdecls=List.rev_map(fun(name,ft)->letname=Ast.no_locnameinletsub:Ast.subtype={typ=Func(functypectxft);supertype=None;final=true;descriptor=None;describes=None;}inAst.no_loc(Ast.Type[|annotatedname.Ast.infonamesub|]))pendinginloop(decls@acc)inloop[](* Merge maximal runs of consecutive single imports from the same module into
one [import "module" { ... }] block; a lone import stays as the standalone
[import "module" <decl>;] form. Recurses through groups and conditionals. *)letrecgroup_importsfields=letrecurse(f:(_Ast.modulefield,_)Ast.Annot.annotated)=matchf.descwith|Ast.Conditionalc->{fwithAst.desc=Ast.Conditional{cwiththen_fields={c.then_fieldswithAst.desc=group_importsc.then_fields.Ast.desc;};else_fields=Option.map(funb->{bwithAst.Annot.desc=group_importsb.Ast.Annot.desc})c.else_fields;};}|_->finletrecmergeacc=function|[]->List.revacc|(f:(_Ast.modulefield,_)Ast.Annot.annotated)::rest->(matchf.descwith|Ast.Import{module_;decl}->letrectakegroup_acc=function|(g:(_Ast.modulefield,_)Ast.Annot.annotated)::tlwhenmatchg.descwith|Ast.Import{module_=m2;_}->m2.Ast.desc=module_.desc|_->false->letd=matchg.Ast.descwith|Ast.Import{decl;_}->decl|_->assertfalseintake(d::group_acc)tl|tl->(List.revgroup_acc,tl)inletdecls,tl=take[decl]restinletfield=matchdeclswith|[_]->f|_->{fwithAst.desc=Ast.Import_group{module_;decls}}inmerge(field::acc)tl|_->merge(f::acc)rest)inmerge[](List.maprecursefields)letmodule_?(strict_constants=false)?(faithful=false)?featuresdiagnostics(module_name,fields)=Wax_utils.Debug.timed"convert"@@fun()->tryletconvertplan=letforbid_numeric=module_has_conditionalfieldsin(* Loads/stores reference the memory implicitly by index 0. When the module
has a single memory that numeric reference is unambiguous (even if the
memory itself sits in a conditional branch), so numeric memory references
are allowed; with several memories, indices may shift across branches like
any other field, so the general constraint stands. *)letforbid_numeric_memory=forbid_numeric&&count_memoriesfields>1inletforbid_numeric_table=forbid_numeric&&count_tablesfields>1inletctx=letcommon_namespace=Namespace.make()inletcond_diag=Wax_utils.Diagnostic.collector()in{diagnostics;types=Sequence.make~forbid_numeric~diagnostics(Namespace.make~kind:`Type())"t";struct_fields=Hashtbl.create16;globals=Sequence.make~forbid_numeric~diagnosticscommon_namespace"g";functions=Sequence.make~forbid_numeric~diagnosticscommon_namespace"f";memories=Sequence.make~forbid_numeric:forbid_numeric_memory~is_conditional:forbid_numeric~diagnosticscommon_namespace"m";tables=Sequence.make~forbid_numeric:forbid_numeric_table~is_conditional:forbid_numeric~diagnosticscommon_namespace"t";tags=Sequence.make~forbid_numeric~diagnostics(Namespace.make())"t";datas=Sequence.make~forbid_numeric~diagnosticscommon_namespace"d";elems=Sequence.make~forbid_numeric~diagnosticscommon_namespace"e";referenced_elems=Hashtbl.create16;type_defs=CondTbl.make();implicit_types=Hashtbl.create16;named_implicit=[];function_types=CondTbl.make();tag_types=CondTbl.make();exports=Hashtbl.create16;starts=Hashtbl.create16;locals=Sequence.make~diagnosticscommon_namespace"x";local_valtypes=Hashtbl.create16;global_valtypes=Hashtbl.create16;labels=LabelStack.make();label_arities=[];block_params=[||];return_arity=0;strict_constants;faithful;address_types=Hashtbl.create8;multi_ref_results=Hashtbl.create8;cond_env=Cond.create();plan;cond_diag;cond_asm=Cond.true_;}inletexport_tbl,export_lst,start_lst=collect_exportsctx.cond_envctx.cond_diagfieldsinregister_namesctxexport_tblfields;ifnotforbid_numericthenelaborate_implicit_typesctxfields;(* Resolve each [(start …)] to its function's Wax name, keeping the branch
condition it appeared under; rendered as a [#[start]] attribute on that
function (guarded when the start is narrower than the function). *)List.iter(fun(index,asm,syn)->letname=(idxctx`Funcindex).Ast.descinHashtbl.replacectx.startsname((asm,syn)::Option.value~default:[](Hashtbl.find_optctx.startsname)))start_lst;List.iter(fun(kind,index,name,asm,syn)->letk=(kind,(idxctx(match(kind:Src.exportable)with|Func->`Func|Memory->`Mem|Table->`Table|Tag->`Tag|Global->`Global)index).desc)inletl=(asm,syn,name)::(matchHashtbl.find_optctx.exportskwith|None->[]|Somel->l)inHashtbl.replacectx.exportskl)export_lst;(* Record which element segments are referenced by table.init / elem.drop /
array.*_elem (recursing into conditional branches), so a declarative
segment used this way is declared rather than dropped. *)letreccollect_field(f:(_Src.modulefield,_)Ast.annotated)=matchf.Ast.descwith|Func{instrs;_}->collect_elem_refs_instrsctxctx.referenced_elemsinstrs|Module_if_annotation{then_fields;else_fields;_}->List.itercollect_fieldthen_fields.desc;Option.iter(fune->List.itercollect_fielde.Wax_utils.Ast.desc)else_fields|_->()inList.itercollect_fieldfields;letconverted=List.concat_map(funf->modulefieldctxexport_tblf)fieldsin(* Prepend the type declarations synthesised for implicit types named by a
ref-type reference (computed after conversion, which is what names them). *)letconverted=extra_type_declsctx@convertedinletrecovered=Recover_match.module_~faithful(Sink_let.module_(Recover_loops.module_(Recover_trycatch.module_(Recover_dispatch.module_converted))))in(* A named module becomes a leading [#![module = "name"]] inner attribute. *)letname_annotation=matchmodule_namewith|Somenm->[Ast.no_loc(Ast.Module_annotation[synth_attr~location:nm.Wax_utils.Ast.info"module"(Some(string_of_namenm))None;]);]|None->[]in(* Stamp a [#![feature = "…"]] inner attribute for each gated feature the
module was seen to exercise ([Feature.used], recorded by the binary
decoder and by validation), so the output recompiles standalone. A
feature the module already declares with a [(@feature "…")] annotation
was converted above; do not stamp it twice. *)letfeature_annotations=matchfeatureswith|None->[]|Somefeatures->letdeclared=List.filter_map(fun(f:(_Src.modulefield,_)Ast.annotated)->matchf.descwith|Feature_annotationnm->Wax_utils.Feature.of_namenm.desc|_->None)fieldsinList.filter_map(funfeature->ifList.memfeaturedeclaredthenNoneelseSome(Ast.no_loc(Ast.Module_annotation[synth_attr~location:Wax_utils.Ast.dummy_loc"feature"(Some(Ast.no_loc_instr(Ast.String(None,Wax_utils.Feature.namefeature))))None;])))(Wax_utils.Feature.usedfeatures)inname_annotation@feature_annotations@group_importsrecoveredin(* The typer builds the plan for the emitted module from its own shape of
it, and so does every later re-parse of the printed Wax; the stack model
in [convert] read the plan built from the SOURCE's shape. The two differ
when the emission reshapes the field level (an [@else] emptied by pulling
out its standalone exports is dropped, a conditional left empty in both
branches too, imports are regrouped ahead of the definitions), and a
plan is a set of decisions reached in stream order, so a reshaped field
level can shift a body's decisions. Compare the decision at every emitted
conditional; on a disagreement convert again with the emitted shape's
plan — the emitted structure does not depend on the plan, so the second
conversion's own plan is that plan and the two agree. *)letplan_ofshape=Wax_wasm.Cond_plan.make(Wax_utils.Diagnostic.collector())shapeinletdecisions_agreepq(m:_Ast.module_)=letagree=reftrueinWax_lang.Ast_utils.iter_module_instr(fun(i:_Ast.instr)->matchi.descwith|If_annotation_->ifWax_wasm.Cond_plan.select_ownedpi.info<>Wax_wasm.Cond_plan.select_ownedqi.infothenagree:=false|_->())m;!agreeinletsource_shape=Wax_wasm.Cond_plan.text_shapefieldsinletsource_plan=plan_ofsource_shapeinletresult=convertsource_planinifsource_shape=[]thenresultelseletemitted_plan=plan_of(Wax_lang.Typing.plan_shape~guards:falseresult)inifdecisions_agreesource_planemitted_planresultthenresultelseletresult=convertemitted_planinletplan=plan_of(Wax_lang.Typing.plan_shape~guards:falseresult)inifdecisions_agreeemitted_planplanresultthenresultelsefailwith"From_wasm: the conditional plan did not converge"with|Numeric_ref_in_conditionallocation->Wax_utils.Diagnostic.reportdiagnostics~location~severity:Error~message:(Wax_utils.Message.text"Numeric references to module fields are not supported in a \
module with conditional annotations; use a symbolic $name.")();Wax_utils.Diagnostic.abort()|Unresolved_referencelocation->Wax_utils.Diagnostic.reportdiagnostics~location~severity:Error~message:(Wax_utils.Message.text"This reference resolves to nothing: it is out of range or names \
an undeclared entity.")();Wax_utils.Diagnostic.abort()