Skip to content

Releases: ATRAPSLLC/analyssa

v0.6.0

Choose a tag to compare

@BinFlip BinFlip released this 05 Sep 03:16

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 into a && b. Total by construction, with Region::Goto
    as the fallback for edges no structured form expresses, so quality is a count
    (StructureMetrics::resized_inputs and depth_limited) rather than a
    precondition.

  • analysis::structure::BlockSet and StructureOptions::max_depth — the two
    types that make the recovery total. BlockSet carries the index domain it was
    built for, so contains is total over every NodeId; max_depth (with
    DEFAULT_MAX_DEPTH) bounds the recovery's stack and every later traversal of
    the result, the derived Drop and Clone included, and caps how many blocks
    one condition may fold. Reaching either costs quality, never correctness.

  • ProtectedHandlerKind and HandlerFilter — the input side of a handler,
    split from HandlerKind, which now describes only a recovered one. A filter
    expression rides ProtectedHandlerKind::Filter, so a filter without an
    expression and an expression on a finally are unrepresentable.

  • analysis::recovery — the bridge from an SsaFunction to that recovery.
    structure_ssa reads the exception table into ProtectedRegions, derives
    condition-only blocks from instructions, and returns Recovered: 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_dependences and ReverseGraph in
    graph::algorithms, with PostDominatorTree and ControlDependences — 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_dominator is the single bounds guard, and strict_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 walking controllers
    as a parent chain does not terminate. DominatorTree::is_reachable exposes
    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.

  • ExceptionBlocks and SsaFunction::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 value BlockRange can hold.
    SsaExceptionHandler carries protected_range, handler_range and
    filter_range, each an Option, which is the whole of a clause's legitimate
    partiality. parts and entry_blocks are the total views the barriers use;
    layout is the checked one, yielding a ClauseLayout and LaidOutHandler or
    an ExceptionTableError. ClausePart names which part a range belongs to,
    and BlockRange::to_bitset is the crate's only interval-to-set expansion.

  • ir::exception::HandlerKind and Target::handler_kind — one handler
    taxonomy and one classifier, re-exported from analysis::structure so the enum
    the recovery reports and the enum the IR classifies are the same enum.

  • ExceptionTableError and SsaVerifier::check_exception_table, at
    VerifyLevel::Quick
    — reports VerifierError::MalformedExceptionClause for
    a clause whose parts run past the last block, claim each other's blocks, or
    disagree about being a filter. EmptyBlockRange is the deserialization-side
    refusal of a pair covering no block, the one path no constructor guards.

  • ConstValue::Symbol(T::SymbolRef), with Target::SymbolRef and
    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. SymbolRef is distinct from
    MethodRef, which names a callable, and symbol_address defaults to None,
    so hosts whose symbols are metadata tokens need write nothing.

  • passes::algebraic collapses 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% of x86_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_64 integer conversions fell 26.7% and
    arm_64 15.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::MockConvLink and MockConversionChain build a chain of any depth
    over any mock integer width, laid out so a test can damage one def site
    deliberately.

  • SsaFunction::recorded_definition and RecordedDefinition — 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_instruction and
    try_constant_value are built on it.

  • analysis::cache::FunctionAnalyses, scheduling::AnalysisCache and
    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 through ir(), 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 the Vec
    that uses() allocates); PointsTo::empty, a const empty 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_blocks and
    to_predecessor_sets; SsaBlock::control_terminator, the crate's single
    definition of "the terminator"; and MemorySsa::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) and SysRegOp (72), carried by the matching SystemOpKind
    variants, by the new SystemOpKind::ControlRegister, and by SsaOp::Break.
    Five of these classes had been rendering a representative member's mnemonic
    for the whole class, so clflush read as invd and xend as serialize —
    worse than a generic label, because it looks correct. SsaOp::Break and
    SystemOpKind::Trap now distinguish ud2 from a breakpoint trap and
    int 0x80 from int 0x2d, and SsaBlockBuilder::break_ takes the
    BreakpointOp as a required argument.

  • MachineStateOp and SystemOpKind::MachineState, replacing
    SystemOpKind::Privileged — one variant per operation across 138 instructions,
    from descriptor tables and port I/O through the CET shadow...

Read more

v0.5.0

Choose a tag to compare

@BinFlip BinFlip released this 10 Aug 01:20

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 NOTICE file. The Apache-2.0 appendix was never filled in — it still carried the literal [yyyy] [name of copyright owner] placeholder.
  • Added a repository field. The manifest declared documentation but no repository, so crates.io showed no source link for any published version.
  • Dropped the deprecated authors field.
  • 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

Choose a tag to compare

@BinFlip BinFlip released this 26 Jul 16:49

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::checkpoint and SsaEvaluator::rollback, with the
    EvaluatorMark token they exchange (re-exported from analysis) — 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; when track_memory or track_path is 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

Choose a tag to compare

@BinFlip BinFlip released this 25 Jul 16:46

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 normalized base + index*stride + offset
    address model with a cell-identity AliasKey projection.
  • 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}
    carry address_space: Option<u16>, and alias analysis treats distinct spaces
    as disjoint — covering x86 FS:/GS:.
  • Interprocedural summary framework (interproc) — CallGraph over an
    opaque method key plus solve, driving a caller-supplied SummaryTransfer
    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; PassScheduler snapshots per
    pass under verify_hard and does not use it.
  • EventKind::LoadForwarded and EventKind::DeadStoreRemoved, with matching
    event-builder support.
  • DataFlowAnalysis::widen — a widening hook the solver applies between
    iterations, giving infinite-height domains a termination argument.
  • ValueRange implements MeetSemiLattice and JoinSemiLattice, 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 value Argument(0), which asserted a signature position the
    lifter has not recovered — and, at rebuild's origin-keyed grouping, collapsed
    every live-in of a function into argument group 0. is_entry_defined accepts
    the new variant, so a read of one needs no definition in the body; Phi stays
    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_in and is_caller_supplied — the latter
    covers Argument and EntryLiveIn together, 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/) over SsaFunction::validate and the pass pipeline,
    fed deliberately malformed IR. Wired into CI as a smoke gate.

Changed (breaking)

  • VariableOrigin gained a fifth variant (EntryLiveIn, see Added). The
    enum is not #[non_exhaustive], so any exhaustive match on it downstream
    needs a new arm; a _ arm compiles unchanged, but check whether it should
    instead join the Argument arm — the question to ask is whether the site cares
    about caller-supplied (use is_caller_supplied) or about a signature
    index
    . All in-crate matches were audited: rebuild's group derivation and
    version-stack seeding treat EntryLiveIn as naming no argument/local slot, and
    deadcode's LoadArg/LoadLocal liveness 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 the variables[i].id().index() == i invariant that
    SsaFunction::variable indexes on. Reimplement against the invariant when a
    pass needs it; the code remains in history at 2aaa81b.
  • The address model is now target-pointer-width aware. normalize_address,
    alias_keys_for_function, MemorySsa::build, and passes::memory::run take a
    PointerSize, and IndirectLocation carries one. Address arithmetic wraps at
    the pointer width, and without it a sign-extended -8 and a zero-extended
    0xFFFF_FFF8 — the two lowerings a 32-bit frontend emits for one displacement
    — decoded 4 GiB apart, so may_alias proved two accesses to one cell
    disjoint. Pass SsaPassHost::ptr_size(); PointerSize::Bit64 reproduces the
    previous behaviour exactly.
  • SsaPassHost::events() now returns &dyn EventListener<T> instead of
    &EventLog<T>, so a host can supply NullListener rather than retaining every
    event for the process lifetime. EventListener gained recorded_count() and
    count_by_kind_since(), both defaulting to "no history". &self.events still
    coerces.
  • ConstValue::to_bytes takes a PointerSize and emits pointer-width bytes
    for NativeInt/NativeUInt rather than always eight.
  • Removed SsaEditScope::StructuredCfg. It ran rebuild_ssa exactly as
    CfgModifying does while documenting a cheaper boundary it never had. Use
    CfgModifying.
  • SsaOp::LoadIndirect and SsaOp::StoreIndirect gained an address_space
    field.
    Exhaustive constructions must supply it; None preserves previous
    behaviour. Patterns using .. are unaffected.
  • MemoryLocation::Indirect now carries an IndirectLocation (decoded
    base/index/stride/offset plus access width and address space) instead of a bare
    SsaVarId. See Fixed.
  • World::methods_reverse_topological now returns Vec<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 from callees instead of falling back
    to all_methods with no ordering guarantee.
  • The crate is now edition 2024 (from 2021). rust-version is unchanged at
    1.88.
  • ir::ops is now a directory module, split into def, kinds, vector,
    native, effects, visit, control, classify, and display. Additive
    only: every type and method is re-exported from ir::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. BinaryOpInfo presents a
    two-definition op through a single-dest view and value_key() omits flags,
    so GVN deleted an instruction whose flags a later BranchFlags still read.
    Strength reduction and reassociation built replacements with flags: 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_mask was the constant 0, folding
    live branches away; And now delegates to ValueRange::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 Const in 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...
Read more

v0.3.0

Choose a tag to compare

@BinFlip BinFlip released this 17 Jul 01:42

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...

Read more

v0.2.0

Choose a tag to compare

@BinFlip BinFlip released this 04 Jun 02:00

[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 Target trait (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::NativeOpaque is now a tuple variant wrapping a boxed payload
    (NativeOpaque(Box<NativeOpaqueData>)) instead of an inline struct variant.
  • ConstValue heap-bearing arms are now boxed: Vector(Box<[ConstValue<T>]>),
    DecryptedString(Box<str>), DecryptedArray(Box<DecryptedArrayData<T>>).
  • SsaOp and ConstValue gained many new variants for the native substrate;
    exhaustive downstream matches must handle them.

Performance & memory

  • Shrunk core IR types ~3–4×: SsaOp 168→40 B, SsaInstruction 176→48 B,
    Option<SsaVarId> 16→4 B (niche-encoded SsaVarId), ConstValue 40→24 B,
    PhiOperand 16→8 B; guarded by a size_of regression test.
  • Removed hot-loop allocations (uses()/successors() purge, word-skipping
    BitSet iterator, DFS/postorder scratch reuse, cached solver exit set).
  • DefUseIndex uses dense Vec-indexed storage instead of BTreeMap.
  • 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.
  • DirectedGraph stores neighbor ids inline, IndexedGraph dedups 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

Choose a tag to compare

@BinFlip BinFlip released this 09 May 13:52
  • 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.