123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014openAstopenInferopenTyping_env(* The diagnostics the lint checks emit. Kept with their emitters (rather than in
the typer's [Error] module) since these warnings fire only from this pass; the
message-building combinators are the same trivial [Wax_utils.Message] aliases
the typer's [Error] uses. *)moduleError=structopenWax_utilslettext=Message.textlet(++)=Message.(++)let(^^)=Message.(^^)letwarn?warning?universal?hint?edit?relatedcontext~locationmessage=ifnot(Wax_utils.Diagnostic.in_recoverycontext)thenDiagnostic.reportcontext~location~severity:Warning?warning?universal?hint?edit?related~message()(* An operation with no effect on its result, or a constant result. *)letredundant_operationcontext~locationmessage=warn~warning:Wax_utils.Warning.Redundant_operation~universal:truecontext~locationmessage(* A shift whose constant count is at least the operand's bit width. Wasm
shifts mask the count modulo the width, so the result is very likely not
what was intended. *)(* [count] is the shift count as an unsigned 64-bit value (a hex literal such
as [0xffff_ffff_ffff_ffff] is a large positive count, not [-1]), so print
and reduce it unsigned. *)letshift_overflowcontext~location~widthcount=warn~warning:Wax_utils.Warning.Shift_overflow~universal:truecontext~location~hint:((text"Wasm masks the count modulo"++Message.intwidth)^^text","++text"shifting by"++Message.uint64(Int64.unsigned_remcount(Int64.of_intwidth))++text"instead.")(text"The shift count"++Message.uint64count++text"is at least the operand width ("^^Message.intwidth^^text" bits).")(* An integer division or remainder by a constant zero: it always traps. *)letdivision_by_zerocontext~location=warn~warning:Wax_utils.Warning.Constant_trap~universal:truecontext~location(text"This integer division or remainder by zero always traps.")(* A comparison whose result does not depend on its variable operand. *)lettautological_comparisoncontext~location~value=warn~warning:Wax_utils.Warning.Tautological_comparison~universal:truecontext~location((text"This comparison is always"++Message.boolvalue)^^text".")(* A branch, loop, or [select] condition that is a constant. *)letconstant_conditioncontext~location~value=warn~warning:Wax_utils.Warning.Constant_condition~universal:truecontext~location((text"This condition is always"++Message.boolvalue)^^text".")(* A side-effect-free expression whose result is computed and then dropped. *)letunused_resultcontext~location=warn~warning:Wax_utils.Warning.Unused_result~universal:truecontext~location(text"The result of this expression is discarded, and computing it has no \
effect.")(* A trapping float-to-integer conversion of a constant that lies outside the
target type's range (or is NaN/infinite): it always traps. *)letconversion_out_of_rangecontext~location=warn~warning:Wax_utils.Warning.Constant_trap~universal:truecontext~location(text"This conversion always traps: the constant is out of the target \
type's range.")(* A trapping or effectful operation inside a branch of a [?:]. Because [?:]
compiles to a [select], which evaluates both branches, the operation runs
even when the condition selects the other branch — unlike the [?:] of most
languages, which is lazy. [select] points at the whole [?:]. *)leteager_selectcontext~location~select=warn~warning:Wax_utils.Warning.Eager_select~universal:truecontext~location~related:[{Wax_utils.Diagnostic.location=select;message=text"This '?:' evaluates both branches (it compiles to a 'select').";};]~hint:(text"Use an 'if' expression to evaluate only the chosen branch.")(text"This operation is evaluated even when the condition selects the \
other branch.")(* Two operators whose relative precedence is easy to misremember are mixed
without parentheses (see {!lint_precedence}). [location] is the outer
operator, [inner] the tighter-binding one; the [kind]s name the two
operator classes ("shift", "arithmetic", "comparison", "bitwise"). *)letprecedence?editcontext~location~inner~outer_kind~inner_kind=warn?edit~warning:Wax_utils.Warning.Precedence~universal:truecontext~location~related:[{Wax_utils.Diagnostic.location=inner;message=text"This"++textinner_kind++text"operator binds tighter than the"++textouter_kind++text"operator.";};]~hint:(text"Add parentheses to make the grouping explicit.")(text"Operator precedence here is easy to misread.")endletis_pure_unary_method=function|"clz"|"ctz"|"popcnt"|"extend8_s"|"extend16_s"|"abs"|"ceil"|"floor"|"trunc"|"nearest"|"sqrt"|"to_bits"|"from_bits"->true|_->falseletis_pure_binary_method=function|"rotl"|"rotr"|"min"|"max"|"copysign"->true|_->false(* Whether a cast is total (never traps), so discarding its result is pointless.
A [strict] float-to-int conversion lowers to [trunc] (traps out of range); its
saturating form and every numeric widen/narrow/convert is total. A reference
cast ([as &T]) may trap and is conservatively treated as non-total (the
never-trapping [extern.convert_any] is spelled the same way but not
distinguished here). Mirrors the Wasm validator's [classify]. *)letcast_is_total=function|Ast.Signedtype{typ;strict;_}->(matchtypwith`F32|`F64->true|`I32|`I64->notstrict)|Valtype(I32|I64|F32|F64)->true|Valtype(V128|Ref_)|Functype_->false(* An ascription is a static assertion; nothing to trap. *)|Ascribed_->trueletrecis_effectless(e:_Ast.instr)=(* A field value; the punning shorthand [{x}] reads a local/global. *)letfield(_,v)=matchvwithSomee->is_effectlesse|None->trueinmatche.descwith|Get_|Int_|Float_|Char_|String_|Null|StructDefault_->true|UnOp(_,a)->is_effectlessa|Call({desc=StructGet(recv,m);_},[])whenis_pure_unary_methodm.desc->is_effectlessrecv|Call({desc=StructGet(recv,m);_},[a])whenis_pure_binary_methodm.desc->is_effectlessrecv&&is_effectlessa(* A SIMD vector method on a value ([v.add_i32x4(w)], [v.trunc_sat_f32x4_u()]):
every vector op is pure and non-trapping (the trapping SIMD accesses are the
[mem.]-path loads/stores, classified separately by [Simd.mem_method]). *)|Call({desc=StructGet(recv,m);_},args)whenWax_wasm.Simd.classifym.desc<>None->is_effectlessrecv&&List.for_allis_effectlessargs(* A [v128::…] SIMD constructor or vector op ([v128::i8x16(…)], a lane build)
is effect-free and non-trapping when its operands are — the trapping SIMD
memory accesses use the [mem.] path, not [v128::]. *)|Call({desc=Path({desc="v128";_},_);_},args)->List.for_allis_effectlessargs(* [memory.size] / [table.size] ([m.size()]) reads the current size: pure and
non-trapping, unlike the effectful grow/fill/copy/init on the same path. *)|Call({desc=StructGet({desc=Get_;_},{desc="size";_});_},[])->true(* A typed null [null as &?t] is [ref.null] (a constant), not a trapping ref
cast, so it is effect-free like a bare [null]. *)|Cast({desc=Null;_},Valtype(Ref{nullable=true;_}))->true|Cast(a,ct)whencast_is_totalct->is_effectlessa|BinOp({desc=Div_|Rem_;_},_,_)->false|BinOp(_,a,b)->is_effectlessa&&is_effectlessb|Select(a,b,c)->is_effectlessa&&is_effectlessb&&is_effectlessc|Test(a,_)->is_effectlessa|Struct(_,fields)->List.for_allfieldfields|StructDesc(d,fields)->is_effectlessd&&List.for_allfieldfields|StructDefaultDescd->is_effectlessd|Array(_,elt,len)->is_effectlesselt&&is_effectlesslen|ArrayDefault(_,len)->is_effectlesslen|ArrayFixed(_,elts)->List.for_allis_effectlesselts|_->false(* Accumulate into [acc] the local names assigned ([Set]/[Tee] targets) anywhere
in [i], recursing through every sub-instruction. Mirrors the case coverage of
{!Sink_let.occurs}: only [Set]/[Tee] write a local, every other case just
recurses. A drop ([_ = e], an anonymous [Let]) names no local, so it just
recurses via the [Let] case. Wasm-derived locals are uniquely named within a
function, so the
resulting by-name set is exact; a stray name collision could only keep an
annotation, never wrongly drop one. *)letreccollect_assigned_localsacci=matchi.descwith|Set(id,_,e)|Tee(id,e)->collect_assigned_locals(StringSet.addid.descacc)e|Block{block;_}|Loop{block;_}|TryTable{block;_}->collect_assigned_locals_listaccblock.desc|While{cond;step;block;_}->letacc=collect_assigned_localsacccondinletacc=Option.fold~none:acc~some:(collect_assigned_localsacc)stepincollect_assigned_locals_listaccblock.desc|If{cond;if_block;else_block;_}->letacc=collect_assigned_locals_list(collect_assigned_localsacccond)if_block.descinOption.fold~none:acc~some:(funb->collect_assigned_locals_listaccb.Annot.desc)else_block|Try{block;catches;catch_all;_}->letacc=collect_assigned_locals_listaccblock.descinletacc=List.fold_left(funacc(_,b)->collect_assigned_locals_listaccb.Annot.desc)acccatchesinOption.fold~none:acc~some:(funb->collect_assigned_locals_listaccb.Annot.desc)catch_all|TryCatch{block;arms;_}->letacc=collect_assigned_locals_listaccblock.descinList.fold_left(funacca->collect_assigned_locals_listacca.arm_body.desc)accarms|Call(t,args)|TailCall(t,args)->collect_assigned_locals_list(collect_assigned_localsacct)args|Cast(e,_)|Test(e,_)|NonNulle|StructGet(e,_)|GetDescriptore|StructDefaultDesce|UnOp(_,e)|Br_if(_,e)|On(e,_)|Labelled(_,e)|Br_table(_,e)|Br_on_null(_,e)|Br_on_non_null(_,e)|Br_on_cast(_,_,e)|Br_on_cast_fail(_,_,e)|ThrowRefe|ArrayDefault(_,e)|ContNew(_,e)->collect_assigned_localsacce(* A punned field ([None]) is a [Get] and assigns nothing. *)|Struct(_,fields)->List.fold_left(funacc(_,e)->Option.fold~none:acc~some:(collect_assigned_localsacc)e)accfields|StructDesc(d,fields)->List.fold_left(funacc(_,e)->Option.fold~none:acc~some:(collect_assigned_localsacc)e)(collect_assigned_localsaccd)fields|CastDesc(e1,_,e2)|Br_on_cast_desc_eq(_,_,e1,e2)|Br_on_cast_desc_eq_fail(_,_,e1,e2)|StructSet(e1,_,e2)|Array(_,e1,e2)|ArraySegment(_,_,e1,e2)|ArrayGet(e1,e2)|BinOp(_,e1,e2)->collect_assigned_locals(collect_assigned_localsacce1)e2|ArraySet(e1,e2,e3)|Select(e1,e2,e3)->collect_assigned_locals(collect_assigned_locals(collect_assigned_localsacce1)e2)e3|ArrayFixed(_,l)|ContBind(_,_,l)|Suspend(_,l)|Resume(_,_,l)|ResumeThrow(_,_,_,l)|ResumeThrowRef(_,_,l)|Switch(_,_,l)|Throw(_,l)|Sequencel->collect_assigned_locals_listaccl|Dispatch{index;arms;_}->List.fold_left(funacc(_,(b:(_instrlist,_)Ast.annotated))->collect_assigned_locals_listaccb.desc)(collect_assigned_localsaccindex)arms|Match{scrutinee;arms;default}->letacc=collect_assigned_localsaccscrutineeinletacc=List.fold_left(funacc(_,b)->collect_assigned_locals_listaccb.Annot.desc)accarmsincollect_assigned_locals_listaccdefault.desc|Let(_,body)->collect_assigned_locals_optaccbody|Br(_,o)|Returno->collect_assigned_locals_optacco|If_annotation{then_body;else_body;_}->letacc=collect_assigned_locals_listaccthen_body.descinOption.fold~none:acc~some:(funb->collect_assigned_locals_listaccb.Annot.desc)else_body|Get_|Path_|Unreachable|Nop|Hole|Null|Char_|String_|Int_|Float_|StructDefault_->acc(* Accumulate into [acc] the block labels declared anywhere in [i], from the
source AST (before any lowering, so synthesized labels from [while]/[dispatch]/
[match] desugaring are never collected). Every case recurses; the labelled
constructs also contribute their own label. The [dispatch]/[match] arm labels
are branch targets, not declarations, so they are not collected. Mirrors the
case coverage of {!collect_assigned_locals}. *)andcollect_assigned_locals_listaccl=List.fold_leftcollect_assigned_localsacclandcollect_assigned_locals_optacco=matchowithSomei->collect_assigned_localsacci|None->accletreccollect_labelsacc(i:_Ast.instr)=letaddacclabel=matchlabelwithSomel->l::acc|None->accinmatchi.descwith|Block{label;block;_}|Loop{label;block;_}|TryTable{label;block;_}->collect_labels_list(addacclabel)block.desc|While{label;cond;step;block;_}->letacc=collect_labels(addacclabel)condinletacc=Option.fold~none:acc~some:(collect_labelsacc)stepincollect_labels_listaccblock.desc|If{label;cond;if_block;else_block;_}->letacc=collect_labels_list(collect_labels(addacclabel)cond)if_block.descinOption.fold~none:acc~some:(funb->collect_labels_listaccb.Annot.desc)else_block|Try{label;block;catches;catch_all;_}->letacc=collect_labels_list(addacclabel)block.descinletacc=List.fold_left(funacc(_,b)->collect_labels_listaccb.Annot.desc)acccatchesinOption.fold~none:acc~some:(funb->collect_labels_listaccb.Annot.desc)catch_all|TryCatch{label;block;arms;_}->letacc=collect_labels_list(addacclabel)block.descinList.fold_left(funacca->collect_labels_listacca.arm_body.desc)accarms|Call(t,args)|TailCall(t,args)->collect_labels_list(collect_labelsacct)args|Set(_,_,e)|Tee(_,e)|Labelled(_,e)|Cast(e,_)|Test(e,_)|NonNulle|StructGet(e,_)|GetDescriptore|StructDefaultDesce|UnOp(_,e)|Br_if(_,e)|On(e,_)|Br_table(_,e)|Br_on_null(_,e)|Br_on_non_null(_,e)|Br_on_cast(_,_,e)|Br_on_cast_fail(_,_,e)|ThrowRefe|ArrayDefault(_,e)|ContNew(_,e)->collect_labelsacce|Struct(_,fields)->List.fold_left(funacc(_,e)->collect_labels_optacce)accfields|StructDesc(d,fields)->List.fold_left(funacc(_,e)->collect_labels_optacce)(collect_labelsaccd)fields|CastDesc(e1,_,e2)|Br_on_cast_desc_eq(_,_,e1,e2)|Br_on_cast_desc_eq_fail(_,_,e1,e2)|StructSet(e1,_,e2)|Array(_,e1,e2)|ArraySegment(_,_,e1,e2)|ArrayGet(e1,e2)|BinOp(_,e1,e2)->collect_labels(collect_labelsacce1)e2|ArraySet(e1,e2,e3)|Select(e1,e2,e3)->collect_labels(collect_labels(collect_labelsacce1)e2)e3|ArrayFixed(_,l)|ContBind(_,_,l)|Suspend(_,l)|Resume(_,_,l)|ResumeThrow(_,_,_,l)|ResumeThrowRef(_,_,l)|Switch(_,_,l)|Throw(_,l)|Sequencel->collect_labels_listaccl|Dispatch{index;arms;_}->List.fold_left(funacc(_,b)->collect_labels_listaccb.Annot.desc)(collect_labelsaccindex)arms|Match{scrutinee;arms;default}->letacc=collect_labelsaccscrutineeinletacc=List.fold_left(funacc(_,b)->collect_labels_listaccb.Annot.desc)accarmsincollect_labels_listaccdefault.desc|Let(_,body)->collect_labels_optaccbody|Br(_,o)|Returno->collect_labels_optacco|If_annotation{then_body;else_body;_}->letacc=collect_labels_listaccthen_body.descinOption.fold~none:acc~some:(funb->collect_labels_listaccb.Annot.desc)else_body|Get_|Path_|Unreachable|Nop|Hole|Null|Char_|String_|Int_|Float_|StructDefault_->acc(* The location of a trapping or effectful operation reached on the eagerly-
evaluated spine of a [?:] branch [e], or [None] if the branch only reads
locals/globals and computes pure arithmetic. Descends through pure operators
(into the operands that are always evaluated) but stops at any nested control
construct (an inner [if], [?:], block, loop, …): the sub-expressions guarded
by it are not evaluated unconditionally, and a nested [?:] is linted in its
own right. The hazard set matches the Wasm validator's ([lint_eager_select]
in [Validation]): integer division/remainder, field and element accesses,
[!], the descriptor cast, [array.new_data]/[array.new_elem], [unreachable],
calls, assignments, throws, and stack-switching — but not plain casts (a
[ref.cast] is diagnosed by [cast-always-fails] instead). *)andcollect_labels_listaccl=List.fold_leftcollect_labelsacclandcollect_labels_optacco=matchowithSomei->collect_labelsacci|None->accletrecfind_eager_hazard(e:_Ast.instr)=let(<|>)of=matchowithSome_->o|None->f()inletdescendl=List.fold_left(funacce->acc<|>fun()->find_eager_hazarde)Nonelinmatche.descwith(* Trapping or effectful operations: report the operation itself. *)|ArrayGet_|ArraySet_|StructGet_|StructSet_|GetDescriptor_|NonNull_|CastDesc_|ArraySegment_|Unreachable|Call_|TailCall_|Set_|Tee_|Throw_|ThrowRef_|ContNew_|ContBind_|Suspend_|Resume_|ResumeThrow_|ResumeThrowRef_|Switch_->Somee.info|BinOp({desc=Div(Some_)|Rem_;_},_,_)->Somee.info(* Pure operators: descend into their eagerly-evaluated operands. *)|BinOp(_,a,b)->descend[a;b]|UnOp(_,a)|Cast(a,_)|Test(a,_)|Labelled(_,a)|ArrayDefault(_,a)|StructDefaultDesca(* An [on]-clause only ever wraps a resume-family call — itself a hazard — so
descend into it rather than treating it as a nested control construct. *)|On(a,_)->find_eager_hazarda|Array(_,a,b)->descend[a;b]|ArrayFixed(_,l)|Sequencel->descendl|Struct(_,fields)->descend(List.filter_map(fun(_,v)->v)fields)|StructDesc(d,fields)->find_eager_hazardd<|>fun()->descend(List.filter_map(fun(_,v)->v)fields)|Let(_,init)->(matchinitwithSomee->find_eager_hazarde|None->None)(* Constants, reads, and allocations of default values never trap; nested
control constructs guard their sub-expressions, so stop there. *)|Get_|Path_|Int_|Float_|Char_|String_|Null|Nop|Hole|StructDefault_|Block_|Loop_|While_|If_|TryTable_|Try_|TryCatch_|Br_|Br_if_|Br_table_|Dispatch_|Match_|Br_on_null_|Br_on_non_null_|Br_on_cast_|Br_on_cast_fail_|Br_on_cast_desc_eq_|Br_on_cast_desc_eq_fail_|Return_|Select_|If_annotation_->None(*** Lint checks on constant operands ***)(* Parse a Wax integer literal (decimal or [0x] hex, with [_] separators) to an
[int64], or [None] if it is malformed or does not fit. *)letint_literal_values=Int64.of_string_opt(String.concat""(String.split_on_char'_'s))(* The [int64] value of a constant integer operand, folding a leading sign
exactly as [To_wasm] does — so the lint fires only when the emitted operand is
a folded [i32.const], matching what the Wasm validator sees. A negative literal
is [UnOp (Neg, Int …)], not a bare [Int]; [To_wasm] folds a [Neg] only when its
operand is a bare literal ([-40] -> [i32.const -40]), and treats [Pos] as
transparent ([+-40] -> [i32.const -40]), but a [Neg] over anything else lowers
to a runtime [i32.sub 0 x] ([--40] / [-+40]) that is NOT a constant operand. So
fold a bare-literal [Neg] and recurse through [Pos] only; anything deeper is
[None]. Folding a nested [Neg] here over-fires relative to the validator. *)letrecint_operand_value(e:_Ast.instr)=matche.descwith|Ast.Ints->int_literal_values|UnOp({desc=Neg;_},{desc=Ast.Ints;_})->Option.mapInt64.neg(int_literal_values)|UnOp({desc=Pos;_},a)->int_operand_valuea|_->None(* Whether [e] is a constant integer operand equal to zero. Signed, so a written
[-0] / [+0] counts: the sign folds into the emitted [iNN.const 0] (see
[int_operand_value]), which is what the Wasm validator's constant stack sees,
so a bare-literal-only test would leave the wat form of [x /s -0] linted and
the wax form silent. *)letint_operand_value_is_zeroe=int_operand_valuee=Some0L(* [x << n] / [x >> n] with a constant [n] at least the operand's bit width:
Wasm masks [n] modulo the width, so the shift is almost certainly not what
was meant. The operand width comes from the result cell: a concrete i32/i64,
or a still-flexible integer at its default width (Number/Int -> i32,
LargeInt -> i64). This is deferred (see [ctx.deferred_lints]) until typing
finishes, so a literal a later context pins to a wider type is already
concrete here — [1 << 40] typed [i64] is fine, typed [i32] is not — and only a
genuinely unconstrained operand falls back to a default. *)letlint_shiftctxopresultrhs=(* Parse a constant shift count unsigned: a hex literal past [2^63] wraps to a
negative [int64] under a signed parse, so compare unsigned. Fold a leading
sign exactly as [int_operand_value] / [To_wasm] do — a bare-literal [Neg]
(a negative count, masked modulo the width, is [UnOp (Neg, Int …)]) and a
transparent [Pos]; a [Neg] over anything else ([--n]) lowers to a runtime
[i32.sub 0 x] the Wasm validator does not see as a constant, so do not fold
it. A literal exceeding [u64] ([None]) is left alone (astronomically large,
and beyond what the message can render). *)letof_ints=letbits=String.concat""(String.split_on_char'_'s)inifString.starts_with~prefix:"0x"bitsthenInt64.of_string_optbitselseInt64.of_string_opt("0u"^bits)inletrecshift_count(e:_Ast.instr)=matche.descwith|Ast.Ints->of_ints|UnOp({desc=Neg;_},{desc=Ast.Ints;_})->Option.mapInt64.neg(of_ints)|UnOp({desc=Pos;_},a)->shift_counta|_->Noneinmatchop.Annot.descwith|Shl|Shr_->(matchmatchCell.getresultwith|Valtype{internal=I32;_}|Number|Int->Some32|Valtype{internal=I64;_}|LargeInt->Some64|_->Nonewith|None->()|Somewidth->(matchshift_countrhswith|SomenwhenInt64.unsigned_comparen(Int64.of_intwidth)>=0->Error.shift_overflowctx.diagnostics~location:op.info~widthn|_->()))|_->()(* Run and clear the lints deferred until their result cells were pinned (see
[ctx.deferred_lints]). Called once each per-body scope (global initializers,
then each function body) finishes typing, so the diagnostics stay in source
order rather than all landing at the end of the module. *)letflush_deferred_lintsctx=List.iter(funf->f())(List.rev!(ctx.deferred_lints));ctx.deferred_lints:=[](* Integer [/] or [%] by a constant zero always traps. [Div (Some _)] and
[Rem _] are the integer forms ([Div None] is float division, which does not
trap on a zero divisor). *)letlint_divisionctx(op:(Ast.binop,location)Ast.annotated)rhs=matchop.descwith|(Div(Some_)|Rem_)whenint_operand_value_is_zerorhs->Error.division_by_zeroctx.diagnostics~location:op.info|_->()(* Parse a Wax float literal (decimal or hex float with [_] separators, or the
[nan:0x…] form) to an OCaml float, or [None] if it is malformed. *)letfloat_literal_values=ifString.lengths>=3&&String.equal(String.subs03)"nan"thenSomeFloat.nanelsefloat_of_string_opt(String.concat""(String.split_on_char'_'s))(* Round an [f64] to the nearest representable [f32] (the demote the runtime
applies), via the single-precision bit layout. *)letround_to_f32f=Int32.float_of_bits(Int32.bits_of_floatf)(* The float value of a constant operand, looking through a leading sign and a
demote/promote to a float type. The latter matters because a constant [f32]
has no literal suffix, so the decompiler prints it as [<lit> as f32] — a
trapping conversion's constant operand ([<big> as f32 as i32_u_strict]) then
hides behind that [as f32]. Round through the demote so the folded value is
the one the conversion actually sees. *)letrecfloat_operand_valuei=matchi.descwith|Ast.Floats->float_literal_values|UnOp({desc=Neg;_},{desc=Ast.Floats;_})->(* [To_wasm] folds a [Neg] into a negated literal only when its operand is a
bare literal ([-1e30] -> [f64.const -1e30]); a [Neg] over anything else
([--1e30]) lowers to a runtime [f64.neg] the Wasm validator does not fold,
so — as for [int_operand_value] — fold only the bare-literal case. *)Option.mapFloat.neg(float_literal_values)|UnOp({desc=Pos;_},e)->float_operand_valuee|Cast(e,ValtypeF32)->Option.mapround_to_f32(float_operand_valuee)|Cast(e,ValtypeF64)->float_operand_valuee|_->None(* Whether a trapping (toward-zero) float-to-integer conversion of [f] to the
given target/signage would trap: [f] is NaN or infinite, or its truncation
lies outside the target range. Bounds are the exact powers of two, so a value
is flagged only when it is definitely out of range (no false positives near a
boundary the float type cannot represent exactly). *)letfloat_conversion_trapstargetsignagef=ifnot(Float.is_finitef)thentrueelselett=Float.truncfinletpow2n=Float.ldexp1.ninmatch(target,signage)with|`I32,Signed->t<-.pow231||t>=pow231|`I32,Unsigned->t<0.||t>=pow232|`I64,Signed->t<-.pow263||t>=pow263|`I64,Unsigned->t<0.||t>=pow264(* A trapping float-to-integer conversion ([e as i32_s] and the like — the
[strict] cast forms lower to [trunc], which traps, rather than [trunc_sat])
of a constant float that is out of the target range: it always traps. *)letlint_conversionctx~locationtypoperand=matchtypwith|Signedtype{typ=(`I32|`I64)astarget;signage;strict=true}->(matchfloat_operand_valueoperandwith|Somefwhenfloat_conversion_trapstargetsignagef->Error.conversion_out_of_rangectx.diagnostics~location|_->())|_->()(* Whether two operands are the same pure read (a local or global [get]), so the
two evaluations yield the same value with no side effect. Restricted to [get]
to stay conservative — no calls, no field/array reads that could trap. *)letidentical_operands(l:_Ast.instr)(r:_Ast.instr)=match(l.desc,r.desc)with|Geta,Getb->String.equala.descb.desc|_->false(* A comparison whose result is constant regardless of its variable operand: an
unsigned comparison against zero ([a <u 0] is false, [a >=u 0] is true), or a
comparison of two identical operands ([a < a] is false, [a == a] is true).
The signed/unsigned option marks an integer comparison; [Eq]/[Ne] carry no
signage, so a self-comparison is only flagged for a concrete integer operand
(a float [a == a] is false on NaN, and reference identity is a separate
concern). *)letlint_comparisonctx(op:(Ast.binop,location)Ast.annotated)(l:_Ast.instr)(r:_Ast.instr)=letis_int(e:_Ast.instr)=matchexpression_type_optewith|Somec->(matchCell.getcwith|Valtype{internal=I32|I64;_}->true|_->false)|None->falseinlettautology=matchop.descwith|Lt(SomeUnsigned)whenint_operand_value_is_zeror->Somefalse|Ge(SomeUnsigned)whenint_operand_value_is_zeror->Sometrue|Gt(SomeUnsigned)whenint_operand_value_is_zerol->Somefalse|Le(SomeUnsigned)whenint_operand_value_is_zerol->Sometrue|(Lt(Some_)|Gt(Some_))whenidentical_operandslr->Somefalse|(Le(Some_)|Ge(Some_))whenidentical_operandslr->Sometrue|Eqwhenidentical_operandslr&&is_intl->Sometrue|Newhenidentical_operandslr&&is_intl->Somefalse|_->Noneinmatchtautologywith|Somevalue->Error.tautological_comparisonctx.diagnostics~location:op.info~value|None->()(* An arithmetic operation with no effect on its result (an identity operand or
two identical operands), or whose result is a constant regardless of the
variable operand (an absorbing operand). Off by default. *)(* A concrete float operand, or a float literal not yet pinned to a width. The
arithmetic identities below hold only for integers (see [lint_redundant]), and
the Wasm validator likewise runs them only for integer binops, so both lints
gate on this. *)letis_float_operande=matchexpression_type_optewith|Somec->(matchCell.getcwith|Valtype{internal=F32|F64;_}|Float->true|_->false)|None->falseletlint_redundantctx(op:(Ast.binop,location)Ast.annotated)(l:_Ast.instr)(r:_Ast.instr)=(* Look through a leading sign so a signed identity literal — [x + -0],
[x * +1] — is recognised, as the Wasm validator does (it sees the folded
[iNN.const]; [-0] is [UnOp (Neg, Int 0)], not a bare [Int], on this side). *)letis0e=int_operand_valuee=Some0Linletis1e=int_operand_valuee=Some1Linletis_inte=matchexpression_type_optewith|Somec->(matchCell.getcwith|Valtype{internal=I32|I64;_}->true|_->false)|None->falsein(* These identities and absorptions hold only for integer arithmetic — a float
[0.0 * x] is NaN when [x] is NaN or an infinity, and [-0.0 + 0.0] is [+0.0],
so neither result is constant or effect-free (see [is_float_operand]). *)letis_float=is_float_operandinletno_effect()=Error.redundant_operationctx.diagnostics~location:op.info(Wax_utils.Message.text"This operation has no effect on its result.")inletalwaysv=Error.redundant_operationctx.diagnostics~location:op.infoWax_utils.Message.((text"This operation always yields"++int64v)^^text".")inifis_floatl||is_floatrthen()elsematchop.descwith|Addwhenis0l||is0r->no_effect()(* x + 0 *)|(Sub|Shl|Shr_)whenis0r->no_effect()(* x - 0, x << 0 *)|Mulwhenis1l||is1r->no_effect()(* x * 1 *)|Div(Some_)whenis1r->no_effect()(* x / 1 *)|(Or|Xor)whenis0l||is0r->no_effect()(* x | 0, x ^ 0 *)|(And|Or)whenidentical_operandslr->no_effect()(* x & x, x | x *)|Mulwhenis0l||is0r->always0L(* x * 0 *)|Andwhenis0l||is0r->always0L(* x & 0 *)|Rem_whenis1r->always0L(* x % 1 *)|Xorwhenidentical_operandslr->always0L(* x ^ x (integer bitwise) *)(* [x - x] is 0 only for integers: a float [x - x] is NaN when [x] is NaN or
an infinity, so the result is not a constant. *)|Subwhenidentical_operandslr&&is_intl->always0L|_->()(* [-e] has no Wasm instruction of its own: it lowers to [0 - e], so the Wasm
validator sees the [x - 0] identity whenever [e] is zero and reports it there.
Mirror it here, with the same wording — it is the same finding, reached through
the lowering — for the one shape that reaches it: a bare literal folds ([-0]
becomes the constant [i32.const -0] and emits no subtraction at all, so the
validator stays silent and so must this), while a negation of anything else
zero-valued ([-(-0)]) does emit the subtraction. Integers only, as for the
binop identities: a float [-0.0] genuinely produces negative zero. *)letlint_redundant_unopctx(op:(Ast.unop,location)Ast.annotated)(e:_Ast.instr)=matchop.descwith|Ast.Negwhen(not(is_float_operande))&&int_operand_valuee=Some0L&&matche.Ast.descwithAst.Int_->false|_->true->Error.redundant_operationctx.diagnostics~location:op.info(Wax_utils.Message.text"This operation has no effect on its result.")|Neg|Pos|Not->()(* A branch, loop, or [select] condition that is a constant literal, so it
always takes the same path. [is_while] excludes the idiomatic infinite loop
[while <nonzero>] (only [while 0], a loop that never runs, is flagged). *)letlint_conditionctx?(is_while=false)(cond:_Ast.instr)=matchint_operand_valuecondwith|Somen->letvalue=n<>0Linifnot(is_while&&value)thenError.constant_conditionctx.diagnostics~location:cond.info~value|None->()(* Report an eager-evaluation hazard in the [?:] branch [arm]; [select] is the
location of the whole [?:] (for the secondary caret). *)letlint_eager_selectctx~selectarm=matchfind_eager_hazardarmwith|Somelocation->Error.eager_selectctx.diagnostics~location~select|None->()(* Human-readable name of a binary operator's precedence class, for the
[precedence] lint's message. The classification and the confusing-mix table
are shared with the Wax printer — see {!Ast_utils.binop_kind} and
{!Ast_utils.confusing_precedence}. *)letbinop_kind_name=function|`Shift->"shift"|`Arith->"arithmetic"|`Bitwise->"bitwise"|`Comparison->"comparison"(* Whether the operand [child] of a binary operator was written parenthesized.
Parentheses are erased by the grammar ([1 << (n - 1)] and [1 << n - 1] parse
to the same tree), so this is decided from the source text: a parenthesized
operand is immediately preceded by [(] (a right operand) or followed by [)]
(a left operand), skipping whitespace. With no source available, assume it is
parenthesized (so the lint stays silent rather than risk a false positive). *)letoperand_parenthesizedctx~(op:(Ast.binop,location)Ast.annotated)~side(child:_Ast.instr)=matchWax_utils.Diagnostic.sourcectx.diagnosticswith|None->true|Somesrc->letis_space=function' '|'\t'|'\n'|'\r'->true|_->falseinletn=String.lengthsrcin(* Index of the next significant (non-trivia) character at or after [i],
skipping whitespace and comments — line ([//…]) and nesting block
([/*…*/]) — so a comment between an operand and its bracket does not
hide the parenthesis (a whitespace-only skip used to warn spuriously). *)letrecskip_fwdi=ifi>=nthenielseifis_spacesrc.[i]thenskip_fwd(i+1)elseifi+1<n&&src.[i]='/'&&src.[i+1]='/'thenletreceolj=ifj>=n||src.[j]='\n'thenjelseeol(j+1)inskip_fwd(eol(i+2))elseifi+1<n&&src.[i]='/'&&src.[i+1]='*'thenletrecblkdepthj=ifj+1>=nthennelseifsrc.[j]='/'&&src.[j+1]='*'thenblk(depth+1)(j+2)elseifsrc.[j]='*'&&src.[j+1]='/'thenifdepth=1thenj+2elseblk(depth-1)(j+2)elseblkdepth(j+1)inskip_fwd(blk1(i+2))elseiinletsignificant_iscfrom=from<n&&src.[from]=cin(* Both operands are tested by scanning forward, since a parenthesised
operand always has a bracket downstream: the right operand's [(] follows
the operator, the left operand's [)] follows the operand. *)letfrom=matchsidewith|`Right->skip_fwdop.info.loc_end.pos_cnum|`Left->skip_fwdchild.info.loc_end.pos_cnuminsignificant_is(matchsidewith`Right->'('|`Left->')')from(* The [precedence] lint: flag a binary operator [op] one of whose operands is
itself a binary operator of a confusingly-related class (see
{!Ast_utils.confusing_precedence}), written without disambiguating
parentheses. The Wax printer parenthesises exactly these mixes (see
[Output]), so re-printed / decompiled Wax stays quiet under the lint. *)letlint_precedencectx(op:(binop,location)annotated)e1e2=letouter=Ast_utils.binop_kindop.descinList.iter(fun(child,side)->matchchild.descwith|BinOp(inner_op,_,_)whenAst_utils.confusing_precedenceouter(Ast_utils.binop_kindinner_op.desc)&¬(operand_parenthesizedctx~op~sidechild)->(* The fix is to parenthesise the tighter-binding sub-expression the
lint identifies ([child]). An edit is one contiguous replacement, so
it replaces [child]'s span with '(' ^ its source slice ^ ')'. *)letedit=matchWax_utils.Diagnostic.sourcectx.diagnosticswith|Somesrc->lets=child.info.loc_start.pos_cnumande=child.info.loc_end.pos_cnuminif0<=s&&s<=e&&e<=String.lengthsrcthenSome{Wax_utils.Diagnostic.edit_location=child.info;new_text="("^String.subsrcs(e-s)^")";}elseNone|None->NoneinError.precedence?editctx.diagnostics~location:op.info~inner:inner_op.info~outer_kind:(binop_kind_nameouter)~inner_kind:(binop_kind_name(Ast_utils.binop_kindinner_op.desc))|_->())[(e1,`Left);(e2,`Right)](* Walk the source AST (before any lowering, so [while] keeps its own condition
rather than the [if] it desugars to) and report the purely-syntactic lints: a
constant branch/loop/select condition, and a drop ([_ = e]) of a
side-effect-free expression. Runs once over the source rather than in the type
checker's expression handling. Mirrors the case coverage of
{!collect_labels}. *)letreclint_sourcectx(i:_Ast.instr)=letlistl=List.iter(lint_sourcectx)linletopto=Option.iter(lint_sourcectx)oinmatchi.descwith|If{cond;if_block;else_block;_}->lint_conditionctxcond;lint_sourcectxcond;listif_block.desc;Option.iter(funb->listb.Annot.desc)else_block|While{cond;step;block;_}->lint_conditionctx~is_while:truecond;lint_sourcectxcond;optstep;listblock.desc|Select(c,t,e)->lint_conditionctxc;lint_eager_selectctx~select:i.infot;lint_eager_selectctx~select:i.infoe;lint_sourcectxc;lint_sourcectxt;lint_sourcectxe|Br_if(_,c)->(* A br_if that carries a value has operand [Sequence [values…; cond]], so
the condition is the last element; a bare br_if's operand is the
condition itself. *)letcond=matchc.descwith|Sequence(_::_asseq)->List.nthseq(List.lengthseq-1)|_->cinlint_conditionctxcond;lint_sourcectxc|Block{block;_}|Loop{block;_}|TryTable{block;_}->listblock.desc|Try{block;catches;catch_all;_}->listblock.desc;List.iter(fun(_,b)->listb.Annot.desc)catches;Option.iter(funb->listb.Annot.desc)catch_all|TryCatch{block;arms;_}->listblock.desc;List.iter(funa->lista.arm_body.desc)arms|Call(t,args)|TailCall(t,args)->lint_sourcectxt;listargs|Set(id,op,e)->(* A plain self-assignment [x = x] has no effect. A compound assignment
[x op= x] is not redundant (e.g. [x += x] doubles it). The pointless-
drop check lives in the [Let] case, since a drop [_ = e] is an anonymous
binding. *)(match(op,e.desc)with|None,Getid'whenString.equalid.descid'.desc->Error.redundant_operationctx.diagnostics~location:i.info(Wax_utils.Message.text"This assignment writes the variable back to itself.")|_->());lint_sourcectxe|Tee(_,e)|Labelled(_,e)|Cast(e,_)|Test(e,_)|NonNulle|StructGet(e,_)|GetDescriptore|StructDefaultDesce|UnOp(_,e)|On(e,_)|Br_table(_,e)|Br_on_null(_,e)|Br_on_non_null(_,e)|Br_on_cast(_,_,e)|Br_on_cast_fail(_,_,e)|ThrowRefe|ArrayDefault(_,e)|ContNew(_,e)->lint_sourcectxe|Struct(_,fields)->List.iter(fun(_,e)->Option.iter(lint_sourcectx)e)fields|StructDesc(d,fields)->lint_sourcectxd;List.iter(fun(_,e)->Option.iter(lint_sourcectx)e)fields|BinOp(op,e1,e2)->lint_precedencectxope1e2;lint_sourcectxe1;lint_sourcectxe2|CastDesc(e1,_,e2)|Br_on_cast_desc_eq(_,_,e1,e2)|Br_on_cast_desc_eq_fail(_,_,e1,e2)|StructSet(e1,_,e2)|Array(_,e1,e2)|ArraySegment(_,_,e1,e2)|ArrayGet(e1,e2)->lint_sourcectxe1;lint_sourcectxe2|ArraySet(e1,e2,e3)->lint_sourcectxe1;lint_sourcectxe2;lint_sourcectxe3|ArrayFixed(_,l)|ContBind(_,_,l)|Suspend(_,l)|Resume(_,_,l)|ResumeThrow(_,_,_,l)|ResumeThrowRef(_,_,l)|Switch(_,_,l)|Throw(_,l)|Sequencel->listl|Dispatch{index;arms;_}->lint_sourcectxindex;List.iter(fun(_,b)->listb.Annot.desc)arms|Match{scrutinee;arms;default}->lint_sourcectxscrutinee;List.iter(fun(_,b)->listb.Annot.desc)arms;listdefault.desc|Let(bindings,body)->(* A drop [_ = e] is a single anonymous binding; if [e] is effect-free,
computing it only to discard the result is pointless. *)(match(bindings,body)with|[(None,_)],Someewhenis_effectlesse->Error.unused_resultctx.diagnostics~location:e.info|_->());optbody|Br(_,o)|Returno->opto|If_annotation{then_body;else_body;_}->listthen_body.desc;Option.iter(funb->listb.Annot.desc)else_body|Get_|Path_|Unreachable|Nop|Hole|Null|Char_|String_|Int_|Float_|StructDefault_->()