Skip to content

v0.3.0

Choose a tag to compare

@BinFlip BinFlip released this 17 Jul 01:42
· 19 commits to main since this release

Native operation coverage for modern lifters — x87/FPU, SVE/SME, AMX tiles,
vector crypto, system/compute intrinsics, PAC — plus a domain-typed conversion
family replacing the single Conv op. Several effect-classification and
value-numbering correctness fixes ship alongside; see Fixed.

Changed (breaking)

  • SsaOp::Conv is removed, replaced by a domain-typed conversion family so
    each conversion carries only the fields that are meaningful for it:

    Old New Notes
    Conv (int → int) IntConv Identical field set — mechanical rename.
    Conv (int → ptr) IntToPtr No overflow_check/unsigned.
    Conv (ptr → int) PtrToInt No overflow_check/unsigned.
    Conv (int → float) IntToFloat Keeps overflow_check/unsigned.
    Conv (float → int) FloatToInt Keeps overflow_check/unsigned.
    Conv (float → float) FloatConv Width change only; drops both flags.

    PtrAdd is a new address-computation op, not a Conv replacement.

  • SsaOp gained 55 variants (146 → 200), and VectorUnaryKind (+20),
    VectorBinaryKind (+23), VectorCompareKind (+8), VectorTernaryKind (+3),
    AtomicRmwOp (+3), and SsaOpClass (+NativeIntrinsic) also grew. No enum in
    this crate is #[non_exhaustive], so downstream exhaustive matches must handle
    the new variants. This is deliberate: a lifter that silently ignores an
    unhandled op is a bug, so op additions are intended to break the build.

  • ConstValue equality is now structural, and the type is Eq + Hash.
    Float arms compare and hash bitwise, which makes Eq reflexive and lets
    constants be hash-map keys. Two visible consequences: F64(NAN) == F64(NAN) is
    now true, and F64(0.0) == F64(-0.0) is now false (they are
    distinguishable at runtime via 1.0 / x). IEEE-754 semantic comparison is
    unchanged and still lives in ceq and the c*_un family.

  • SsaOp and its operand payloads are now Eq + Hash. No Target change is
    required — the trait already demanded Eq + Hash on its associated types.

Fixed

  • VectorStructLoadReplicate (AArch64 ld2r/ld3r/ld4r) was classified
    Pure
    despite reading memory through its address base. GVN could CSE two of
    them across an intervening store, and DCE could delete them outright. It now
    classifies as Read + TrapClass::MemoryFault and reports may_throw(), like
    every other vector load.

  • GVN could merge distinct ComputeFlags computations. ComputeFlags models
    the flags of bsf/bsr/popcnt/bt alike but carries no opcode
    discriminator, so its result is not a function of its SSA operands; two
    different native flag computations over the same operands produced one key and
    the wrong flags value. It is no longer value-numbered.

  • GVN could merge CallClobber markers, aliasing the fresh undefined values
    of two different calls' caller-saved registers (all of its operands are
    definitions, which the key normalizes to a sentinel). It is no longer
    value-numbered. Phi is likewise excluded — it is block-relative, and the key
    does not encode the defining block.

  • SsaEditor::nop_instruction left the removed value's result_type stamped
    on the Nop.
    set_op_preserving_type now clears the type when the new op
    has no destination.

  • SystemOpKind::Barrier (dsb/dmb/isb/mfence) was classified Write.
    It is an ordering construct, not a clobber: it now classifies as Fence, so
    Memory SSA emits a MemoryOp::Barrier and the verifier's fence invariant
    applies.

  • setffr/wrffr were deletable by DCE. They write the SVE first-fault
    register, which is not an SSA operand, and their outputs may be empty — pure
    plus zero definitions meant DCE removed them, dropping the FFR initialization a
    following first-faulting load depends on. They now report Opaque effects.

  • Pointer conversions sign-extended when folded. IntToPtr/PtrToInt
    hardcoded a signed source, so a 32-bit 0x8000_0000 would widen to
    0xFFFF_FFFF_8000_0000 on a 64-bit target. Pointers are unsigned and now
    zero-extend. (Latent: no in-tree Target implements convert_const.)

  • LICM did not converge on functions with deep loop-invariant dependency
    chains.
    The hoist phase moved only one dependency "wave" per invocation —
    leaving dependent invariants for a later call — so deeply-chained invariants in
    large loops needed O(chain-depth) expensive invocations and routinely exhausted
    the driving fixpoint's iteration cap with work still pending. It now hoists a
    loop's entire invariant chain in one pass, inserting in topological order so a
    hoisted definition always precedes its hoisted uses.

  • Loop canonicalization oscillated against CFG simplification. controlflow
    (jump threading) and blockmerge (trampoline elimination) now preserve
    canonical loop preheaders and unified latches instead of removing them as empty
    forwarding blocks — which loopcanon immediately re-inserted, so the
    normalization fixpoint never settled. Loop-simplify form is now stable (the
    same trade-off LLVM's simplifycfg makes).

Performance

  • GVN's generic value key is now structured rather than a formatted string.
    It stores the operand-normalized SsaOp and probes via derived Eq/Hash,
    removing a per-candidate deep clone, a Debug render, and a String
    allocation, and turning every map probe from a string compare into a
    structural one.
  • LICM can_hoist is O(1) per candidate instead of O(loop). The
    "result feeds a phi on a loop back-edge" test is precomputed once per loop as a
    single backward taint propagation seeded from the back-edge phi operands,
    replacing a fresh forward def-use traversal per candidate
    (O(candidates × loop) → O(loop)).
  • LICM invariant detection and hoist-availability no longer scan every variable
    in the function once per loop; the loop-body-defined variable set is built once
    per loop in O(loop-body) (O(loops × variables) → O(loops × loop-body)).
  • Loop canonicalization re-analyzes the loop forest once per pass instead of once
    per transformation: each pass now canonicalizes every loop the forest reports
    (still one transformation per loop per pass, preserving phi-management
    simplicity), turning O(transformations × loop-analysis) into
    O(passes × loop-analysis).
  • GVN builds the CFG and dominator tree once per run rather than rebuilding it
    (and rescanning every block) for each redundant value pair.

Together these make the normalization pipeline converge in a couple of iterations
on control-flow-flattened inputs (many small nested loops with long invariant
chains), where it previously ran to the iteration cap without converging and
spent super-linear time per call. Output is unchanged.

Added

  • Native intrinsic modeling: NativeIntrinsic, SystemOp, ComputeOp,
    VectorCrypto, TileOp, BcdAdjust, x87/FPU (FpTranscendental,
    FpuControl), and pointer authentication (PacKind).

  • SVE/SME and AMX operations: predicate/first-fault ops, SVE compute, SME
    outer-product and ZA-tile ops, matrix multiply-accumulate, tile operations.

  • serde feature (off by default): serializes the IR data model — the
    operation-kind taxonomy, the native descriptors, and the SSA graph itself
    (SsaFunction and everything reachable from it: blocks, instructions, ops,
    phis, variables, constants, exception handlers).

    Generic IR types carry #[serde(bound(...))], so SsaFunction<T> is
    serializable exactly when the host's Target associated types are. Hosts that
    don't serialize the IR are unaffected: the impl does not apply to them, and no
    serde bound is forced onto Target.

    Two encoding decisions are part of the wire contract:

    • SsaVarId serializes as its logical index, not its internal
      complement encoding (an Option-niche layout optimization that must not leak
      into a persisted format).
    • Maps keyed by VariableOrigin encode as (key, value) sequences. A derived
      map would compile but fail at runtime with "key must be a string" in every
      format that requires string keys, JSON included.

    Not covered: borrowed views (SsaDefs<'a>, MemoryEffect<'a, T>), transient
    pass machinery (builders, editors, edit reports), and SsaFeatureToken — its
    &'static str opcode can serialize but cannot deserialize, so a derive there
    would be a one-way trap.

  • num_enum::{IntoPrimitive, TryFromPrimitive} on seven kind enums. Note:
    this pins those enums' numeric discriminants as public API — inserting a
    variant mid-enum silently remaps any value a host persisted by discriminant.
    The serde derives encode by variant name and are not affected.

  • New public helpers: SsaVarId::as_u32, SsaInstruction::set_op_preserving_type,
    SsaOp::{visit_operands, visit_operands_mut, replace_uses_with, arith_signedness, compare_kind, memory_effect}, and a fp_classify builder.

  • ConstValue scalar folds recurse lane-wise into Vector operands (previously
    returned None).

  • SsaEditor::replace_uses_checked_with — replace instruction uses against a
    caller-supplied dominator tree, letting a pass that rewrites many variables
    (e.g. GVN) build the tree once and reuse it across the whole batch instead of
    per replacement.

  • BitSet::{contains_checked, insert_checked} — bounds-tolerant accessors that
    treat an out-of-range index as unset rather than panicking, for callers whose
    index comes from data that may legitimately name a position outside the set
    (e.g. a terminator referencing a block that was never recovered).

  • The lattice traits are re-exported from analysis::dataflow
    (MeetSemiLattice, JoinSemiLattice, Lattice), matching every other trait
    the module documents. They were previously reachable only through
    analysis::dataflow::lattice::, so the path the docs told you to use did not
    compile.