Repository navigation
Releases: ATRAPSLLC/analyssa
Release list
v0.6.0
Structured control-flow recovery arrives: any control-flow graph becomes a tree
of statements, total by construction, with the exception model rebuilt
underneath it — a clause is now three typed block ranges instead of five loose
indices, and one exception-aware flow view replaces five hand-written copies of
the same loop. An address becomes a value the IR can carry, so a function,
global or import stays distinguishable from the integer it lowers to. And the
native taxonomy gains an identity per instruction where it had one per class,
with an operation table that keeps those identities injective.
The defects fixed alongside share a shape with the 0.5.0 set, one step further
out: a fact about the function asserted from metadata rather than established
against the IR as it stands. A leader assumed to be available everywhere it was
found. A phi identified by an origin that does not identify it. A block index
taken from a def site, a phi operand, or an exception table and used to index a
set sized for the current CFG.
Added
-
analysis::structure— structured control-flow recovery: loops classified
by where they test, multi-way branches, protected regions with their handlers,
and conditions spanning several blocks (Predicate), which is what turns two
nested branches back intoa && b. Total by construction, withRegion::Goto
as the fallback for edges no structured form expresses, so quality is a count
(StructureMetrics::resized_inputsanddepth_limited) rather than a
precondition. -
analysis::structure::BlockSetandStructureOptions::max_depth— the two
types that make the recovery total.BlockSetcarries the index domain it was
built for, socontainsis total over everyNodeId;max_depth(with
DEFAULT_MAX_DEPTH) bounds the recovery's stack and every later traversal of
the result, the derivedDropandCloneincluded, and caps how many blocks
one condition may fold. Reaching either costs quality, never correctness. -
ProtectedHandlerKindandHandlerFilter— the input side of a handler,
split fromHandlerKind, which now describes only a recovered one. A filter
expression ridesProtectedHandlerKind::Filter, so a filter without an
expression and an expression on afinallyare unrepresentable. -
analysis::recovery— the bridge from anSsaFunctionto that recovery.
structure_ssareads the exception table intoProtectedRegions, derives
condition-only blocks from instructions, and returnsRecovered: the tree
and the clauses it could not convert (RejectedClause,ClauseRejection).
Clauses sharing a try range become one region with several handlers, so
try/catch(A)/catch(B)does not lose a catch. -
compute_post_dominators,control_dependencesandReverseGraphin
graph::algorithms, withPostDominatorTreeandControlDependences— the
results are indexed by the original nodes, over a reversed graph rooted at a
virtual exit so a function with several returns, or none, still has one.
immediate_post_dominatoris the single bounds guard, andstrict_controllers
is the answer a doc sentence could not give: under Ferrante-Ottenstein-Warren a
loop header lies in its own post-dominance frontier, so walkingcontrollers
as a parent chain does not terminate.DominatorTree::is_reachableexposes
reachability the tree already knew, instead of a second BFS. -
analysis::exceptions— one exception-aware flow view, replacing five
hand-written copies of one loop over the exception table:FunctionRoots
(where control can enter),EhCfg(the graph in which those are reachable) and
EhDominance. The last keeps apart three questions that were one answer used
for all of them —definition_reaches, which knows a throw can interrupt a
guard part-way,dominates_block, and the verifier's floor
definition_is_well_formed. -
ExceptionBlocksandSsaFunction::exception_blocks— the block roles an
exception table assigns (is_runtime_entry,is_region_start,
is_region_end), indexed once instead of re-derived from five loose
Option<usize>fields at eleven reader sites. It borrows the function, because
an owned snapshot threaded through a mutating fixpoint goes stale unnoticed. -
ir::exception::BlockRange, and one exception clause as three of them. A
clause part is a non-empty half-open block range, and the pairing is the type:
a start without its end is not a valueBlockRangecan hold.
SsaExceptionHandlercarriesprotected_range,handler_rangeand
filter_range, each anOption, which is the whole of a clause's legitimate
partiality.partsandentry_blocksare the total views the barriers use;
layoutis the checked one, yielding aClauseLayoutandLaidOutHandleror
anExceptionTableError.ClausePartnames which part a range belongs to,
andBlockRange::to_bitsetis the crate's only interval-to-set expansion. -
ir::exception::HandlerKindandTarget::handler_kind— one handler
taxonomy and one classifier, re-exported fromanalysis::structureso the enum
the recovery reports and the enum the IR classifies are the same enum. -
ExceptionTableErrorandSsaVerifier::check_exception_table, at
VerifyLevel::Quick— reportsVerifierError::MalformedExceptionClausefor
a clause whose parts run past the last block, claim each other's blocks, or
disagree about being a filter.EmptyBlockRangeis the deserialization-side
refusal of a pair covering no block, the one path no constructor guards. -
ConstValue::Symbol(T::SymbolRef), withTarget::SymbolRefand
Target::symbol_address— a located or named entity used as a value. It
folds exactly as the other reference constants do, which is to say not at all:
address arithmetic stays the address model's job.SymbolRefis distinct from
MethodRef, which names a callable, andsymbol_addressdefaults toNone,
so hosts whose symbols are metadata tokens need write nothing. -
passes::algebraiccollapses redundant integer conversion chains. A lift
emits one conversion per width change, so a value narrowed, widened and
narrowed again arrives as(uint16_t)(uint64_t)(uint32_t)x— 40.8% ofx86_64
conversions took another conversion as their operand. An intermediate is
unobservable when the result is no wider than it and no wider than the
source; where the result is wider, the two are interchangeable exactly when
they agree on sign or zero. A conversion that checks overflow is never removed.
Over the committed fixtures,x86_64integer conversions fell 26.7% and
arm_6415.8%; three-deep casts in rendered source fell 64%. -
analysis::convert::collapse_conversion_chain— that rule as a documented,
testable analysis instead of a fast path buried in a pass, with an oracle
matrix over all 2048 three-link shapes the mock target can express.
testing::MockConvLinkandMockConversionChainbuild a chain of any depth
over any mock integer width, laid out so a test can damage one def site
deliberately. -
SsaFunction::recorded_definitionandRecordedDefinition— the crate's
one guarded, scan-free def-site dereference. A def site is variable metadata:
an edit that does not restate it leaves it naming whatever instruction now sits
at that index, so dereferencing it unchecked answers with a foreign operation
rather than failing.get_definition,get_definition_instructionand
try_constant_valueare built on it. -
analysis::cache::FunctionAnalyses,scheduling::AnalysisCacheand
CacheStats— the derived analyses of one function, each computed at most
once and only if asked for, and a per-method cache of those that outlives the
visit which built it. The handle borrows the function and hands that borrow
back throughir(), so pairing two functions' results by mistake fails to
compile; the map lock is released before a caller touches an entry, so an
analysis never runs under it. -
Smaller additions.
SsaOp::first_use(the first operand without theVec
thatuses()allocates);PointsTo::empty, aconstempty relation a
consumer can borrow;SsaEditor::replace_phi_predecessor_group_for_result, the
origin-keyed rewrite keyed by the phi's result instead;
SsaFunction::demote_runtime_entry_phis, which removes phis from blocks no
terminator transfers to;SsaCfg::predecessor_blocksand
to_predecessor_sets;SsaBlock::control_terminator, the crate's single
definition of "the terminator"; andMemorySsa::classify_memory_operation,
now public, since "does this touch memory, and where" is a question a consumer
can have without wanting the versioning built on top of it. -
A payload on every many-to-one system kind —
CacheMaintenanceOp(13
instructions),TlbMaintenanceOp(13),BarrierOp(3),HypervisorOp(32),
HardwareEngineOp(12),InterruptReturnOp(13),BreakpointOp(17),
TrapOp(16) andSysRegOp(72), carried by the matchingSystemOpKind
variants, by the newSystemOpKind::ControlRegister, and bySsaOp::Break.
Five of these classes had been rendering a representative member's mnemonic
for the whole class, soclflushread asinvdandxendasserialize—
worse than a generic label, because it looks correct.SsaOp::Breakand
SystemOpKind::Trapnow distinguishud2from a breakpoint trap and
int 0x80fromint 0x2d, andSsaBlockBuilder::break_takes the
BreakpointOpas a required argument. -
MachineStateOpandSystemOpKind::MachineState, replacing
SystemOpKind::Privileged— one variant per operation across 138 instructions,
from descriptor tables and port I/O through the CET shadow...
v0.5.0
Correctness of the SSA rebuild and the phi transforms. Every defect fixed here shares a shape: a value substitution or a definition record that was stated rather than established — a chain composed without resolving to a surviving target, a label asserting a definition that did not exist, or a rewrite recorded before its operands were known good.
On a 125 MB x86-64 reference binary (50,000 functions), pass rollbacks went from 6,094 to 0 and verifier-reported undefined uses from ~28,960 to 0; no function is now floored by normalization. Output is 6% leaner (10,089,320 to 9,490,820 lowered rows) while landing 4% more rewrites (94,257 to 98,467), because rejected work is no longer discarded wholesale.
Bumps serde to 1.0.229.
Ownership
- Recorded ATRAPS LLC as copyright holder and added a
NOTICEfile. The Apache-2.0 appendix was never filled in — it still carried the literal[yyyy] [name of copyright owner]placeholder. - Added a
repositoryfield. The manifest declareddocumentationbut no repository, so crates.io showed no source link for any published version. - Dropped the deprecated
authorsfield. - Publishing now uses crates.io trusted publishing instead of a stored registry token, and refuses to publish a release whose commit is not contained in
main.
First release under ATRAPS LLC ownership.
v0.4.1
Speculative evaluation for SsaEvaluator. Consumers that explore alternative
executions — a symbolic executor taking both arms of a branch, a control-flow
deobfuscator tracing every path through a dispatcher — previously had to clone
the evaluator per fork, paying for everything learned so far on every fork. A
checkpoint costs nothing to take and one journal entry per mutation to undo, so
the cost tracks the speculation rather than the accumulated state.
Added
SsaEvaluator::checkpointandSsaEvaluator::rollback, with the
EvaluatorMarktoken they exchange (re-exported fromanalysis) — mark a
state, evaluate speculatively, and restore exactly what was marked. Marks
nest, and rolling back to an outer mark subsumes any inner ones, so an
abandoned branch is discarded in one step rather than unwound level by level.
Rolling back a stale mark leaves the state unchanged rather than corrupting
it. Journaling is latched on by the first checkpoint, so evaluators that never
speculate are unaffected; whentrack_memoryortrack_pathis on, the mark
additionally snapshots those two structures outright instead of journaling
them.
Performance
- Trivial-phi elimination in rebuild mode is no longer quadratic. The
rebuild path substituted each trivial phi one at a time, scanning the whole
function per substitution, and a rebuild produces trivial phis in proportion
to the function — so every fixpoint round scaled with the square of the
function. The substitutions are now composed into a single map (back-to-front,
which reproduces the order-sensitive sequential result exactly) and applied in
one pass. The repair path had already been corrected for this in 0.4.0; this
brings the rebuild path in line.
v0.4.0
Memory optimization arrives: a Memory SSA-backed pass doing store-to-load
forwarding, redundant load elimination, and dead store elimination, together
with the alias machinery it needs — points-to and address analyses, address
spaces on indirect accesses, and a rewritten MemoryLocation::Indirect that
keys on the decoded address rather than the SSA value holding it. Alongside it:
an interprocedural summary framework, a transactional pass-group primitive
(scheduling::PassTransaction), and a widening hook that gives the generic
dataflow solver a termination argument for infinite-height domains.
Normalization is substantially faster on large functions. Profiling a real
17,284-function binary found nearly all of its lift time in one 27,384-block
function, spent on work that scaled with the function rather than with the
change being made. Fixing that takes the binary's lift from 1014.5 s to
34.6 s. Separately, trivial-phi elimination went from cubic to quadratic —
26 s to 48 ms on a 1,600-phi chain.
Added
- Memory optimization pass (
passes::memory,MemoryOptimizationPass) —
store-to-load forwarding, redundant load elimination, and block-local dead
store elimination, every rewrite gated on an alias proof rather than a
syntactic shape. Calls, fences, atomics, volatile prefixes, and opaque native
operations are barriers. - Points-to and address analyses (
analysis::pointsto,analysis::address)
— an inclusion-based (Andersen) constraint solver with a field-sensitive
intraprocedural extractor, and a normalizedbase + index*stride + offset
address model with a cell-identityAliasKeyprojection. Target::field_member_index— optional host accessor giving field-sensitive
points-to a stable per-field cell identity.- Address spaces on indirect accesses.
SsaOp::{LoadIndirect, StoreIndirect}
carryaddress_space: Option<u16>, and alias analysis treats distinct spaces
as disjoint — covering x86FS:/GS:. - Interprocedural summary framework (
interproc) —CallGraphover an
opaque method key plussolve, driving a caller-suppliedSummaryTransfer
over the call graph's SCCs, callees before callers, with a bounded fixpoint on
recursive components. - Transactional pass execution (
scheduling::PassTransaction) — runs a pass
group against one reusable snapshot, verifies once, and rolls back on verifier
rejection or panic, re-running the group pass-by-pass to isolate the culprit.
Offered to hosts driving their own pass loop;PassSchedulersnapshots per
pass underverify_hardand does not use it. EventKind::LoadForwardedandEventKind::DeadStoreRemoved, with matching
event-builder support.DataFlowAnalysis::widen— a widening hook the solver applies between
iterations, giving infinite-height domains a termination argument.ValueRangeimplementsMeetSemiLatticeandJoinSemiLattice, so range
domains compose with the generic solver.VariableOrigin::EntryLiveIn— a value the caller supplies whose position
in the signature is not known. The other four variants describe a CIL method,
which has a declared signature and local table; a machine-code front end has
neither, so a register read before the body ever writes it is neither an
argument at a known index nor a declared local. Front ends previously had to
stamp such a valueArgument(0), which asserted a signature position the
lifter has not recovered — and, atrebuild's origin-keyed grouping, collapsed
every live-in of a function into argument group 0.is_entry_definedaccepts
the new variant, so a read of one needs no definition in the body;Phistays
rejected, so a variable whose definition was destroyed still cannot masquerade
as an entry value. ABI recovery may later reclassify one as a real
Argument(n).VariableOrigin::is_entry_live_inandis_caller_supplied— the latter
coversArgumentandEntryLiveIntogether, which is the predicate consumers
actually want when asking "does the body have to define this?".- The entry-defined origin set is now a tested public contract
(tests/verifier.rs), not a verifier implementation detail. Tightening it
previously broke a downstream lifter with no failure in this suite. - Fuzz targets (
fuzz/) overSsaFunction::validateand the pass pipeline,
fed deliberately malformed IR. Wired into CI as a smoke gate.
Changed (breaking)
VariableOrigingained a fifth variant (EntryLiveIn, see Added). The
enum is not#[non_exhaustive], so any exhaustivematchon it downstream
needs a new arm; a_arm compiles unchanged, but check whether it should
instead join theArgumentarm — the question to ask is whether the site cares
about caller-supplied (useis_caller_supplied) or about a signature
index. All in-crate matches were audited:rebuild's group derivation and
version-stack seeding treatEntryLiveInas naming no argument/local slot, and
deadcode'sLoadArg/LoadLocalliveness bridge excludes it, since a
machine-code front end emits neither op. Serde encoding is additive but not
backward compatible in the reading direction: a 0.3.0 deserializer rejects a
payload containing the new variant.- Removed the block-duplication methods on
SsaFunction<T>—
allocate_fresh_variables_for_block,clone_block_with_remap,
duplicate_block,remap_block_targets. Shipped in 0.2.0 with no caller in
this crate or any consumer, and never executed by a test. Adding tests showed
why that mattered: they draw ids from the variable allocator without appending
the matching rows, breaking thevariables[i].id().index() == iinvariant that
SsaFunction::variableindexes on. Reimplement against the invariant when a
pass needs it; the code remains in history at2aaa81b. - The address model is now target-pointer-width aware.
normalize_address,
alias_keys_for_function,MemorySsa::build, andpasses::memory::runtake a
PointerSize, andIndirectLocationcarries one. Address arithmetic wraps at
the pointer width, and without it a sign-extended-8and a zero-extended
0xFFFF_FFF8— the two lowerings a 32-bit frontend emits for one displacement
— decoded 4 GiB apart, somay_aliasproved two accesses to one cell
disjoint. PassSsaPassHost::ptr_size();PointerSize::Bit64reproduces the
previous behaviour exactly. SsaPassHost::events()now returns&dyn EventListener<T>instead of
&EventLog<T>, so a host can supplyNullListenerrather than retaining every
event for the process lifetime.EventListenergainedrecorded_count()and
count_by_kind_since(), both defaulting to "no history".&self.eventsstill
coerces.ConstValue::to_bytestakes aPointerSizeand emits pointer-width bytes
forNativeInt/NativeUIntrather than always eight.- Removed
SsaEditScope::StructuredCfg. It ranrebuild_ssaexactly as
CfgModifyingdoes while documenting a cheaper boundary it never had. Use
CfgModifying. SsaOp::LoadIndirectandSsaOp::StoreIndirectgained anaddress_space
field. Exhaustive constructions must supply it;Nonepreserves previous
behaviour. Patterns using..are unaffected.MemoryLocation::Indirectnow carries anIndirectLocation(decoded
base/index/stride/offset plus access width and address space) instead of a bare
SsaVarId. See Fixed.World::methods_reverse_topologicalnow returnsVec<Vec<T::MethodRef>>—
the call graph's SCCs in reverse topological order rather than a flat list. The
grouping is load-bearing: a non-recursive component converges in one visit
while a recursive one needs a bounded fixpoint, and a flat list cannot express
that. The default now derives the order fromcalleesinstead of falling back
toall_methodswith no ordering guarantee.- The crate is now edition 2024 (from 2021).
rust-versionis unchanged at
1.88. ir::opsis now a directory module, split intodef,kinds,vector,
native,effects,visit,control,classify, anddisplay. Additive
only: every type and method is re-exported fromir::ops, verified by diffing
the public surface (108 types, 80 methods) before and after.
Fixed
Miscompiles. Passes could produce wrong optimized IR, silently, in release
builds, on shapes that are ordinary for lifted native code:
- Secondary (
flags) definitions were dropped.BinaryOpInfopresents a
two-definition op through a single-destview andvalue_key()omitsflags,
so GVN deleted an instruction whose flags a laterBranchFlagsstill read.
Strength reduction and reassociation built replacements withflags: None.
All three now check liveness of the flags definition — a dead one still
optimizes, a live one declines the rewrite. - Memory SSA versioned only the location a definition named, so a barrier or
an overlapping store left a may-aliasing cell's version untouched and the
memory pass forwarded a value that had already been destroyed. Versioning now
covers the may-alias closure, in both phi placement and renaming. - Range propagation proved
x & negative_maskwas the constant 0, folding
live branches away;Andnow delegates toValueRange::and_constant. - A phi folded to a constant when its first operand was phi-defined, deleting
the opposite arm. Self-referential loop phis now fold correctly too. - Reassociation overwrote a shared
Constin place, corrupting every other
reader; the use-count guard checked the intermediate result, not the constant. - Chained shifts combined past the operand width, where masking semantics
make two shifts unequal to one. - **SCCP tre...
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...
v0.2.0
[0.2.0] - 2026-06-03
Major release: a target-agnostic native SSA substrate for modern lifters,
new construction/editing infrastructure, and a broad performance/memory pass.
A few IR enum shapes changed (see Changed).
Added
- Native SSA substrate: target-independent pointer sizes & endianness;
first-class SIMD/vector operations with target-independent lane/vector-type
semantics; native atomics (exchange, lock-RMW, compare-exchange); native
opaque operations with machine-state clobbers; multi-output SSA definitions;
boolean ops and native condition helpers; native flag semantics; and implicit
wide (low/high, quotient/remainder) arithmetic. - Memory effect summaries, exception/interrupt support, and native operation
classes plus target-generic feature tokens. - Fluent SSA builder (
ir::function::builder) and a checked, verifier-preserving
SSA editor (ir::function::editor); all built-in passes migrated onto the
checked mutation APIs. - Recommended normalization-pipeline API, pass bisection/debug hooks, and
structured verifier diagnostics. - Expanded
Targettrait (vector descriptors, pointer sizes, endianness),
MockTarget, and a much larger test suite (builder, scheduling, verifier,
pipeline, and canonicalization coverage). - Allocation-free helpers:
SsaOp::{uses_var, for_each_successor, has_successor},
SsaInstruction::{uses_var, for_each_variable},SsaBlock::for_each_successor,
SymbolicExpr::for_each_variable,SsaFunction::compute_predecessors,
BitSet::is_full,EventLog::into_events,EventListener::is_enabled.
Changed (breaking)
SsaOp::NativeOpaqueis now a tuple variant wrapping a boxed payload
(NativeOpaque(Box<NativeOpaqueData>)) instead of an inline struct variant.ConstValueheap-bearing arms are now boxed:Vector(Box<[ConstValue<T>]>),
DecryptedString(Box<str>),DecryptedArray(Box<DecryptedArrayData<T>>).SsaOpandConstValuegained many new variants for the native substrate;
exhaustive downstream matches must handle them.
Performance & memory
- Shrunk core IR types ~3–4×:
SsaOp168→40 B,SsaInstruction176→48 B,
Option<SsaVarId>16→4 B (niche-encodedSsaVarId),ConstValue40→24 B,
PhiOperand16→8 B; guarded by asize_ofregression test. - Removed hot-loop allocations (
uses()/successors()purge, word-skipping
BitSetiterator, DFS/postorder scratch reuse, cached solver exit set). DefUseIndexuses denseVec-indexed storage instead ofBTreeMap.- De-quadratized DCE, GVN removal, jump-threading safety check, LICM invariant
detection, predicate branch evaluation, trivial-phi predecessor build, and
block-merge redirection; dataflow solver uses an RPO-priority worklist. DirectedGraphstores neighbor ids inline,IndexedGraphdedups edges in
O(1), and cycle detection is iterative (stack-safe on deep graphs).- Scheduler dirty-set membership is O(1); logging allocates nothing under
NullListener.
v0.1.0
- Target-agnostic SSA IR for blocks, instructions, phi nodes, variables, values, exception handlers, and functions.
- SSA analyses for CFGs, constants, liveness, memory, phi placement, symbolic expressions, dataflow, def-use, loop structure, and verification.
- Optimization and deobfuscation passes including algebraic simplification, block merging, control-flow cleanup, copy propagation, dead-code elimination, GVN, LICM, loop canonicalization, reassociation, scheduling, strength reduction, and jump threading.
- Generic graph and bitset utilities used by the IR and analyses.
- Host adapter traits and a pass scheduler for integrating analyssa into target lifters without tying the crate to one instruction set or metadata model.