Repository navigation
v0.3.0
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::Convis 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)IntConvIdentical field set — mechanical rename. Conv(int → ptr)IntToPtrNo overflow_check/unsigned.Conv(ptr → int)PtrToIntNo overflow_check/unsigned.Conv(int → float)IntToFloatKeeps overflow_check/unsigned.Conv(float → int)FloatToIntKeeps overflow_check/unsigned.Conv(float → float)FloatConvWidth change only; drops both flags. PtrAddis a new address-computation op, not aConvreplacement. -
SsaOpgained 55 variants (146 → 200), andVectorUnaryKind(+20),
VectorBinaryKind(+23),VectorCompareKind(+8),VectorTernaryKind(+3),
AtomicRmwOp(+3), andSsaOpClass(+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. -
ConstValueequality is now structural, and the type isEq + Hash.
Float arms compare and hash bitwise, which makesEqreflexive and lets
constants be hash-map keys. Two visible consequences:F64(NAN) == F64(NAN)is
nowtrue, andF64(0.0) == F64(-0.0)is nowfalse(they are
distinguishable at runtime via1.0 / x). IEEE-754 semantic comparison is
unchanged and still lives inceqand thec*_unfamily. -
SsaOpand its operand payloads are nowEq + Hash. NoTargetchange is
required — the trait already demandedEq + Hashon its associated types.
Fixed
-
VectorStructLoadReplicate(AArch64ld2r/ld3r/ld4r) was classified
Puredespite 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 asRead+TrapClass::MemoryFaultand reportsmay_throw(), like
every other vector load. -
GVN could merge distinct
ComputeFlagscomputations.ComputeFlagsmodels
the flags ofbsf/bsr/popcnt/btalike 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
CallClobbermarkers, 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.Phiis likewise excluded — it is block-relative, and the key
does not encode the defining block. -
SsaEditor::nop_instructionleft the removed value'sresult_typestamped
on theNop.set_op_preserving_typenow clears the type when the new op
has no destination. -
SystemOpKind::Barrier(dsb/dmb/isb/mfence) was classifiedWrite.
It is an ordering construct, not a clobber: it now classifies asFence, so
Memory SSA emits aMemoryOp::Barrierand the verifier's fence invariant
applies. -
setffr/wrffrwere deletable by DCE. They write the SVE first-fault
register, which is not an SSA operand, and theiroutputsmay be empty — pure
plus zero definitions meant DCE removed them, dropping the FFR initialization a
following first-faulting load depends on. They now reportOpaqueeffects. -
Pointer conversions sign-extended when folded.
IntToPtr/PtrToInt
hardcoded a signed source, so a 32-bit0x8000_0000would widen to
0xFFFF_FFFF_8000_0000on a 64-bit target. Pointers are unsigned and now
zero-extend. (Latent: no in-treeTargetimplementsconvert_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) andblockmerge(trampoline elimination) now preserve
canonical loop preheaders and unified latches instead of removing them as empty
forwarding blocks — whichloopcanonimmediately re-inserted, so the
normalization fixpoint never settled. Loop-simplify form is now stable (the
same trade-off LLVM'ssimplifycfgmakes).
Performance
- GVN's generic value key is now structured rather than a formatted string.
It stores the operand-normalizedSsaOpand probes via derivedEq/Hash,
removing a per-candidate deep clone, aDebugrender, and aString
allocation, and turning every map probe from a string compare into a
structural one. - LICM
can_hoistis 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. -
serdefeature (off by default): serializes the IR data model — the
operation-kind taxonomy, the native descriptors, and the SSA graph itself
(SsaFunctionand everything reachable from it: blocks, instructions, ops,
phis, variables, constants, exception handlers).Generic IR types carry
#[serde(bound(...))], soSsaFunction<T>is
serializable exactly when the host'sTargetassociated types are. Hosts that
don't serialize the IR are unaffected: the impl does not apply to them, and no
serde bound is forced ontoTarget.Two encoding decisions are part of the wire contract:
SsaVarIdserializes as its logical index, not its internal
complement encoding (anOption-niche layout optimization that must not leak
into a persisted format).- Maps keyed by
VariableOriginencode 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), andSsaFeatureToken— its
&'static stropcode 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.
Theserdederives 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 afp_classifybuilder. -
ConstValuescalar folds recurse lane-wise intoVectoroperands (previously
returnedNone). -
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.