Skip to content

Refactor EVM memory planning - #298

Merged
sbillig merged 11 commits into
fe-lang:mainfrom
sbillig:refact-mem
Jul 11, 2026
Merged

Refactor EVM memory planning#298
sbillig merged 11 commits into
fe-lang:mainfrom
sbillig:refact-mem

Conversation

@sbillig

@sbillig sbillig commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator

Makes phase boundaries explicit and eliminates duplicated interprocedural analysis.

  • Extract placement search and separate semantic planning from machine lowering.
  • Share call-graph and pre-analysis results across allocation, heap, malloc, and free-pointer planning.
  • Simplify final-spill scoring and placement search while retaining debug cross-checks.
  • Clean up obsolete planner state.

sbillig added 11 commits July 11, 2026 07:12
Move the promote-vs-shadow local search (problem/state types, move
search, packing-entry builders, cost model) and its tests out of
memory_plan.rs, which now holds the plan data model and program-level
assembly only. No behavior change.
The inline test module was more than half of ptr_escape.rs. Also point
the PtrEscapeSummary doc at the production call-site derivation path
(escape_scan) instead of a test-only helper.
- scratch_plan.rs -> fixed_slots.rs: the module is about the fixed
  spill slots at 0x00-0x3f, not the arena scratch region. Rename the
  scratch_effects/touches_scratch family to fixed_slot_effects/
  touches_fixed_slots to match.
- isa/evm/provenance.rs -> ptr_provenance.rs: avoids the collision with
  the unrelated transform/aggregate/provenance.rs.
- static_chain_peak_words -> stable_chain_peak_words and
  StableMode::StaticAbs -> StableAbs: the call-surviving tier is
  consistently 'stable' now.
- EvmCall/EvmCallCode ret_offset -> ret_len: the operand is the return
  buffer length; delete the comment apologizing for the misnomer.

Snapshot diffs are the renamed dump labels only.
Add CallGraphSchedule (call graph + SCCs + callers-before-callees topo
order + condensation edges) to module_analysis and thread it through
the EVM backend, replacing eight independent CallGraph+SCC builds and
three duplicated topo_sort_sccs/build_scc_edges copies.

Rewrite the naive while-changed interprocedural fixpoints as single
passes over the condensation:
- compute_abs_clobber_words_with_extra: reverse-topo join over callees
- compute_return_escape_caller_clamp_words: forward-topo join over
  callers (cyclic SCCs fold their own members' clobber bounds)
- compute_entry_may_have_live_frame: forward-topo reachability from
  dynamic-frame functions
- mem_effects: shared join_over_callees

The duplicated stable-chain prefix layout in memory_plan.rs is also
unified into compute_stable_chain_layout.
Extend FuncPreAnalysis with the CFG, pointer provenance (real escape
summaries), and the deliberately conservative value-provenance variant
that heap bounds rely on. The bundle is built once per section, right
after critical-edge splitting; the source module is never mutated inside
the spill-reserve convergence loop, so every consumer now reads the
bundle instead of recomputing provenance and CFGs per call:

- static arena object collection, malloc escape kinds, transient
  mallocs, heap future bounds, arena-base reservation scan, free-ptr
  slot facts, malloc placement, private-static malloc planning, and the
  free-pointer floor dataflow (which previously rebuilt CFG+provenance
  inside three nested interprocedural fixpoints).

A debug assertion on instruction counts guards against future IR
mutations invalidating the bundle.
Add machine/heap_state.rs: a shared forward-dataflow engine over the
state of memory[0x40], instantiated by two passes — the exactness pass
(pre-placement; drives Fixed-vs-Heap and private-static candidacy) and
the numeric floor pass in free_ptr_floor (post-placement; its transfer
reads MallocPlacement, which is why one fixpoint cannot serve both).

- Delete ExactHeapBaseAnalysis and the duplicated
  inst_writes_free_ptr_slot helper.
- Hoist per-function heap facts (exactness + terminal-private mallocs +
  dyn-sp clamp need) into FuncHeapFacts, computed once and shared by
  malloc placement and private-static planning, which previously each
  recomputed both analyses per function.
- Rewrite compute_free_ptr_write_summaries as a condensation join
  (all callees are section-internal at this stage; asserted by the
  memory planner).
- Document (PARITY notes) two preserved conservatisms: calls kill
  exactness unconditionally, and the floor merge loses information at
  any block with predecessors. Both are precision, not soundness;
  relaxing them is a separate snapshot-visible change.
FuncMemPlan served four phases: built by the semantic planner,
retrofitted by reserve application, half-cleared by
machine_mem_plan_from_semantic, remapped by hand, then grown by final
spills. Split it:

- SemanticFuncPlan: the planner output (object/alloca locations, frame
  words, call-preserve keyed by source insts, malloc annotations). The
  malloc annotation fields no longer need clearing — they simply do not
  exist on the machine type.
- MachineFuncPlan: what lowering, final spills, and emission consume.
  Its only constructor, from_semantic(plan, func_map), forces the
  call_preserve source->machine key remap that was previously enforced
  by convention only (a missed remap silently dropped shadow saves).

Also delete confirmed-dead surface: ObjLoc::StackPinned (every consumer
panicked on it), the PreserveMode enum (collapsed into CallPreservePlan
- ShadowRuns was the only live variant), ProgramMemoryPlan::sccs,
CallSiteObjects::arg_count, and the semantic plan's always-empty
spill_obj together with heap_plan's dead value_spill_bound path (spills
exist only on the machine side since the stackify cutover).

abs_words_end_with_extra_reserve becomes
SemanticFuncPlan::abs_words_end_with_reserve with a doc note on how it
deliberately differs from abs_words_end for StableMode::None.
- add_words: one overflow policy for word-count arithmetic in the
  memory planner, replacing per-site checked_add(...).expect ceremony
  where the operands are word counts.
- align_up_to_word: one copy replacing the identical helpers in
  prepare.rs and final_spills.rs and backing lower.rs's malloc size
  alignment.
- expect_func_entry: total-map lookup replacing the repeated
  unwrap_or_else(|| panic!("missing ... for func")) incantations.
- prepare.rs's byte_ranges_overlap renamed to addr_len_overlaps_range:
  it is not a duplicate of final_spills' end-based helper — it takes a
  length and treats an overflowing end as overlapping (conservative);
  the review's duplication claim was wrong in detail.
choose_optional_placements evaluated each scratch-vs-stable choice for
optional final spills by re-running the entire program memory planner
(every function's placement search, malloc planning, and free-pointer
dataflows) - 2+N full replans per convergence iteration. Corpus
measurement (SONATINA_MEM_LOOP_STATS): 313 sections, 91% converge in
one iteration, 9% in two, none more; 21 sections carry optional spills
and paid the replan amplification.

The per-function placements are reserve-independent (reserves are
applied after the placement search), so a trial reserve only shifts
each function's frame words by its delta over the committed reserve.
The new scorer recomputes the section aggregates directly: scratch peak
(one max), stable chain peak (one pass over the condensation), arena
base (the loop-invariant fixed-reservation scan is now computed once
per section and combined with the per-trial facts).

Two second-order feedback paths are held at their base values
(private-static malloc extras; transient-malloc flips from new
fixed-slot effects). The old replanning scorer is kept in debug builds
as a cross-check oracle: SONATINA_SPILL_SCORE_XCHECK=1 compares every
evaluation - 150 evaluations across the corpus, zero mismatches.

This implements the sanctioned interim from the memory plans' R5; the
full late-layout rework (R1) is deliberately left for its own effort:
the measurement shows the convergence loop's cost is architectural
rather than compile-time, and R1's symbolic-immediate stages need
their own snapshot-reviewed landing sequence.
Corpus measurement (SONATINA_PLACEMENT_STATS, kept in-tree as a
debug-only oracle): of 1220 placement problems, 93% have zero
promotion candidates and every problem small enough to enumerate
(N <= 14, 76 cases) is solved exactly optimally by the local search;
4 problems reach N = 31. The search's results are optimal-in-practice,
so the machinery worth removing is the incremental move-evaluation
layer, whose only purpose was speed the workload does not need.

Score each candidate move by cloning the state, applying the move, and
evaluating directly - deleting the trial iterators, per-move item-list
builders, changed-shadow-call tracking, staged lower-bound
short-circuit, and their dedicated tests (~400 lines). The debug
cross-check that already asserted incremental == direct evaluation is
what guarantees this refactor is behavior-identical; the snapshot
corpus confirms it.
@sbillig
sbillig merged commit 5028ed2 into fe-lang:main Jul 11, 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