Skip to content

Refactor stackify - #299

Merged
sbillig merged 14 commits into
fe-lang:mainfrom
sbillig:refact-stackify
Jul 12, 2026
Merged

Refactor stackify#299
sbillig merged 14 commits into
fe-lang:mainfrom
sbillig:refact-stackify

Conversation

@sbillig

@sbillig sbillig commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator

Refactor the EVM stackify pipeline to make its core state and interfaces explicit, remove duplicated bookkeeping, and close several latent correctness gaps.

  • one immutable allocator interface based on stored pre/post/case actions;
  • one source of truth for spill storage;
  • a dedicated block planner and explicit block-entry state machine;
  • structured trace events instead of placeholder substitution;
  • one ValueId identity per immediate stack word;
  • ordinary use tracking for cached immediates.

Simplify allocator and storage boundaries

  • Replace read/write/read_br_table_case with borrowed pre_inst/post_inst/br_table_case action lists.
  • Rewrite final allocator actions once during construction instead of on every lookup.
  • Remove the unused edge-traversal API.
  • Make spill_storage the single stored representation of scratch, object, and exact-local spill locations.
  • Group memory-planning state behind a smaller MemPlan construction boundary.

Reorganize stackify planning

  • Consolidate instruction simulation into BlockPlanner.
  • Move use tracking and reachability rescue into dedicated modules.
  • Replace the parallel template, inherited-stack, pending-edge, and planned-block tables with an explicit EntryTable.
  • Model block entries as function entry, opaque terminal chain, inherited single-predecessor entry, or merge entry with a Pending -> Frozen lifecycle.
  • Encapsulate the internal-call operand rotation, continuation push, and final swap behind prepare_internal_call.

Harden control-flow handling

  • Run StackifyEdgeSplitter from the machine preparation pipeline.
  • Split retreating multiway edges that target an already-planned block.
  • Preserve the real function entry when splitting an edge targeting the entry block.
  • Keep br_table base pre-actions separate from per-case comparison actions.
  • Conservatively disable lazy-frame injection whenever either base or case actions touch frame state, including frame-address materialization.
  • Add regressions for entry self-loops and br_table lazy-frame action handling.

Simplify immediate identity

  • Canonicalize numerically equal immediates to one representative ValueId.
  • Choose the cheapest materialization representative, then the lowest value id for deterministic ties.
  • Remove the parallel immediate semantic-equality helpers.
  • Fold cached-immediate bookkeeping into ordinary per-block use tracking.

Simplify tracing

  • Replace string placeholders with structured trace events.
  • Make rollback a vector truncation.
  • Store deferred actions structurally and remap stack-object ids before rendering.

sbillig added 12 commits July 11, 2026 10:13
Delete the never-called traverse_edge; rename read/write/read_br_table_case
to pre_inst/post_inst/br_table_case and drop the ignored operand params;
take &dyn Allocator instead of &mut dyn; rewrite FinalAlloc action lists
once at construction so lookups return borrows; factor the spill-storage
store-action mapping into SpillStorage::store_action.
Delete the spill_obj and scratch_slot_of_value projection fields; derive
them via accessors (spill_obj, scratch_slot, object_spills) with a single
set_spill_object mutation path. The provisional per-iteration object-id
map moves out of StackifyAlloc into the planning driver. The map-drift
assertions in validate_spill_storage become unrepresentable.
- Replace three confirmed-unreachable branches with debug asserts:
  the dead-value defensive loop in clean_dead_stack_prefix, the
  dominated-pred arm in choose_transfer, and the plan_block prologue
  fallback; clear_inst_actions becomes an emptiness assertion.
- Express StackifySearchProfile knobs as SearchBudgets const tables.
- terminal_chain_blocks becomes a BitSet.
- Drop the inst collect in run_block_sim (iterate the layout directly).
- Share the storage-eligibility predicate between MemPlan and the
  builder's entry-arg pre-pass.
- Document the SWAP-chain cost of the stable SymStack ops; drop the
  imm_materialization_code_len alias.
The machine pipeline ran only CriticalEdgeSplitter, so a multiway
self-loop on the entry block tripped a debug assert (or silently
dropped the edge fixup in release). Run StackifyEdgeSplitter in
prepare_machine_stackify_analysis instead, and:

- Fix a latent CfgEditor::split_edge bug: inserting the split block
  before an entry-block destination silently made it the new entry,
  recreating the multiway-edge-to-planned-block problem.
- Narrow the splitter from all in-cycle multiway edges to retreating
  ones (dom-RPO rank[to] <= rank[from]) — exactly the planner's assert
  condition — so existing corpus bytecode is byte-identical.
- Move stackify_edge.rs to stackalloc/edge_split.rs; document the
  precondition; upgrade the planner guards to release asserts.
- Add an end-to-end regression test for the entry self-loop shape.
StackifyTrace records structured TraceEvents instead of interleaving
placeholder markers into one string and substituting at render time.
Rendering is a single forward walk; checkpoint/rollback is a vec
truncate; deferred-exit actions fill in by event index; object-id
remapping rewrites raw Action payloads in events. The observer trait
loses both associated types. Rendered output is byte-identical.
Emit and the lazy-frame analysis read a br_table's pre-actions via
pre_inst like every other instruction; brtable_actions now hold only
per-case compare preparation. This removes the take/prefix threading
through PlannerActionSink. The lazy-frame planner bails to the
always-active frame when br_table base pre-actions touch the frame,
since emit replays their action indexes once per case.
Planner::prepare_internal_call now owns the operand-rotation +
continuation-push + single-SWAP sequence with the ABI documented in
one place; the rotation site and SymStack helpers cross-reference it.
remove_call_ret_addr's linear marker scan becomes pop_call_ret_addr,
asserting the continuation is on top (it is, by construction).
- UseTracker (uses.rs) owns per-block use counting, cached-immediate
  tracking, and last-use/preserve queries; BlockLiveSets is gone.
- rescue.rs owns dead-prefix cleanup and reachability-rescue
  heuristics; the ReachabilityValues wrapper is gone.
- MemState groups the driver's memory-planning borrows; MemPlan::new
  drops from nine args to four and with_planner constructs it once.
- BlockPlanner (block.rs, was block_sim.rs) owns the per-block walk
  with one method per terminator kind; observer calls live at one
  layer; edge bookkeeping stays on the driver as record_*_edge. All
  four take_stack dances are gone.
- IterationPlanner is renamed FunctionPlanner and lives in driver.rs.
New entry.rs owns the per-block EntryKind classification (function
entry / terminal-chain opaque / single-pred inherited / merge) and the
Pending -> Frozen entry lifecycle. EntryTable::record_edge is the one
edge-dispatch point and EntryTable::freeze_entry the one transfer-
selection point; frozen templates live in the entry state. This
replaces the templates / inherited_stack / pending_edges /
planned_blocks / terminal_chain side tables, and double-freezing a
template is now structurally unreachable instead of silently ignored.
plan_block becomes a short dispatch over ResolvedEntry.
Stackify already treats equal as_i256 words as interchangeable stack
items (rename_immediate_slots_to_match, semantic suffix matching).
Give each distinct word one representative ValueId via the existing
value-alias mechanism, choosing the class member with the cheapest
materialization (then lowest id) so a class never pushes worse bytes
than its best member. This is stage 1 of folding the parallel
immediate-identity machinery into ordinary ValueId equality.
With one canonical ValueId per immediate word, plain equality subsumes
the word-level matching: delete rename_immediate_slots_to_match,
common_suffix_len_semantic, operand_prep_item_matches_arg, and
unary_operand_prep_find_arg; plan-replay mismatches still roll back to
the greedy fallback, and flush_rebuild asserts exact equality.
With one canonical ValueId per immediate word, UseTracker's parallel
I256-keyed cached_imm_remaining map merges into the ValueId-keyed
remaining map: cached immediates enter live_future until their last
in-block use, cleanup_live reduces to live_future, and cache_preserve
becomes the plain not-a-last-use rule. Rescue keeps immediates
evictable regardless of liveness, preserving its prior behavior.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1e9fcdcd7d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/codegen/src/stackalloc/edge_split.rs Outdated
@sbillig

sbillig commented Jul 12, 2026

Copy link
Copy Markdown
Collaborator Author

@codex review

@sbillig
sbillig merged commit 55ca888 into fe-lang:main Jul 12, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant