// The lints that read the SOURCE shape rather than the types.
//
// Ported from `lint_source` in wax/src/lib-wax/typing_lint.ml.
//
// These ask what the author wrote, not what it means: an expression computed and
// then dropped, a condition already decided, a variable assigned to itself.
// None of them needs the checker's result, which is why they walk the untyped
// tree -- and why they can say `x = x` is pointless without knowing what `x` is.
///|
/// The `int64` value of a constant integer operand, folded exactly as the code
/// generator folds it.
///
/// A negative literal is `UnOp(Neg, Int ..)` rather than a bare `Int`, and the
/// generator folds a `Neg` only over a bare literal -- `-40` becomes a constant,
/// `--40` becomes a runtime subtraction. `Pos` is transparent. Folding anything
/// deeper here would make the lint fire where the emitted code has no constant
/// at all.
fn int_operand_value(e : @ast.Instr[@basic.Location]) -> Int64? {
match e.desc {
Int(s) => int_literal_value(s)
UnOpI(op, inner) =>
match (op.desc, inner.desc) {
(Neg, Int(s)) => int_literal_value(s).map(v => -v)
(Pos, _) => int_operand_value(inner)
_ => None
}
_ => None
}
}
///|
/// A decimal or hex integer literal's value, underscores ignored.
fn int_literal_value(s : String) -> Int64? {
let cleaned = s.split("_").join("")
Some(@string.parse_int64(cleaned)) catch {
_ =>
// A `0x..` literal past 2^63 does not fit a signed parse; read it
// unsigned and reinterpret, which is the bit pattern the encoder emits.
Some(@string.parse_uint64(cleaned).reinterpret_as_int64()) catch {
_ => None
}
}
}
///|
/// Whether a cast never traps, so discarding its result is pointless.
///
/// A STRICT float-to-integer conversion lowers to `trunc`, which traps out of
/// range; its saturating form and every numeric widen, narrow and convert are
/// total. A reference cast may trap and is conservatively treated as partial.
fn cast_is_total(c : @ast.CastType) -> Bool {
match c {
Signed(typ~, strict~, ..) =>
match typ {
F32 | F64 => true
I32 | I64 => !strict
}
Value(I32 | I64 | F32 | F64) => true
Value(V128 | Ref(_)) | Func(..) => false
}
}
///|
/// Whether a method computes its result and does nothing else.
fn is_pure_unary_method(m : String) -> Bool {
match m {
"clz"
| "ctz"
| "popcnt"
| "extend8_s"
| "extend16_s"
| "abs"
| "ceil"
| "floor"
| "trunc"
| "nearest"
| "sqrt"
| "to_bits"
| "from_bits" => true
_ => false
}
}
///|
/// Likewise for the two-operand methods.
fn is_pure_binary_method(m : String) -> Bool {
match m {
"rotl" | "rotr" | "min" | "max" | "copysign" => true
_ => false
}
}
///|
/// Whether evaluating this expression can do nothing but produce a value.
///
/// Conservative in the one direction that matters: anything not listed is
/// assumed to have an effect, so the lint never claims a computation is
/// pointless when it might trap, write, or call.
pub fn is_effectless(e : @ast.Instr[@basic.Location]) -> Bool {
fn field(f : (@ast.Ident, @ast.Instr[@basic.Location]?)) -> Bool {
match f.1 {
Some(v) => is_effectless(v)
// A punned field `{x}` reads a local or a global, which is effectless.
None => true
}
}
match e.desc {
Get(_)
| Int(_)
| Float(_)
| Char(_)
| Str(_, _)
| Null
| StructDefault(_) => true
UnOpI(_, a) => is_effectless(a)
Call(callee, args) =>
match callee.desc {
StructGet(recv, m) =>
if args.is_empty() && is_pure_unary_method(m.name) {
is_effectless(recv)
} else if args.length() == 1 && is_pure_binary_method(m.name) {
is_effectless(recv) && is_effectless(args[0])
} else if @simd.classify(m.name) is Some(_) {
// Every vector op is pure and non-trapping; the trapping SIMD
// accesses go through the `mem.` path and are classified there.
is_effectless(recv) && args.iter().all(is_effectless)
} else if m.name == "size" && recv.desc is Get(_) && args.is_empty() {
// `m.size()` / `tab.size()` reads the current size, unlike the
// effectful grow/fill/copy/init on the same path.
true
} else {
false
}
// A `v128::..` constructor or vector op is effect-free when its
// operands are.
Path(ns, _) => ns.name == "v128" && args.iter().all(is_effectless)
_ => false
}
// A typed null `null as &?t` is `ref.null`, a constant, not a trapping
// reference cast.
Cast(inner, Value(Ref({ nullable: true, .. }))) => inner.desc is Null
Cast(a, ct) => cast_is_total(ct) && is_effectless(a)
// Division and remainder trap on a zero divisor.
BinOpI(op, a, b) =>
if op.desc is (Div(_) | Rem(_)) {
false
} else {
is_effectless(a) && is_effectless(b)
}
Select(a, b, c) => is_effectless(a) && is_effectless(b) && is_effectless(c)
Test(a, _) => is_effectless(a)
Struct(_, fields) => fields.iter().all(field)
StructDesc(d, fields) => is_effectless(d) && fields.iter().all(field)
StructDefaultDesc(d) => is_effectless(d)
Array(_, elt, len) => is_effectless(elt) && is_effectless(len)
ArrayDefault(_, len) => is_effectless(len)
ArrayFixed(_, elts) => elts.iter().all(is_effectless)
_ => false
}
}
///|
/// Report a condition whose value is already decided.
///
/// A `while (true)` is exempt: an infinite loop written that way is
/// deliberate, and saying so would be noise on the one shape that means it.
fn lint_condition(
ctx : @typing_env.ModuleContext,
cond : @ast.Instr[@basic.Location],
is_while? : Bool = false,
) -> Unit {
guard int_operand_value(cond) is Some(n) else { return }
let value = n != 0
if !(is_while && value) {
constant_condition(ctx.diagnostics, cond.info, value)
}
}
///|
/// Walk a function body reporting the source-shape lints.
///
/// Every arm here is a recursion; the ones that also report are `Let` (a drop
/// whose value is computed for nothing), `Set` (a variable written back to
/// itself), and the three that carry a condition.
fn lint_source(
ctx : @typing_env.ModuleContext,
i : @ast.Instr[@basic.Location],
) -> Unit {
fn go(x : @ast.Instr[@basic.Location]) -> Unit {
lint_source(ctx, x)
}
fn list(l : Array[@ast.Instr[@basic.Location]]) -> Unit {
for x in l {
go(x)
}
}
match i.desc {
If(cond~, if_block~, else_block~, ..) => {
lint_condition(ctx, cond)
go(cond)
list(if_block.desc)
if else_block is Some(b) {
list(b.desc)
}
}
While(cond~, step~, block~, ..) => {
lint_condition(ctx, cond, is_while=true)
go(cond)
if step is Some(s) {
go(s)
}
list(block.desc)
}
BrIf(_, c) => {
// A `br_if` carrying a value has `Sequence [values..; cond]`, so the
// condition is the last element; a bare one's operand IS the condition.
let cond = match c.desc {
Sequence(seq) => if seq.is_empty() { c } else { seq[seq.length() - 1] }
_ => c
}
lint_condition(ctx, cond)
go(c)
}
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.
if bindings.length() == 1 &&
bindings[0].0 is None &&
body is Some(e) &&
is_effectless(e) {
unused_result(ctx.diagnostics, e.info)
}
if body is Some(e) {
go(e)
}
}
BinOpI(op, e1, e2) => {
lint_precedence(ctx, op, e1, e2)
go(e1)
go(e2)
}
Set(idx, op, e) => {
// A plain self-assignment `x = x` does nothing. A compound `x op= x` is
// not redundant -- `x += x` doubles it.
if op is None && e.desc is Get(other) && other.name == idx.name {
redundant_operation(
ctx.diagnostics,
i.info,
@message.text("This assignment writes the variable back to itself."),
)
}
go(e)
}
Select(c, t, e) => {
lint_condition(ctx, c)
// Both branches run, so a hazard in either is reported -- against the
// `?:` itself, which is what makes it happen.
for arm in [t, e] {
if find_eager_hazard(arm) is Some(loc) {
eager_select(ctx.diagnostics, loc, i.info)
}
}
go(c)
go(t)
go(e)
}
Block(block~, ..) | Loop(block~, ..) | TryTable(block~, ..) =>
list(block.desc)
Try(block~, catches~, catch_all~, ..) => {
list(block.desc)
for c in catches {
list(c.1.desc)
}
if catch_all is Some(b) {
list(b.desc)
}
}
TryCatch(block~, arms~, ..) => {
list(block.desc)
for a in arms {
list(a.arm_body.desc)
}
}
Call(t, args) | TailCall(t, args) => {
go(t)
list(args)
}
Dispatch(index~, arms~, ..) => {
go(index)
for a in arms {
list(a.1.desc)
}
}
Match(scrutinee~, arms~, default~) => {
go(scrutinee)
for a in arms {
list(a.1.desc)
}
list(default.desc)
}
IfAnnotation(then_body~, else_body~, ..) => {
list(then_body.desc)
if else_body is Some(b) {
list(b.desc)
}
}
Struct(_, fields) =>
for f in fields {
if f.1 is Some(v) {
go(v)
}
}
StructDesc(d, fields) => {
go(d)
for f in fields {
if f.1 is Some(v) {
go(v)
}
}
}
Br(_, o) | Return(o) => if o is Some(e) { go(e) }
// Everything else is a plain recursion into whatever it contains. NOT
// `iter_instr`, which visits the node itself first and would recurse
// forever here.
_ =>
for sub in i.sub_instrs() {
go(sub)
}
}
}
///|
/// The first operation in `e` that traps or has an effect, if any.
///
/// A `?:` compiles to a wasm `select`, which evaluates both operands before
/// choosing -- so anything here runs whichever way the condition goes. Pure
/// operators are descended into, because their operands run too; a nested
/// CONTROL construct guards its own sub-expressions, so the walk stops there.
fn find_eager_hazard(e : @ast.Instr[@basic.Location]) -> @basic.Location? {
fn descend(l : Array[@ast.Instr[@basic.Location]]) -> @basic.Location? {
for x in l {
if find_eager_hazard(x) is Some(loc) {
return Some(loc)
}
}
None
}
fn field_values(
fields : Array[(@ast.Ident, @ast.Instr[@basic.Location]?)],
) -> Array[@ast.Instr[@basic.Location]] {
let out : Array[@ast.Instr[@basic.Location]] = []
for f in fields {
if f.1 is Some(v) {
out.push(v)
}
}
out
}
match e.desc {
// Trapping or effectful operations: the operation itself is the hazard.
ArrayGet(_, _)
| ArraySet(_, _, _)
| StructGet(_, _)
| StructSet(_, _, _)
| GetDescriptor(_)
| NonNull(_)
| CastDesc(_, _, _)
| ArraySegment(_, _, _, _)
| Unreachable
| Call(_, _)
| TailCall(_, _)
| Set(_, _, _)
| Tee(_, _)
| Throw(_, _)
| ThrowRef(_)
| ContNew(_, _)
| ContBind(_, _, _)
| Suspend(_, _)
| Resume(_, _, _)
| ResumeThrow(_, _, _, _)
| ResumeThrowRef(_, _, _)
| Switch(_, _, _) => Some(e.info)
// Division and remainder trap on a zero divisor; the other operators do
// not, so their operands are what to look at.
BinOpI(op, a, b) =>
if op.desc is (Div(Some(_)) | Rem(_)) {
Some(e.info)
} else {
descend([a, b])
}
UnOpI(_, a)
| Cast(a, _)
| Test(a, _)
| Labelled(_, a)
| ArrayDefault(_, a)
| StructDefaultDesc(a)
// An `on` clause only ever wraps a resume-family call -- itself a hazard --
// so descend into it rather than treating it as a control construct.
| On(a, _) => find_eager_hazard(a)
Array(_, a, b) => descend([a, b])
ArrayFixed(_, l) | Sequence(l) => descend(l)
Struct(_, fields) => descend(field_values(fields))
StructDesc(d, fields) =>
match find_eager_hazard(d) {
Some(loc) => Some(loc)
None => descend(field_values(fields))
}
Let(_, init) =>
match init {
Some(x) => find_eager_hazard(x)
None => None
}
// Constants, reads and default allocations never trap; a nested control
// construct guards its own sub-expressions, so the walk stops at both.
_ => None
}
}
///|
/// Every label a function body declares.
///
/// Collected once, up front, because the unused-label lint needs the set of
/// DECLARATIONS and the checker only ever sees them one scope at a time. Only
/// the block-like forms carry a label; the function's own is not among them,
/// since it is bound by the frame rather than declared in the body.
fn collect_labels(
instrs : Array[@ast.Instr[@basic.Location]],
) -> Array[@ast.Ident] {
let out : Array[@ast.Ident] = []
fn go(i : @ast.Instr[@basic.Location]) -> Unit {
let label = match i.desc {
Block(label~, ..)
| Loop(label~, ..)
| While(label~, ..)
| If(label~, ..)
| TryTable(label~, ..)
| Try(label~, ..)
| TryCatch(label~, ..) => label
_ => None
}
if label is Some(l) {
out.push(l)
}
for sub in i.sub_instrs() {
go(sub)
}
}
for s in instrs {
go(s)
}
out
}
///|
/// A shift count parsed UNSIGNED.
///
/// A hex literal past 2^63 wraps to a negative `Int64` under a signed parse, so
/// the count is read unsigned and compared that way. The sign folds exactly as
/// the code generator folds it, for the same reason as `int_operand_value`.
fn shift_count(e : @ast.Instr[@basic.Location]) -> Int64? {
fn of_int(s : String) -> Int64? {
let bits = s.split("_").join("")
Some(@string.parse_uint64(bits).reinterpret_as_int64()) catch {
_ => None
}
}
match e.desc {
Int(s) => of_int(s)
UnOpI(op, inner) =>
match (op.desc, inner.desc) {
(Neg, Int(s)) => of_int(s).map(v => -v)
(Pos, _) => shift_count(inner)
_ => None
}
_ => None
}
}
///|
/// Report a shift whose constant count is at least the operand's width.
///
/// DEFERRED until typing finishes: the width comes from the result cell, and a
/// later context can still widen it -- `1 << 40` typed `i64` is fine, typed
/// `i32` is not. Only a genuinely unconstrained operand falls back to a default.
fn lint_shift(
ctx : @typing_env.ModuleContext,
op : @basic.Annotated[@ast.BinOp, @basic.Location],
result : @infer.Cell[@infer.InferredType],
rhs : @ast.Instr[@basic.Location],
) -> Unit {
guard op.desc is (Shl | Shr(_)) else { return }
let width = match result.get() {
Valtype({ internal: I32, .. }) | Number | Int => Some(32)
Valtype({ internal: I64, .. }) | LargeInt => Some(64)
_ => None
}
guard width is Some(width) else { return }
guard shift_count(rhs) is Some(n) else { return }
if n.reinterpret_as_uint64() >= width.to_uint64() {
shift_overflow(ctx.diagnostics, op.info, width, n)
}
}
///|
/// Report an integer division or remainder by a constant zero.
///
/// `Div(None)` is float division, which does not trap on a zero divisor.
fn lint_division(
ctx : @typing_env.ModuleContext,
op : @basic.Annotated[@ast.BinOp, @basic.Location],
rhs : @ast.Instr[@basic.Location],
) -> Unit {
guard op.desc is (Div(Some(_)) | Rem(_)) else { return }
if int_operand_value(rhs) is Some(0) {
division_by_zero(ctx.diagnostics, op.info)
}
}
///|
/// A float literal's value, underscores ignored.
///
/// `nan`, `nan:0x..` and the like are all NaN as far as a range check is
/// concerned: what matters is that no truncation of one is in range.
fn float_literal_value(s : String) -> Double? {
if s.length() >= 3 && s[0:3] == "nan" {
return Some(@double.not_a_number)
}
Some(@string.parse_double(s.split("_").join(""))) catch {
_ => None
}
}
///|
/// The value of a constant float operand, folded as the code generator folds
/// it.
///
/// A leading sign, as for an integer -- and a cast to a float type, which is
/// transparent to the value but not to its PRECISION. A constant `f32` has no
/// literal suffix, so a decompiler writes it ` as f32`; rounding through
/// that demote is what makes the folded value the one the conversion actually
/// sees.
fn float_operand_value(e : @ast.Instr[@basic.Location]) -> Double? {
match e.desc {
Float(s) => float_literal_value(s)
UnOpI(op, inner) =>
match (op.desc, inner.desc) {
(Neg, Float(s)) => float_literal_value(s).map(v => -v)
(Pos, _) => float_operand_value(inner)
_ => None
}
Cast(inner, Value(F32)) =>
float_operand_value(inner).map(v => {
(Float::from_double(v) : Float).to_double()
})
Cast(inner, Value(F64)) => float_operand_value(inner)
_ => None
}
}
///|
/// Report a trapping float-to-integer conversion of a constant that is out of
/// the target's range.
///
/// Only the STRICT forms: those lower to `trunc`, which traps, where the
/// saturating forms clamp. The bounds are exact powers of two, so a value is
/// flagged only when it is definitely outside -- never one the float type
/// cannot represent exactly enough to be sure about.
fn lint_conversion(
ctx : @typing_env.ModuleContext,
location : @basic.Location,
target : @ast.CastType,
operand : @ast.Instr[@basic.Location],
) -> Unit {
guard target is Signed(typ~, signage~, strict=true) else { return }
guard typ is (I32 | I64) else { return }
guard float_operand_value(operand) is Some(f) else { return }
let traps = if f.is_nan() || f.is_inf() {
true
} else {
let t = f.trunc()
let pow2 = (n : Int) => @math.pow(2.0, n.to_double())
match (typ, signage) {
(I32, Signed) => t < -pow2(31) || t >= pow2(31)
(I32, Unsigned) => t < 0.0 || t >= pow2(32)
(I64, Signed) => t < -pow2(63) || t >= pow2(63)
(I64, Unsigned) => t < 0.0 || t >= pow2(64)
_ => false
}
}
if traps {
conversion_out_of_range(ctx.diagnostics, location)
}
}
///|
/// Report a comparison whose answer does not depend on its variable operand.
///
/// Two shapes: an unsigned comparison against zero, which every unsigned value
/// is at or above; and a comparison of one expression with itself. The second
/// is restricted to a pure read of a name -- anything else could have an effect
/// or trap, and then the two evaluations are not the same act.
///
/// `Eq`/`Ne` carry no signage, so a self-comparison needs a concrete integer
/// operand to be flagged: a float `a == a` is FALSE when `a` is NaN, and
/// reference identity is a different question.
fn lint_comparison(
ctx : @typing_env.ModuleContext,
op : @basic.Annotated[@ast.BinOp, @basic.Location],
l : @ast.Instr[@typing_env.InferredAnnotation],
_r : @ast.Instr[@typing_env.InferredAnnotation],
l_src : @ast.Instr[@basic.Location],
r_src : @ast.Instr[@basic.Location],
) -> Unit {
fn is_int(e : @ast.Instr[@typing_env.InferredAnnotation]) -> Bool {
match @typing_env.expression_type_opt(e.info) {
Some(c) => c.get() is Valtype({ internal: I32 | I64, .. })
None => false
}
}
fn is_zero(e : @ast.Instr[@basic.Location]) -> Bool {
int_operand_value(e) is Some(0)
}
let same = match (l_src.desc, r_src.desc) {
(Get(a), Get(b)) => a.name == b.name
_ => false
}
let tautology = match op.desc {
Lt(Some(Unsigned)) if is_zero(r_src) => Some(false)
Ge(Some(Unsigned)) if is_zero(r_src) => Some(true)
Gt(Some(Unsigned)) if is_zero(l_src) => Some(false)
Le(Some(Unsigned)) if is_zero(l_src) => Some(true)
Lt(Some(_)) | Gt(Some(_)) if same => Some(false)
Le(Some(_)) | Ge(Some(_)) if same => Some(true)
Eq if same && is_int(l) => Some(true)
Ne if same && is_int(l) => Some(false)
_ => None
}
if tautology is Some(value) {
tautological_comparison(ctx.diagnostics, op.info, value)
}
}
///|
/// Run and clear the lints that were waiting for their result cells to settle.
fn flush_deferred_lints(ctx : @typing_env.ModuleContext) -> Unit {
for f in ctx.deferred_lints {
f()
}
ctx.deferred_lints.clear()
}
///|
/// The next significant character at or after `i`, skipping trivia.
///
/// Whitespace and comments -- line and NESTING block -- because a comment
/// between an operand and its bracket would otherwise hide the parenthesis and
/// make the lint fire on code that reads perfectly well.
fn skip_trivia(src : String, start : Int) -> Int {
let n = src.length()
let mut i = start
while i < n {
let c = src[i]
if c == 32 || c == 9 || c == 10 || c == 13 {
i = i + 1
} else if i + 1 < n && c == 47 && src[i + 1] == 47 {
// A line comment, to the newline.
while i < n && src[i] != 10 {
i = i + 1
}
} else if i + 1 < n && c == 47 && src[i + 1] == 42 {
let mut depth = 1
let mut j = i + 2
while j + 1 < n && depth > 0 {
if src[j] == 47 && src[j + 1] == 42 {
depth = depth + 1
j = j + 2
} else if src[j] == 42 && src[j + 1] == 47 {
depth = depth - 1
j = j + 2
} else {
j = j + 1
}
}
i = if depth > 0 { n } else { j }
} else {
return i
}
}
i
}
///|
/// Whether an operand was WRITTEN parenthesized.
///
/// The grammar erases parentheses -- `1 << (n - 1)` and `1 << n - 1` parse to
/// the same tree -- so this can only be answered from the source text. Both
/// sides are found by scanning FORWARD, since a parenthesized operand always has
/// a bracket downstream: the right operand's `(` follows the operator, the left
/// operand's `)` follows the operand.
///
/// With no source, assume it was: the lint then stays quiet rather than risking
/// a false positive on text it cannot see.
fn operand_parenthesized(
ctx : @typing_env.ModuleContext,
op : @basic.Annotated[@ast.BinOp, @basic.Location],
child : @ast.Instr[@basic.Location],
right~ : Bool,
) -> Bool {
guard ctx.diagnostics.source is Some(src) else { return true }
let from = skip_trivia(
src,
if right {
op.info.end.cnum
} else {
child.info.end.cnum
},
)
guard from < src.length() else { return false }
src[from] == (if right { 40 } else { 41 })
}
///|
/// The name of a binary operator's precedence class, for the message.
fn binop_kind_name(k : @output.BinopKind) -> String {
match k {
Shift => "shift"
Arith => "arithmetic"
Bitwise => "bitwise"
Comparison => "comparison"
}
}
///|
/// Report a binary operator whose operand is a tighter-binding operator of a
/// confusingly-related class, written without disambiguating parentheses.
///
/// The classification is the PRINTER's -- it parenthesises exactly these mixes,
/// so re-printed or decompiled Wax stays quiet under the lint. Sharing the table
/// is what makes that true rather than merely likely.
fn lint_precedence(
ctx : @typing_env.ModuleContext,
op : @basic.Annotated[@ast.BinOp, @basic.Location],
e1 : @ast.Instr[@basic.Location],
e2 : @ast.Instr[@basic.Location],
) -> Unit {
let outer = @output.binop_kind(op.desc)
for side in [(e1, false), (e2, true)] {
let (child, right) = side
guard child.desc is BinOpI(inner_op, _, _) else { continue }
let inner = @output.binop_kind(inner_op.desc)
guard @output.confusing_precedence(outer, inner) else { continue }
guard !operand_parenthesized(ctx, op, child, right~) else { continue }
// The fix parenthesises the tighter-binding sub-expression the lint names.
// An edit is one contiguous replacement, so it replaces that span with its
// own source slice wrapped in brackets.
let edit = match ctx.diagnostics.source {
Some(src) => {
let s = child.info.start.cnum
let e = child.info.end.cnum
if 0 <= s && s <= e && e <= src.length() {
Some(
(
{ loc: child.info, new_text: "(" + src[s:e].to_owned() + ")" } :
@diagnostic.Edit),
)
} else {
None
}
}
None => None
}
precedence(
ctx.diagnostics,
op.info,
inner_op.info,
binop_kind_name(outer),
binop_kind_name(inner),
edit~,
)
}
}