From ee2763eb0f43fdd5bc4b5d66ec77596e877baeca Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 16 Mar 2026 10:33:49 +0000 Subject: [PATCH 1/7] Add constant propagation pass with ISA support and CLI wiring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements a DFS-based constant propagation pass (opt.ConstantPropagation) that substitutes known-constant registers with immediates and folds constant expressions (e.g. add #2 #3 → #5). Key design points: - Uses a DFS forest over the CFG so that unreachable blocks are handled correctly and never corrupt reaching-constant stacks. - Tracks a per-register stack of reaching constants; only propagates when all definitions of a register reachable in the current DFS component are in the current block (safe for non-SSA code at join points). - Leverages the SubstituteArgument API (PR 1) to keep register usage metadata consistent during substitution. - Leverages FunctionControlFlowInfo (PR 2) for CFG traversal without pulling in SSA construction. ISA support: add, sub, mul, and, or, xor, move, phi all implement PropagateConstants; branches and calls embed PropagatesNoConstants. Binary folding lives in usm/isa/binary_calculation.go. Wired into the CLI as 'cp' / 'constant-propagation'. Bumps alon.kr/x/graph to a version that provides DfsForest(). https://claude.ai/code/session_0165cPFCURfazKMvWB6yPZBa --- CLAUDE.md | 33 ++- go.mod | 2 +- go.sum | 4 +- opt/constant_propagation.go | 269 ++++++++++++++++++ opt/constant_propagation_test.go | 11 + .../constant_propagation/arithmetic.usm | 17 ++ .../constant_propagation/branch_constant.usm | 31 ++ opt/testdata/constant_propagation/chain.usm | 17 ++ .../constant_fold_chain.usm | 21 ++ .../constant_propagation/diamond_no_phi.usm | 35 +++ .../constant_propagation/loop_invariant.usm | 34 +++ .../constant_propagation/loop_redef.usm | 31 ++ .../constant_propagation/multi_block.usm | 19 ++ .../constant_propagation/multiple_uses.usm | 17 ++ .../constant_propagation/no_change.usm | 17 ++ .../phi_same_constant.usm | 33 +++ .../constant_propagation/phi_value.usm | 25 ++ .../same_constant_diamond.usm | 36 +++ .../constant_propagation/sequential_redef.usm | 20 ++ opt/testdata/constant_propagation/simple.usm | 15 + .../unreachable_redef.usm | 39 +++ usm.go | 14 + usm/isa/add.go | 9 + usm/isa/and.go | 9 + usm/isa/binary_calculation.go | 40 +++ usm/isa/call.go | 3 + usm/isa/conditional_jump.go | 3 + usm/isa/j.go | 3 + usm/isa/move.go | 18 ++ usm/isa/mul.go | 9 + usm/isa/or.go | 9 + usm/isa/phi.go | 27 ++ usm/isa/ret.go | 3 + usm/isa/sub.go | 9 + usm/isa/xor.go | 9 + 35 files changed, 885 insertions(+), 6 deletions(-) create mode 100644 opt/constant_propagation.go create mode 100644 opt/constant_propagation_test.go create mode 100644 opt/testdata/constant_propagation/arithmetic.usm create mode 100644 opt/testdata/constant_propagation/branch_constant.usm create mode 100644 opt/testdata/constant_propagation/chain.usm create mode 100644 opt/testdata/constant_propagation/constant_fold_chain.usm create mode 100644 opt/testdata/constant_propagation/diamond_no_phi.usm create mode 100644 opt/testdata/constant_propagation/loop_invariant.usm create mode 100644 opt/testdata/constant_propagation/loop_redef.usm create mode 100644 opt/testdata/constant_propagation/multi_block.usm create mode 100644 opt/testdata/constant_propagation/multiple_uses.usm create mode 100644 opt/testdata/constant_propagation/no_change.usm create mode 100644 opt/testdata/constant_propagation/phi_same_constant.usm create mode 100644 opt/testdata/constant_propagation/phi_value.usm create mode 100644 opt/testdata/constant_propagation/same_constant_diamond.usm create mode 100644 opt/testdata/constant_propagation/sequential_redef.usm create mode 100644 opt/testdata/constant_propagation/simple.usm create mode 100644 opt/testdata/constant_propagation/unreachable_redef.usm diff --git a/CLAUDE.md b/CLAUDE.md index 91d1827..cbaa07c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -65,11 +65,12 @@ Available transformations (in order): | Name | Effect | | ---------------------------------- | ----------------------------------- | | `static-single-assignment` / `ssa` | Convert to SSA form | +| `constant-propagation` / `cp` | Propagate and fold constants | | `dead-code-elimination` / `dce` | Remove unused instructions | | `aarch64` / `arm64` | Translate USM → AArch64 assembly | | `macho` / `macho-obj` | Emit Mach-O `.o` object file | -Typical pipeline: `usm input.usm ssa dce aarch64 macho` +Typical pipeline: `usm input.usm ssa cp dce aarch64 macho` ## Architecture: Key Patterns @@ -93,6 +94,9 @@ Instructions declare capabilities via trait interfaces: - `NonBranchingInstruction` — falls through to the next instruction - `Uses(i int)` — returns the i-th used value (for liveness analysis) - `Defines()` — returns the defined value (for SSA/DCE) +- `PropagateConstants(info) []ConstantDefinition` — for constant propagation: + returns (register, immediate) pairs that are known-constant after this + instruction executes; embed `opt.PropagatesNoConstants` as the default no-op ### Result/Error System (`core`) @@ -172,12 +176,35 @@ package. ## Adding a New Optimization Pass 1. Create a file in `opt/`. -2. The pass receives a `*gen.FunctionGenerationContext` (or similar IR). +2. The pass receives a `*gen.FunctionInfo` (or similar IR). 3. Implement liveness/dataflow analysis if needed — see `opt/dead_code_elimination.go` - as a reference. + and `opt/constant_propagation.go` as references. 4. Passes must run **after SSA construction** if they rely on SSA properties. 5. Register the pass as a `transform.Transformation`. +### Constant Propagation Pass (`opt/constant_propagation.go`) + +Propagates known-constant registers to their use sites and folds constant +expressions. Uses a DFS over the CFG (`ControlFlowGraph.Dfs(0).Timeline`) with +a per-register reaching-constants stack (mirrors `opt/ssa.ReachingDefinitionsSet`): + +- Only tracks registers with exactly **one reachable definition**. Unreachable + definitions (dead code, isolated loops) do not count. Registers with multiple + reachable definitions are propagated only when all those definitions are + sequential redefinitions in the same basic block (later definitions shadow + earlier ones on the DFS stack). Diamond joins and cross-block redefinitions + are never propagated. +- After substituting arguments, calls `PropagateConstants` on the instruction to + fold constant expressions (e.g. `add #2 #3 → #5`). Folded results propagate + to downstream uses in the same DFS scope. +- Uses CFG DFS (not dominator-tree DFS) to correctly handle unreachable blocks: + Lengauer-Tarjan assigns `PreOrder=0` to unvisited nodes, which can corrupt the + dominator tree when unreachable blocks are present. + +To support a new instruction in CP: implement `PropagateConstants` or embed +`opt.PropagatesNoConstants`. Binary arithmetic helpers (`foldBinaryConstants`) live +in `usm/isa/binary_calculation.go`. + ## CI/CD GitHub Actions (`.github/workflows/ci.yml`): diff --git a/go.mod b/go.mod index 3552939..b9551b1 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.23.0 require ( alon.kr/x/aarch64codegen v0.0.0-20250509160800-a2358754df5d alon.kr/x/faststringmap v0.0.0-20250503134653-20d6364c2c94 - alon.kr/x/graph v0.0.0-20250319212444-dd67d0281ab7 + alon.kr/x/graph v0.0.0-20260316082925-2052fa913ff8 alon.kr/x/list v0.0.0-20241203223347-3173d76828c0 alon.kr/x/macho v0.0.0-20250426181816-9e10903e5803 alon.kr/x/set v0.0.0-20250319203758-c0328d5645c9 diff --git a/go.sum b/go.sum index f328d52..b0c3017 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ alon.kr/x/aarch64codegen v0.0.0-20250509160800-a2358754df5d h1:zaesIgOOypUXQT8za alon.kr/x/aarch64codegen v0.0.0-20250509160800-a2358754df5d/go.mod h1:WAdZYqOdp9KwoBjWamrMDAphahR5oXkPSICj3+tLIyQ= alon.kr/x/faststringmap v0.0.0-20250503134653-20d6364c2c94 h1:EdG6bQh5IT5MZ76MY85/GK4Ma7zoJ4zcFqd2fpxK6NI= alon.kr/x/faststringmap v0.0.0-20250503134653-20d6364c2c94/go.mod h1:dPORoTvAFIehHRaI/lfJV3eT6PL5tY7fpAtxzz7rQVw= -alon.kr/x/graph v0.0.0-20250319212444-dd67d0281ab7 h1:0d/oLvPQWc1B38ZfWfJ5UQXeHx9JT8gmp3K0AOOG8aQ= -alon.kr/x/graph v0.0.0-20250319212444-dd67d0281ab7/go.mod h1:Cat+EJocX/ddkDX1quMqwe+itOfD7DILXV76nG0tk8Q= +alon.kr/x/graph v0.0.0-20260316082925-2052fa913ff8 h1:iMPYCLtbIZSc+csc+NVtGgDjgaFQ4oThgKcyq2Nu2Sk= +alon.kr/x/graph v0.0.0-20260316082925-2052fa913ff8/go.mod h1:Cat+EJocX/ddkDX1quMqwe+itOfD7DILXV76nG0tk8Q= alon.kr/x/list v0.0.0-20241203223347-3173d76828c0 h1:NedGw3ZJMKT9Y4RByuogGfBEXT4st8eX/XO5frezx6o= alon.kr/x/list v0.0.0-20241203223347-3173d76828c0/go.mod h1:7syBFMuVmxetKppmlKe6nQlp2BRueprI1r8/d7GB7fs= alon.kr/x/macho v0.0.0-20250426181816-9e10903e5803 h1:iVNUBqzMx3kA7hJRKQRDsfPX7TQ/kpnNjQ1NnSZ7vcQ= diff --git a/opt/constant_propagation.go b/opt/constant_propagation.go new file mode 100644 index 0000000..2adfcd7 --- /dev/null +++ b/opt/constant_propagation.go @@ -0,0 +1,269 @@ +package opt + +import ( + "alon.kr/x/list" + "alon.kr/x/stack" + "alon.kr/x/usm/core" + "alon.kr/x/usm/gen" +) + +// ConstantDefinition pairs a target register with the constant immediate +// value that an instruction always assigns to it. +type ConstantDefinition struct { + Register *gen.RegisterInfo + Immediate *gen.ImmediateInfo +} + +// ConstantPropagationSupportedInstruction is implemented by ISA instructions +// that support the constant propagation optimization pass. +type ConstantPropagationSupportedInstruction interface { + gen.InstructionDefinition + + // PropagateConstants returns (register, constant) pairs for each target + // that this instruction provably assigns a constant immediate to. + // Returns an empty slice if this instruction defines no constants. + // Example: "$64 %x = $64 #5" → [{Register: %x, Immediate: $64 #5}] + PropagateConstants(info *gen.InstructionInfo) []ConstantDefinition +} + +// PropagatesNoConstants can be embedded in instruction definitions that never +// assign a constant immediate to any of their targets. +type PropagatesNoConstants struct{} + +func (PropagatesNoConstants) PropagateConstants(*gen.InstructionInfo) []ConstantDefinition { + return nil +} + +func newCPNotSupportedError(instruction *gen.InstructionInfo) core.ResultList { + return list.FromSingle(core.Result{{ + Type: core.InternalErrorResult, + Message: "Instruction does not support constant propagation", + Location: instruction.Declaration, + }}) +} + +// cpBlockSeparator is the sentinel pushed onto cpReachingConstants.registerPushes +// to mark the boundary between basic blocks in the DFS. +// Mirrors the blockSeparator sentinel used in ReachingDefinitionsSet (opt/ssa). +const cpBlockSeparator = ^uint(0) + +// cpReachingConstants tracks, for each register, the constant value of its +// reaching definition at the current position in a DFS forest traversal. +// +// The design mirrors ReachingDefinitionsSet in opt/ssa: a per-register stack +// holds the sequence of constant values introduced along the path from the +// DFS tree root to the current block, and pushBlock/popBlock scope those +// values to the block's DFS subtree. +type cpReachingConstants struct { + // Per-register stacks of reaching constant values (nil = not constant). + registerStacks []stack.Stack[*gen.ImmediateInfo] + + // Records which register indices were pushed in each block, with + // cpBlockSeparator values marking block boundaries. Mirrors + // ReachingDefinitionsSet.registerDefinitionPushes in opt/ssa. + registerPushes stack.Stack[uint] + + // Maps each *RegisterInfo to its index in registerStacks. + registersToIndex map[*gen.RegisterInfo]uint + + // Maps each basic block to the index of its DFS tree root (its component + // ID). Used by define() to determine whether a definition belongs to the + // same connected component as the block currently being processed. + blockComponent map[*gen.BasicBlockInfo]uint +} + +// collectRegisters scans all instruction targets in the given blocks to build +// a register→index map. Scanning targets directly (rather than using +// function.Registers) ensures that registers introduced by any prior +// transformation pass are included. +func collectRegisters(blocks []*gen.BasicBlockInfo) (map[*gen.RegisterInfo]uint, uint) { + index := make(map[*gen.RegisterInfo]uint) + var count uint + for _, block := range blocks { + for _, instruction := range block.Instructions { + for _, target := range instruction.Targets { + if _, ok := index[target.Register]; !ok { + index[target.Register] = count + count++ + } + } + } + } + return index, count +} + +func newCPReachingConstants( + blocks []*gen.BasicBlockInfo, + blockComponent map[*gen.BasicBlockInfo]uint, +) cpReachingConstants { + registersToIndex, count := collectRegisters(blocks) + return cpReachingConstants{ + registerStacks: make([]stack.Stack[*gen.ImmediateInfo], count), + registerPushes: stack.New[uint](), + registersToIndex: registersToIndex, + blockComponent: blockComponent, + } +} + +// get returns the constant value of the reaching definition for reg at the +// current DFS position, or nil if no constant reaching definition is known. +func (c *cpReachingConstants) get(reg *gen.RegisterInfo) *gen.ImmediateInfo { + idx, ok := c.registersToIndex[reg] + if !ok { + return nil + } + stk := c.registerStacks[idx] + if len(stk) == 0 { + return nil + } + return stk[len(stk)-1] +} + +// define records a new constant reaching definition for reg in currentBlock. +// +// A constant is pushed onto reg's stack only when every definition of reg in +// the same connected component as currentBlock is in currentBlock itself. This +// covers two cases uniformly: +// - Exactly one in-component definition, in currentBlock: it dominates all +// uses in well-formed code, so the stack value is always the true reaching +// value. +// - Multiple in-component definitions, all in currentBlock: they are +// sequential redefinitions within the same block. Each define call pushes +// one value; later definitions shadow earlier ones on the stack, and all +// are popped together when the block is exited. +// +// Any in-component definition outside currentBlock disqualifies the register: +// DFS ancestry does not imply domination at join points, so propagating such a +// value would be unsound. Definitions in other components (dead code, isolated +// loops) do not count and do not disqualify the register. +func (c *cpReachingConstants) define( + reg *gen.RegisterInfo, + imm *gen.ImmediateInfo, + currentBlock *gen.BasicBlockInfo, +) { + if imm == nil { + return + } + + currentComponent := c.blockComponent[currentBlock] + for _, def := range reg.Definitions { + if c.blockComponent[def.BasicBlockInfo] == currentComponent && def.BasicBlockInfo != currentBlock { + return + } + } + + idx, ok := c.registersToIndex[reg] + if !ok { + return + } + c.registerPushes.Push(idx) + c.registerStacks[idx].Push(imm) +} + +func (c *cpReachingConstants) pushBlock() { + c.registerPushes.Push(cpBlockSeparator) +} + +func (c *cpReachingConstants) popBlock() { + for c.registerPushes.Top() != cpBlockSeparator { + idx := c.registerPushes.Top() + c.registerPushes.Pop() + c.registerStacks[idx].Pop() + } + c.registerPushes.Pop() // pop the block separator itself +} + +// cpProcessInstruction performs constant propagation for one instruction: +// it substitutes any register arguments whose reaching definition is a known +// constant, then records new reaching constants for targets whose value is +// now known after the substitution. +func cpProcessInstruction( + instruction *gen.InstructionInfo, + reaching *cpReachingConstants, +) core.ResultList { + cpInstruction, ok := instruction.Definition.(ConstantPropagationSupportedInstruction) + if !ok { + return newCPNotSupportedError(instruction) + } + + // Substitute arguments whose reaching definition is a known constant. + for i, arg := range instruction.Arguments { + regArg, ok := arg.(*gen.RegisterArgumentInfo) + if !ok { + continue + } + if imm := reaching.get(regArg.Register); imm != nil { + instruction.SubstituteArgument(i, imm) + } + } + + // After substitution, record the reaching constant for each target. + // PropagateConstants is called after substitution so that instructions + // whose arguments just became constants (e.g. a move whose source was + // just replaced) can propagate the constant to their own targets. + for _, def := range cpInstruction.PropagateConstants(instruction) { + reaching.define(def.Register, def.Immediate, instruction.BasicBlockInfo) + } + + return core.ResultList{} +} + +// ConstantPropagation replaces all uses of registers that are provably +// assigned a constant immediate value with that immediate directly. +// +// The pass uses DfsForest to traverse all basic blocks in a single unified +// DFS, covering every connected component of the CFG including unreachable +// blocks (isolated loops, dead code). Within each DFS tree the +// ancestor-before-descendant property ensures that any definition dominating +// a use is on the DFS stack when that use is processed. +// +// define() only pushes a constant for a register when all of its definitions +// in the same connected component are confined to the current block. This +// keeps the analysis sound for both SSA and non-SSA code: registers with +// definitions spanning multiple blocks in the same component are skipped +// because DFS ancestry does not imply domination at join points. +// +// All instructions in the function must implement +// ConstantPropagationSupportedInstruction. +func ConstantPropagation(function *gen.FunctionInfo) core.ResultList { + cfInfo := NewFunctionControlFlowInfo(function) + n := uint(len(cfInfo.BasicBlocks)) + forest := cfInfo.ControlFlowGraph.DfsForest() + + // Compute the component ID for each block: the index of its DFS tree root. + // Since parents always have a lower preorder than their children in a DFS + // tree, iterating in preorder lets us propagate component IDs in one pass. + componentOf := make([]uint, n) + for preorder := uint(0); preorder < n; preorder++ { + node := forest.PreOrderReversed[preorder] + if forest.Parent[node] == node { + componentOf[node] = node + } else { + componentOf[node] = componentOf[forest.Parent[node]] + } + } + + blockComponent := make(map[*gen.BasicBlockInfo]uint, n) + for i, block := range cfInfo.BasicBlocks { + blockComponent[block] = componentOf[uint(i)] + } + + reaching := newCPReachingConstants(cfInfo.BasicBlocks, blockComponent) + results := core.ResultList{} + + for _, event := range forest.Timeline { + if event >= n { + reaching.popBlock() + continue + } + + reaching.pushBlock() + block := cfInfo.BasicBlocks[event] + for _, instruction := range block.Instructions { + curResults := cpProcessInstruction(instruction, &reaching) + results.Extend(&curResults) + } + } + + return results +} diff --git a/opt/constant_propagation_test.go b/opt/constant_propagation_test.go new file mode 100644 index 0000000..4a1f082 --- /dev/null +++ b/opt/constant_propagation_test.go @@ -0,0 +1,11 @@ +package opt_test + +import ( + "testing" + + "alon.kr/x/usm/opt" +) + +func TestConstantPropagation(t *testing.T) { + RunOptimizationTests(t, "constant_propagation", opt.ConstantPropagation) +} diff --git a/opt/testdata/constant_propagation/arithmetic.usm b/opt/testdata/constant_propagation/arithmetic.usm new file mode 100644 index 0000000..417a482 --- /dev/null +++ b/opt/testdata/constant_propagation/arithmetic.usm @@ -0,0 +1,17 @@ +func @use $64 %reg + +func @input $64 %param { +.entry + $64 %x = $64 #3 + $64 %z = add %x %param + call @use %z + ret +} + +func @expected $64 %param { +.entry + $64 %x = $64 #3 + $64 %z = add $64 #3 %param + call @use %z + ret +} diff --git a/opt/testdata/constant_propagation/branch_constant.usm b/opt/testdata/constant_propagation/branch_constant.usm new file mode 100644 index 0000000..ba26df3 --- /dev/null +++ b/opt/testdata/constant_propagation/branch_constant.usm @@ -0,0 +1,31 @@ +func @use $64 %reg + +; %x = #5 (non-zero) is a constant. CP substitutes it into the jnz condition +; and into both branch arms. +func @input { +.entry + $64 %x = $64 #5 + jnz %x .left +.right + call @use %x + j .exit +.left + call @use %x + j .exit +.exit + ret +} + +func @expected { +.entry + $64 %x = $64 #5 + jnz $64 #5 .left +.right + call @use $64 #5 + j .exit +.left + call @use $64 #5 + j .exit +.exit + ret +} diff --git a/opt/testdata/constant_propagation/chain.usm b/opt/testdata/constant_propagation/chain.usm new file mode 100644 index 0000000..c554b2b --- /dev/null +++ b/opt/testdata/constant_propagation/chain.usm @@ -0,0 +1,17 @@ +func @use $64 %reg + +func @input { +.entry + $64 %x = $64 #5 + $64 %y = %x + call @use %y + ret +} + +func @expected { +.entry + $64 %x = $64 #5 + $64 %y = $64 #5 + call @use $64 #5 + ret +} diff --git a/opt/testdata/constant_propagation/constant_fold_chain.usm b/opt/testdata/constant_propagation/constant_fold_chain.usm new file mode 100644 index 0000000..fbb7349 --- /dev/null +++ b/opt/testdata/constant_propagation/constant_fold_chain.usm @@ -0,0 +1,21 @@ +func @use $64 %reg + +func @input { +.entry + $64 %a = $64 #2 + $64 %b = $64 #3 + $64 %c = add %a %b + $64 %d = mul %c %a + call @use %d + ret +} + +func @expected { +.entry + $64 %a = $64 #2 + $64 %b = $64 #3 + $64 %c = add $64 #2 $64 #3 + $64 %d = mul $64 #5 $64 #2 + call @use $64 #10 + ret +} diff --git a/opt/testdata/constant_propagation/diamond_no_phi.usm b/opt/testdata/constant_propagation/diamond_no_phi.usm new file mode 100644 index 0000000..2d79feb --- /dev/null +++ b/opt/testdata/constant_propagation/diamond_no_phi.usm @@ -0,0 +1,35 @@ +func $64 @getcond + +func @use $64 %reg + +; Two branches assign different constants to %val without a phi node. +; %val has two definitions, so CP must NOT propagate either value to .merge. +func @input { +.entry + $64 %cond = call @getcond + jnz %cond .left +.right + $64 %val = $64 #1 + j .merge +.left + $64 %val = $64 #2 + j .merge +.merge + call @use %val + ret +} + +func @expected { +.entry + $64 %cond = call @getcond + jnz %cond .left +.right + $64 %val = $64 #1 + j .merge +.left + $64 %val = $64 #2 + j .merge +.merge + call @use %val + ret +} diff --git a/opt/testdata/constant_propagation/loop_invariant.usm b/opt/testdata/constant_propagation/loop_invariant.usm new file mode 100644 index 0000000..58d2cea --- /dev/null +++ b/opt/testdata/constant_propagation/loop_invariant.usm @@ -0,0 +1,34 @@ +func @use $64 %reg + +; %k = #42 is defined once in .entry and is loop-invariant. CP propagates it +; into the loop body across basic blocks via DFS ancestry. +; %i is defined by a phi (not a constant), so it is not propagated. +func @input { +.entry + $64 %k = $64 #42 + j .header +.header + $64 %i = phi .entry $64 #0 .body %next + jz %i .exit +.body + call @use %k + $64 %next = add %i $64 #1 + j .header +.exit + ret +} + +func @expected { +.entry + $64 %k = $64 #42 + j .header +.header + $64 %i = phi .entry $64 #0 .body %next + jz %i .exit +.body + call @use $64 #42 + $64 %next = add %i $64 #1 + j .header +.exit + ret +} diff --git a/opt/testdata/constant_propagation/loop_redef.usm b/opt/testdata/constant_propagation/loop_redef.usm new file mode 100644 index 0000000..aeaa01f --- /dev/null +++ b/opt/testdata/constant_propagation/loop_redef.usm @@ -0,0 +1,31 @@ +func $64 @getcond + +func @use $64 %reg + +; %x is defined with constant #5 in .entry, then redefined in the loop body. +; Because %x has two definitions, CP must NOT propagate #5 anywhere. +func @input { +.entry + $64 %x = $64 #5 + j .loop +.loop + $64 %cond = call @getcond + $64 %x = add %x $64 #1 + jnz %cond .done +.done + call @use %x + ret +} + +func @expected { +.entry + $64 %x = $64 #5 + j .loop +.loop + $64 %cond = call @getcond + $64 %x = add %x $64 #1 + jnz %cond .done +.done + call @use %x + ret +} diff --git a/opt/testdata/constant_propagation/multi_block.usm b/opt/testdata/constant_propagation/multi_block.usm new file mode 100644 index 0000000..dcd1276 --- /dev/null +++ b/opt/testdata/constant_propagation/multi_block.usm @@ -0,0 +1,19 @@ +func @use $64 %reg + +func @input { +.entry + $64 %x = $64 #7 + j .body +.body + call @use %x + ret +} + +func @expected { +.entry + $64 %x = $64 #7 + j .body +.body + call @use $64 #7 + ret +} diff --git a/opt/testdata/constant_propagation/multiple_uses.usm b/opt/testdata/constant_propagation/multiple_uses.usm new file mode 100644 index 0000000..96ab22b --- /dev/null +++ b/opt/testdata/constant_propagation/multiple_uses.usm @@ -0,0 +1,17 @@ +func @use $64 %reg + +func @input { +.entry + $64 %x = $64 #3 + $64 %y = add %x %x + call @use %y + ret +} + +func @expected { +.entry + $64 %x = $64 #3 + $64 %y = add $64 #3 $64 #3 + call @use $64 #6 + ret +} diff --git a/opt/testdata/constant_propagation/no_change.usm b/opt/testdata/constant_propagation/no_change.usm new file mode 100644 index 0000000..169b095 --- /dev/null +++ b/opt/testdata/constant_propagation/no_change.usm @@ -0,0 +1,17 @@ +func $64 @getval + +func @use $64 %reg + +func @input { +.entry + $64 %x = call @getval + call @use %x + ret +} + +func @expected { +.entry + $64 %x = call @getval + call @use %x + ret +} diff --git a/opt/testdata/constant_propagation/phi_same_constant.usm b/opt/testdata/constant_propagation/phi_same_constant.usm new file mode 100644 index 0000000..a71a38b --- /dev/null +++ b/opt/testdata/constant_propagation/phi_same_constant.usm @@ -0,0 +1,33 @@ +func $64 @getcond + +func @use $64 %reg + +; When all incoming value arguments of a phi are the same constant, the phi +; yields that constant and CP propagates it to downstream uses. +func @input { +.entry + $64 %cond = call @getcond + jnz %cond .left +.right + j .merge +.left + j .merge +.merge + $64 %x = phi .right $64 #7 .left $64 #7 + call @use %x + ret +} + +func @expected { +.entry + $64 %cond = call @getcond + jnz %cond .left +.right + j .merge +.left + j .merge +.merge + $64 %x = phi .right $64 #7 .left $64 #7 + call @use $64 #7 + ret +} diff --git a/opt/testdata/constant_propagation/phi_value.usm b/opt/testdata/constant_propagation/phi_value.usm new file mode 100644 index 0000000..308a07d --- /dev/null +++ b/opt/testdata/constant_propagation/phi_value.usm @@ -0,0 +1,25 @@ +func @use $64 %reg + +func @input { +.entry + $64 %c = $64 #1 + j .merge +.other + j .merge +.merge + $64 %v = phi .entry %c .other $64 #2 + call @use %v + ret +} + +func @expected { +.entry + $64 %c = $64 #1 + j .merge +.other + j .merge +.merge + $64 %v = phi .entry $64 #1 .other $64 #2 + call @use %v + ret +} diff --git a/opt/testdata/constant_propagation/same_constant_diamond.usm b/opt/testdata/constant_propagation/same_constant_diamond.usm new file mode 100644 index 0000000..b458eff --- /dev/null +++ b/opt/testdata/constant_propagation/same_constant_diamond.usm @@ -0,0 +1,36 @@ +func $64 @getcond + +func @use $64 %reg + +; Both branches assign the same constant #42 to %val, but without a phi node. +; %val still has two definitions, so CP conservatively does NOT propagate. +; The pass only propagates single-definition registers. +func @input { +.entry + $64 %cond = call @getcond + jnz %cond .left +.right + $64 %val = $64 #42 + j .merge +.left + $64 %val = $64 #42 + j .merge +.merge + call @use %val + ret +} + +func @expected { +.entry + $64 %cond = call @getcond + jnz %cond .left +.right + $64 %val = $64 #42 + j .merge +.left + $64 %val = $64 #42 + j .merge +.merge + call @use %val + ret +} diff --git a/opt/testdata/constant_propagation/sequential_redef.usm b/opt/testdata/constant_propagation/sequential_redef.usm new file mode 100644 index 0000000..7ffbef0 --- /dev/null +++ b/opt/testdata/constant_propagation/sequential_redef.usm @@ -0,0 +1,20 @@ +func @use $64 %reg + +; %x is assigned twice in the same block. The second definition (#99) is the +; unique reaching definition at the use, so CP propagates #99. +; Both definitions have the same block, satisfying the same-block guard. +func @input { +.entry + $64 %x = $64 #1 + $64 %x = $64 #99 + call @use %x + ret +} + +func @expected { +.entry + $64 %x = $64 #1 + $64 %x = $64 #99 + call @use $64 #99 + ret +} diff --git a/opt/testdata/constant_propagation/simple.usm b/opt/testdata/constant_propagation/simple.usm new file mode 100644 index 0000000..5ba9edc --- /dev/null +++ b/opt/testdata/constant_propagation/simple.usm @@ -0,0 +1,15 @@ +func @use $64 %reg + +func @input { +.entry + $64 %x = $64 #5 + call @use %x + ret +} + +func @expected { +.entry + $64 %x = $64 #5 + call @use $64 #5 + ret +} diff --git a/opt/testdata/constant_propagation/unreachable_redef.usm b/opt/testdata/constant_propagation/unreachable_redef.usm new file mode 100644 index 0000000..e9d3d44 --- /dev/null +++ b/opt/testdata/constant_propagation/unreachable_redef.usm @@ -0,0 +1,39 @@ +func @use $64 %reg + +; %k has two definitions: #42 in .entry (reachable) and #0 in .isolated +; (an isolated self-loop, never jumped to from outside). The two definitions +; belong to different connected components, so each component's CP run sees +; only its own reachable definitions. +; +; Component 1 (.entry + .end): only %k = #42 is reachable, so CP propagates +; #42 to "call @use %k" in .end. +; +; Component 2 (.isolated): only %k = #0 is reachable within this component, +; so CP propagates #0 to "call @use %k" inside .isolated. +func @input { +.entry + $64 %k = $64 #42 + j .end +.isolated + $64 %k = $64 #0 + $64 %j = $64 #7 + call @use %k + j .isolated +.end + call @use %k + ret +} + +func @expected { +.entry + $64 %k = $64 #42 + j .end +.isolated + $64 %k = $64 #0 + $64 %j = $64 #7 + call @use $64 #0 + j .isolated +.end + call @use $64 #42 + ret +} diff --git a/usm.go b/usm.go index eb1a505..5e158c1 100644 --- a/usm.go +++ b/usm.go @@ -10,6 +10,7 @@ import ( "alon.kr/x/usm/core" "alon.kr/x/usm/gen" "alon.kr/x/usm/lex" + "alon.kr/x/usm/opt" "alon.kr/x/usm/parse" "alon.kr/x/usm/transform" usmmanagers "alon.kr/x/usm/usm/managers" @@ -24,6 +25,19 @@ var targets = transform.NewTargetCollection( Description: "A universal assembly language", GenerationContext: usmmanagers.NewGenerationContext(), Transformations: *transform.NewTransformationCollection( + &transform.Transformation{ + Names: []string{"constant-propagation", "cp"}, + Description: "An optimization pass that propagates and folds constant expressions", + TargetName: "usm", + Transform: func(data *transform.TargetData) (*transform.TargetData, core.ResultList) { + results := core.ResultList{} + for _, function := range data.Code.Functions { + curResults := opt.ConstantPropagation(function) + results.Extend(&curResults) + } + return data, results + }, + }, &transform.Transformation{ Names: []string{"dead-code-elimination", "dce"}, Description: "An optimization pass that eliminates unnecessary instructions", diff --git a/usm/isa/add.go b/usm/isa/add.go index ac377ca..4a72ca9 100644 --- a/usm/isa/add.go +++ b/usm/isa/add.go @@ -1,7 +1,10 @@ package usmisa import ( + "math/big" + "alon.kr/x/usm/gen" + "alon.kr/x/usm/opt" ) type Add struct { @@ -16,3 +19,9 @@ func NewAdd() gen.InstructionDefinition { func (Add) Operator(*gen.InstructionInfo) string { return "add" } + +func (Add) PropagateConstants(info *gen.InstructionInfo) []opt.ConstantDefinition { + return foldBinaryConstants(info, func(l, r *big.Int) *big.Int { + return new(big.Int).Add(l, r) + }) +} diff --git a/usm/isa/and.go b/usm/isa/and.go index 899626f..e2f496e 100644 --- a/usm/isa/and.go +++ b/usm/isa/and.go @@ -1,7 +1,10 @@ package usmisa import ( + "math/big" + "alon.kr/x/usm/gen" + "alon.kr/x/usm/opt" ) type And struct { @@ -16,3 +19,9 @@ func NewAnd() gen.InstructionDefinition { func (And) Operator(*gen.InstructionInfo) string { return "and" } + +func (And) PropagateConstants(info *gen.InstructionInfo) []opt.ConstantDefinition { + return foldBinaryConstants(info, func(l, r *big.Int) *big.Int { + return new(big.Int).And(l, r) + }) +} diff --git a/usm/isa/binary_calculation.go b/usm/isa/binary_calculation.go index 5fe9919..1027cc5 100644 --- a/usm/isa/binary_calculation.go +++ b/usm/isa/binary_calculation.go @@ -1,6 +1,8 @@ package usmisa import ( + "math/big" + "alon.kr/x/usm/core" "alon.kr/x/usm/gen" "alon.kr/x/usm/opt" @@ -14,6 +16,10 @@ type BinaryCalculation struct { opt.NonCriticalInstruction opt.UsesArgumentsInstruction opt.DefinesTargetsInstruction + + // Constant Propagation: default is no folding; specific operations + // override PropagateConstants to implement constant folding. + opt.PropagatesNoConstants } // Validates that there are exactly two arguments and one target, all of the @@ -65,3 +71,37 @@ func (BinaryCalculation) Validate(info *gen.InstructionInfo) core.ResultList { return results } + +// foldBinaryConstants evaluates a binary instruction when both arguments are +// immediates, returning the folded constant for the single target. +// fold receives the left and right big.Int values and returns the result. +// Returns nil if either argument is not an immediate. +func foldBinaryConstants( + info *gen.InstructionInfo, + fold func(l, r *big.Int) *big.Int, +) []opt.ConstantDefinition { + if len(info.Arguments) != 2 || len(info.Targets) != 1 { + return nil + } + + left, ok := info.Arguments[0].(*gen.ImmediateInfo) + if !ok { + return nil + } + + right, ok := info.Arguments[1].(*gen.ImmediateInfo) + if !ok { + return nil + } + + result := fold(left.Value, right.Value) + imm := &gen.ImmediateInfo{ + Type: info.Targets[0].Register.Type, + Value: result, + } + + return []opt.ConstantDefinition{{ + Register: info.Targets[0].Register, + Immediate: imm, + }} +} diff --git a/usm/isa/call.go b/usm/isa/call.go index 5243243..df38fe9 100644 --- a/usm/isa/call.go +++ b/usm/isa/call.go @@ -16,6 +16,9 @@ type Call struct { opt.CriticalInstruction opt.UsesArgumentsInstruction opt.DefinesTargetsInstruction + + // Constant Propagation + opt.PropagatesNoConstants } func NewCall() gen.InstructionDefinition { diff --git a/usm/isa/conditional_jump.go b/usm/isa/conditional_jump.go index c40d051..a658da1 100644 --- a/usm/isa/conditional_jump.go +++ b/usm/isa/conditional_jump.go @@ -14,6 +14,9 @@ type ConditionalJump struct { opt.CriticalInstruction opt.UsesArgumentsInstruction opt.DefinesNothingInstruction + + // Constant Propagation + opt.PropagatesNoConstants } func (ConditionalJump) Validate(info *gen.InstructionInfo) core.ResultList { diff --git a/usm/isa/j.go b/usm/isa/j.go index 2b507f5..a27039f 100644 --- a/usm/isa/j.go +++ b/usm/isa/j.go @@ -14,6 +14,9 @@ type J struct { opt.CriticalInstruction opt.UsesNothingInstruction opt.DefinesNothingInstruction + + // Constant Propagation + opt.PropagatesNoConstants } func NewJump() J { diff --git a/usm/isa/move.go b/usm/isa/move.go index c66bb9b..632e14a 100644 --- a/usm/isa/move.go +++ b/usm/isa/move.go @@ -19,6 +19,24 @@ type Move struct { opt.DefinesTargetsInstruction } +// PropagateConstants returns the immediate constant assigned to the target +// register if the source argument is a constant immediate, otherwise nil. +func (Move) PropagateConstants(info *gen.InstructionInfo) []opt.ConstantDefinition { + if len(info.Arguments) != 1 || len(info.Targets) != 1 { + return nil + } + + immediate, ok := info.Arguments[0].(*gen.ImmediateInfo) + if !ok { + return nil + } + + return []opt.ConstantDefinition{{ + Register: info.Targets[0].Register, + Immediate: immediate, + }} +} + func NewMove() gen.InstructionDefinition { return Move{} } diff --git a/usm/isa/mul.go b/usm/isa/mul.go index 5f54645..2f58aca 100644 --- a/usm/isa/mul.go +++ b/usm/isa/mul.go @@ -1,7 +1,10 @@ package usmisa import ( + "math/big" + "alon.kr/x/usm/gen" + "alon.kr/x/usm/opt" ) type Mul struct { @@ -16,3 +19,9 @@ func NewMul() gen.InstructionDefinition { func (Mul) Operator(*gen.InstructionInfo) string { return "mul" } + +func (Mul) PropagateConstants(info *gen.InstructionInfo) []opt.ConstantDefinition { + return foldBinaryConstants(info, func(l, r *big.Int) *big.Int { + return new(big.Int).Mul(l, r) + }) +} diff --git a/usm/isa/or.go b/usm/isa/or.go index 83f7b25..7826ea2 100644 --- a/usm/isa/or.go +++ b/usm/isa/or.go @@ -1,7 +1,10 @@ package usmisa import ( + "math/big" + "alon.kr/x/usm/gen" + "alon.kr/x/usm/opt" ) type Or struct { @@ -16,3 +19,9 @@ func NewOr() gen.InstructionDefinition { func (Or) Operator(*gen.InstructionInfo) string { return "or" } + +func (Or) PropagateConstants(info *gen.InstructionInfo) []opt.ConstantDefinition { + return foldBinaryConstants(info, func(l, r *big.Int) *big.Int { + return new(big.Int).Or(l, r) + }) +} diff --git a/usm/isa/phi.go b/usm/isa/phi.go index 7434e53..a84f58a 100644 --- a/usm/isa/phi.go +++ b/usm/isa/phi.go @@ -18,6 +18,33 @@ type Phi struct { opt.DefinesTargetsInstruction } +// PropagateConstants returns the shared constant if all incoming value arguments +// of the phi are the same immediate, otherwise nil. +// Phi arguments alternate: label, value, label, value, ... +// Value arguments are at odd indices (1, 3, 5, ...). +func (Phi) PropagateConstants(info *gen.InstructionInfo) []opt.ConstantDefinition { + if len(info.Arguments) < 2 || len(info.Targets) != 1 { + return nil + } + + first, ok := info.Arguments[1].(*gen.ImmediateInfo) + if !ok { + return nil + } + + for i := 3; i < len(info.Arguments); i += 2 { + imm, ok := info.Arguments[i].(*gen.ImmediateInfo) + if !ok || imm.Value.Cmp(first.Value) != 0 { + return nil + } + } + + return []opt.ConstantDefinition{{ + Register: info.Targets[0].Register, + Immediate: first, + }} +} + func NewPhi() gen.InstructionDefinition { return Phi{} } diff --git a/usm/isa/ret.go b/usm/isa/ret.go index 7d217a8..92e26e5 100644 --- a/usm/isa/ret.go +++ b/usm/isa/ret.go @@ -16,6 +16,9 @@ type Ret struct { opt.CriticalInstruction opt.UsesArgumentsInstruction opt.DefinesNothingInstruction + + // Constant Propagation + opt.PropagatesNoConstants } func NewRet() gen.InstructionDefinition { diff --git a/usm/isa/sub.go b/usm/isa/sub.go index c094876..7d2de85 100644 --- a/usm/isa/sub.go +++ b/usm/isa/sub.go @@ -1,7 +1,10 @@ package usmisa import ( + "math/big" + "alon.kr/x/usm/gen" + "alon.kr/x/usm/opt" ) type Sub struct { @@ -16,3 +19,9 @@ func NewSub() gen.InstructionDefinition { func (Sub) Operator(*gen.InstructionInfo) string { return "sub" } + +func (Sub) PropagateConstants(info *gen.InstructionInfo) []opt.ConstantDefinition { + return foldBinaryConstants(info, func(l, r *big.Int) *big.Int { + return new(big.Int).Sub(l, r) + }) +} diff --git a/usm/isa/xor.go b/usm/isa/xor.go index 75eb659..b87b814 100644 --- a/usm/isa/xor.go +++ b/usm/isa/xor.go @@ -1,7 +1,10 @@ package usmisa import ( + "math/big" + "alon.kr/x/usm/gen" + "alon.kr/x/usm/opt" ) type Xor struct { @@ -16,3 +19,9 @@ func NewXor() gen.InstructionDefinition { func (Xor) Operator(*gen.InstructionInfo) string { return "xor" } + +func (Xor) PropagateConstants(info *gen.InstructionInfo) []opt.ConstantDefinition { + return foldBinaryConstants(info, func(l, r *big.Int) *big.Int { + return new(big.Int).Xor(l, r) + }) +} From a9efcccf94f6eeb7d2dabee94e1e02e133fd7dd7 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 16 Mar 2026 15:47:35 +0000 Subject: [PATCH 2/7] Fix FunctionControlFlowInfo reference after move to gen package https://claude.ai/code/session_0165cPFCURfazKMvWB6yPZBa --- opt/constant_propagation.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opt/constant_propagation.go b/opt/constant_propagation.go index 2adfcd7..0a62a1b 100644 --- a/opt/constant_propagation.go +++ b/opt/constant_propagation.go @@ -226,7 +226,7 @@ func cpProcessInstruction( // All instructions in the function must implement // ConstantPropagationSupportedInstruction. func ConstantPropagation(function *gen.FunctionInfo) core.ResultList { - cfInfo := NewFunctionControlFlowInfo(function) + cfInfo := gen.NewFunctionControlFlowInfo(function) n := uint(len(cfInfo.BasicBlocks)) forest := cfInfo.ControlFlowGraph.DfsForest() From c2c2f61da73d32b6ac8fb03d231bdda625cc7937 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 16 Mar 2026 17:20:31 +0000 Subject: [PATCH 3/7] Replace pre-scanned register index with lazy map in cpReachingConstants Removes collectRegisters and the cpBlockSeparator sentinel. registerStacks is now a map[*RegisterInfo]*Stack created on demand in define(), and registerPushes tracks *RegisterInfo pointers directly using nil as the block boundary marker. This eliminates the upfront O(instructions) scan and the parallel registersToIndex map. https://claude.ai/code/session_0165cPFCURfazKMvWB6yPZBa --- opt/constant_propagation.go | 79 +++++++++++-------------------------- 1 file changed, 23 insertions(+), 56 deletions(-) diff --git a/opt/constant_propagation.go b/opt/constant_propagation.go index 0a62a1b..bffa54f 100644 --- a/opt/constant_propagation.go +++ b/opt/constant_propagation.go @@ -42,11 +42,6 @@ func newCPNotSupportedError(instruction *gen.InstructionInfo) core.ResultList { }}) } -// cpBlockSeparator is the sentinel pushed onto cpReachingConstants.registerPushes -// to mark the boundary between basic blocks in the DFS. -// Mirrors the blockSeparator sentinel used in ReachingDefinitionsSet (opt/ssa). -const cpBlockSeparator = ^uint(0) - // cpReachingConstants tracks, for each register, the constant value of its // reaching definition at the current position in a DFS forest traversal. // @@ -55,16 +50,13 @@ const cpBlockSeparator = ^uint(0) // DFS tree root to the current block, and pushBlock/popBlock scope those // values to the block's DFS subtree. type cpReachingConstants struct { - // Per-register stacks of reaching constant values (nil = not constant). - registerStacks []stack.Stack[*gen.ImmediateInfo] + // Per-register stacks of reaching constant values, created on demand. + registerStacks map[*gen.RegisterInfo]*stack.Stack[*gen.ImmediateInfo] - // Records which register indices were pushed in each block, with - // cpBlockSeparator values marking block boundaries. Mirrors + // Records which registers were pushed in each block, with nil entries + // marking block boundaries. Mirrors // ReachingDefinitionsSet.registerDefinitionPushes in opt/ssa. - registerPushes stack.Stack[uint] - - // Maps each *RegisterInfo to its index in registerStacks. - registersToIndex map[*gen.RegisterInfo]uint + registerPushes stack.Stack[*gen.RegisterInfo] // Maps each basic block to the index of its DFS tree root (its component // ID). Used by define() to determine whether a definition belongs to the @@ -72,51 +64,24 @@ type cpReachingConstants struct { blockComponent map[*gen.BasicBlockInfo]uint } -// collectRegisters scans all instruction targets in the given blocks to build -// a register→index map. Scanning targets directly (rather than using -// function.Registers) ensures that registers introduced by any prior -// transformation pass are included. -func collectRegisters(blocks []*gen.BasicBlockInfo) (map[*gen.RegisterInfo]uint, uint) { - index := make(map[*gen.RegisterInfo]uint) - var count uint - for _, block := range blocks { - for _, instruction := range block.Instructions { - for _, target := range instruction.Targets { - if _, ok := index[target.Register]; !ok { - index[target.Register] = count - count++ - } - } - } - } - return index, count -} - func newCPReachingConstants( - blocks []*gen.BasicBlockInfo, blockComponent map[*gen.BasicBlockInfo]uint, ) cpReachingConstants { - registersToIndex, count := collectRegisters(blocks) return cpReachingConstants{ - registerStacks: make([]stack.Stack[*gen.ImmediateInfo], count), - registerPushes: stack.New[uint](), - registersToIndex: registersToIndex, - blockComponent: blockComponent, + registerStacks: make(map[*gen.RegisterInfo]*stack.Stack[*gen.ImmediateInfo]), + registerPushes: stack.New[*gen.RegisterInfo](), + blockComponent: blockComponent, } } // get returns the constant value of the reaching definition for reg at the // current DFS position, or nil if no constant reaching definition is known. func (c *cpReachingConstants) get(reg *gen.RegisterInfo) *gen.ImmediateInfo { - idx, ok := c.registersToIndex[reg] - if !ok { + stk, ok := c.registerStacks[reg] + if !ok || len(*stk) == 0 { return nil } - stk := c.registerStacks[idx] - if len(stk) == 0 { - return nil - } - return stk[len(stk)-1] + return stk.Top() } // define records a new constant reaching definition for reg in currentBlock. @@ -152,25 +117,27 @@ func (c *cpReachingConstants) define( } } - idx, ok := c.registersToIndex[reg] + stk, ok := c.registerStacks[reg] if !ok { - return + newStk := stack.New[*gen.ImmediateInfo]() + stk = &newStk + c.registerStacks[reg] = stk } - c.registerPushes.Push(idx) - c.registerStacks[idx].Push(imm) + stk.Push(imm) + c.registerPushes.Push(reg) } func (c *cpReachingConstants) pushBlock() { - c.registerPushes.Push(cpBlockSeparator) + c.registerPushes.Push(nil) } func (c *cpReachingConstants) popBlock() { - for c.registerPushes.Top() != cpBlockSeparator { - idx := c.registerPushes.Top() + for c.registerPushes.Top() != nil { + reg := c.registerPushes.Top() c.registerPushes.Pop() - c.registerStacks[idx].Pop() + c.registerStacks[reg].Pop() } - c.registerPushes.Pop() // pop the block separator itself + c.registerPushes.Pop() // pop the nil block separator itself } // cpProcessInstruction performs constant propagation for one instruction: @@ -248,7 +215,7 @@ func ConstantPropagation(function *gen.FunctionInfo) core.ResultList { blockComponent[block] = componentOf[uint(i)] } - reaching := newCPReachingConstants(cfInfo.BasicBlocks, blockComponent) + reaching := newCPReachingConstants(blockComponent) results := core.ResultList{} for _, event := range forest.Timeline { From 77beea590ff84ce0163bccbdcad39f24f96c67ad Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 16 Mar 2026 17:26:26 +0000 Subject: [PATCH 4/7] Assign register stack indices lazily instead of pre-scanning Removes collectRegisters and populates registersToIndex on demand in define(): the first time a register is seen, it gets the next available index and an empty stack is appended to registerStacks. This keeps the slice-of-stacks layout (better cache behaviour than a map of pointers) while eliminating the upfront O(instructions) scan. https://claude.ai/code/session_0165cPFCURfazKMvWB6yPZBa --- opt/constant_propagation.go | 59 +++++++++++++++++++++++-------------- 1 file changed, 37 insertions(+), 22 deletions(-) diff --git a/opt/constant_propagation.go b/opt/constant_propagation.go index bffa54f..263e78c 100644 --- a/opt/constant_propagation.go +++ b/opt/constant_propagation.go @@ -42,6 +42,11 @@ func newCPNotSupportedError(instruction *gen.InstructionInfo) core.ResultList { }}) } +// cpBlockSeparator is the sentinel pushed onto cpReachingConstants.registerPushes +// to mark the boundary between basic blocks in the DFS. +// Mirrors the blockSeparator sentinel used in ReachingDefinitionsSet (opt/ssa). +const cpBlockSeparator = ^uint(0) + // cpReachingConstants tracks, for each register, the constant value of its // reaching definition at the current position in a DFS forest traversal. // @@ -50,13 +55,18 @@ func newCPNotSupportedError(instruction *gen.InstructionInfo) core.ResultList { // DFS tree root to the current block, and pushBlock/popBlock scope those // values to the block's DFS subtree. type cpReachingConstants struct { - // Per-register stacks of reaching constant values, created on demand. - registerStacks map[*gen.RegisterInfo]*stack.Stack[*gen.ImmediateInfo] + // Per-register stacks of reaching constant values. Indices are assigned + // lazily via registersToIndex the first time a register is defined. + registerStacks []stack.Stack[*gen.ImmediateInfo] - // Records which registers were pushed in each block, with nil entries - // marking block boundaries. Mirrors + // Records which register indices were pushed in each block, with + // cpBlockSeparator values marking block boundaries. Mirrors // ReachingDefinitionsSet.registerDefinitionPushes in opt/ssa. - registerPushes stack.Stack[*gen.RegisterInfo] + registerPushes stack.Stack[uint] + + // Maps each *RegisterInfo to its index in registerStacks. Entries are + // added on demand in define(). + registersToIndex map[*gen.RegisterInfo]uint // Maps each basic block to the index of its DFS tree root (its component // ID). Used by define() to determine whether a definition belongs to the @@ -68,20 +78,25 @@ func newCPReachingConstants( blockComponent map[*gen.BasicBlockInfo]uint, ) cpReachingConstants { return cpReachingConstants{ - registerStacks: make(map[*gen.RegisterInfo]*stack.Stack[*gen.ImmediateInfo]), - registerPushes: stack.New[*gen.RegisterInfo](), - blockComponent: blockComponent, + registerStacks: []stack.Stack[*gen.ImmediateInfo]{}, + registerPushes: stack.New[uint](), + registersToIndex: make(map[*gen.RegisterInfo]uint), + blockComponent: blockComponent, } } // get returns the constant value of the reaching definition for reg at the // current DFS position, or nil if no constant reaching definition is known. func (c *cpReachingConstants) get(reg *gen.RegisterInfo) *gen.ImmediateInfo { - stk, ok := c.registerStacks[reg] - if !ok || len(*stk) == 0 { + idx, ok := c.registersToIndex[reg] + if !ok { + return nil + } + stk := c.registerStacks[idx] + if len(stk) == 0 { return nil } - return stk.Top() + return stk[len(stk)-1] } // define records a new constant reaching definition for reg in currentBlock. @@ -117,27 +132,27 @@ func (c *cpReachingConstants) define( } } - stk, ok := c.registerStacks[reg] + idx, ok := c.registersToIndex[reg] if !ok { - newStk := stack.New[*gen.ImmediateInfo]() - stk = &newStk - c.registerStacks[reg] = stk + idx = uint(len(c.registerStacks)) + c.registersToIndex[reg] = idx + c.registerStacks = append(c.registerStacks, stack.New[*gen.ImmediateInfo]()) } - stk.Push(imm) - c.registerPushes.Push(reg) + c.registerPushes.Push(idx) + c.registerStacks[idx].Push(imm) } func (c *cpReachingConstants) pushBlock() { - c.registerPushes.Push(nil) + c.registerPushes.Push(cpBlockSeparator) } func (c *cpReachingConstants) popBlock() { - for c.registerPushes.Top() != nil { - reg := c.registerPushes.Top() + for c.registerPushes.Top() != cpBlockSeparator { + idx := c.registerPushes.Top() c.registerPushes.Pop() - c.registerStacks[reg].Pop() + c.registerStacks[idx].Pop() } - c.registerPushes.Pop() // pop the nil block separator itself + c.registerPushes.Pop() // pop the block separator itself } // cpProcessInstruction performs constant propagation for one instruction: From 288493b307243167679764a3f8b80b5739e249d2 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 08:31:21 +0000 Subject: [PATCH 5/7] Add cross_component_def CP test: definition in separate isolated component Tests the case where a register is defined in one unreachable DFS component (.second_isolated) and used in a different unreachable DFS component (.first_isolated), with a jump from the defining block to the using block that becomes a cross-edge in the DFS forest. A third isolated self-loop (.noise) sits between them to ensure the two blocks are assigned to separate DFS trees. CP must not propagate the constant to the cross-component use. https://claude.ai/code/session_0165cPFCURfazKMvWB6yPZBa --- .../cross_component_def.usm | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 opt/testdata/constant_propagation/cross_component_def.usm diff --git a/opt/testdata/constant_propagation/cross_component_def.usm b/opt/testdata/constant_propagation/cross_component_def.usm new file mode 100644 index 0000000..915aae1 --- /dev/null +++ b/opt/testdata/constant_propagation/cross_component_def.usm @@ -0,0 +1,50 @@ +func @use $64 %reg + +; %k is defined only in .second_isolated (#99) and used in .first_isolated. +; .second_isolated jumps to .first_isolated, which might suggest the constant +; reaches the use — but CP must not propagate it across component boundaries. +; +; Block ordering determines DFS component membership: +; +; Component 0 (.entry): reachable root; just returns. +; Component 1 (.first_isolated): unreachable root; uses %k — but %k has no +; definition within this component. +; Component 2 (.noise): unreachable self-loop; sits between the two +; isolated components so that .second_isolated +; is not adjacent to .first_isolated in DFS +; ordering, keeping them in separate trees. +; Component 3 (.second_isolated): unreachable root; defines %k = #99, uses +; %k, then jumps to .first_isolated. The jump +; is a cross-edge (.first_isolated was already +; visited as its own root), so the constant is +; never propagated into component 1. +; +; Expected: #99 is propagated to the use inside .second_isolated (same +; component), but the use in .first_isolated remains unchanged. +func @input { +.entry + ret +.first_isolated + call @use %k + j .first_isolated +.noise + j .noise +.second_isolated + $64 %k = $64 #99 + call @use %k + j .first_isolated +} + +func @expected { +.entry + ret +.first_isolated + call @use %k + j .first_isolated +.noise + j .noise +.second_isolated + $64 %k = $64 #99 + call @use $64 #99 + j .first_isolated +} From 9fa7a539622221e3c2eefde483404091db0eb041 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 08:38:02 +0000 Subject: [PATCH 6/7] Update cross_component_def: first jumps to second, second jumps to noise https://claude.ai/code/session_0165cPFCURfazKMvWB6yPZBa --- .../cross_component_def.usm | 36 +++++++------------ 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/opt/testdata/constant_propagation/cross_component_def.usm b/opt/testdata/constant_propagation/cross_component_def.usm index 915aae1..9a3d00e 100644 --- a/opt/testdata/constant_propagation/cross_component_def.usm +++ b/opt/testdata/constant_propagation/cross_component_def.usm @@ -1,38 +1,28 @@ func @use $64 %reg -; %k is defined only in .second_isolated (#99) and used in .first_isolated. -; .second_isolated jumps to .first_isolated, which might suggest the constant -; reaches the use — but CP must not propagate it across component boundaries. +; %k is defined in .second_isolated (#99) and used in .first_isolated. +; .first_isolated jumps to .second_isolated, which jumps to .noise. ; -; Block ordering determines DFS component membership: +; All three blocks belong to the same DFS component (rooted at .first_isolated). +; However, .first_isolated is the root and is therefore processed first in the +; DFS traversal — before .second_isolated defines %k. CP must not propagate +; the constant backwards to the usage in .first_isolated. ; -; Component 0 (.entry): reachable root; just returns. -; Component 1 (.first_isolated): unreachable root; uses %k — but %k has no -; definition within this component. -; Component 2 (.noise): unreachable self-loop; sits between the two -; isolated components so that .second_isolated -; is not adjacent to .first_isolated in DFS -; ordering, keeping them in separate trees. -; Component 3 (.second_isolated): unreachable root; defines %k = #99, uses -; %k, then jumps to .first_isolated. The jump -; is a cross-edge (.first_isolated was already -; visited as its own root), so the constant is -; never propagated into component 1. -; -; Expected: #99 is propagated to the use inside .second_isolated (same -; component), but the use in .first_isolated remains unchanged. +; Expected: #99 is propagated to the use inside .second_isolated (definition +; precedes the use in DFS traversal order), but the use in .first_isolated +; remains unchanged (processed before the definition is pushed). func @input { .entry ret .first_isolated call @use %k - j .first_isolated + j .second_isolated .noise j .noise .second_isolated $64 %k = $64 #99 call @use %k - j .first_isolated + j .noise } func @expected { @@ -40,11 +30,11 @@ func @expected { ret .first_isolated call @use %k - j .first_isolated + j .second_isolated .noise j .noise .second_isolated $64 %k = $64 #99 call @use $64 #99 - j .first_isolated + j .noise } From 9448dd3637e5f26479a9ef0496bacdc09dafb768 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 11:16:38 +0000 Subject: [PATCH 7/7] Simplify CP to only process reachable blocks (Dfs from entry) Replaces DfsForest with Dfs(0) so only code reachable from the entry block is transformed. Removes all connected-component tracking; define() now checks only reachable definitions when deciding whether to propagate a constant. Unreachable blocks are left unchanged. https://claude.ai/code/session_0165cPFCURfazKMvWB6yPZBa --- opt/constant_propagation.go | 95 ++++++------------- .../cross_component_def.usm | 40 -------- .../unreachable_redef.usm | 14 +-- 3 files changed, 36 insertions(+), 113 deletions(-) delete mode 100644 opt/testdata/constant_propagation/cross_component_def.usm diff --git a/opt/constant_propagation.go b/opt/constant_propagation.go index 263e78c..87f871d 100644 --- a/opt/constant_propagation.go +++ b/opt/constant_propagation.go @@ -48,40 +48,31 @@ func newCPNotSupportedError(instruction *gen.InstructionInfo) core.ResultList { const cpBlockSeparator = ^uint(0) // cpReachingConstants tracks, for each register, the constant value of its -// reaching definition at the current position in a DFS forest traversal. -// -// The design mirrors ReachingDefinitionsSet in opt/ssa: a per-register stack -// holds the sequence of constant values introduced along the path from the -// DFS tree root to the current block, and pushBlock/popBlock scope those -// values to the block's DFS subtree. +// reaching definition at the current position in a DFS traversal. type cpReachingConstants struct { // Per-register stacks of reaching constant values. Indices are assigned // lazily via registersToIndex the first time a register is defined. registerStacks []stack.Stack[*gen.ImmediateInfo] // Records which register indices were pushed in each block, with - // cpBlockSeparator values marking block boundaries. Mirrors - // ReachingDefinitionsSet.registerDefinitionPushes in opt/ssa. + // cpBlockSeparator values marking block boundaries. registerPushes stack.Stack[uint] // Maps each *RegisterInfo to its index in registerStacks. Entries are // added on demand in define(). registersToIndex map[*gen.RegisterInfo]uint - // Maps each basic block to the index of its DFS tree root (its component - // ID). Used by define() to determine whether a definition belongs to the - // same connected component as the block currently being processed. - blockComponent map[*gen.BasicBlockInfo]uint + // Set of reachable blocks (from entry). Used by define() to ignore + // definitions in unreachable blocks when checking eligibility. + reachable map[*gen.BasicBlockInfo]bool } -func newCPReachingConstants( - blockComponent map[*gen.BasicBlockInfo]uint, -) cpReachingConstants { +func newCPReachingConstants(reachable map[*gen.BasicBlockInfo]bool) cpReachingConstants { return cpReachingConstants{ registerStacks: []stack.Stack[*gen.ImmediateInfo]{}, registerPushes: stack.New[uint](), registersToIndex: make(map[*gen.RegisterInfo]uint), - blockComponent: blockComponent, + reachable: reachable, } } @@ -101,21 +92,16 @@ func (c *cpReachingConstants) get(reg *gen.RegisterInfo) *gen.ImmediateInfo { // define records a new constant reaching definition for reg in currentBlock. // -// A constant is pushed onto reg's stack only when every definition of reg in -// the same connected component as currentBlock is in currentBlock itself. This -// covers two cases uniformly: -// - Exactly one in-component definition, in currentBlock: it dominates all -// uses in well-formed code, so the stack value is always the true reaching -// value. -// - Multiple in-component definitions, all in currentBlock: they are -// sequential redefinitions within the same block. Each define call pushes -// one value; later definitions shadow earlier ones on the stack, and all -// are popped together when the block is exited. +// A constant is only pushed when every reachable definition of reg is in +// currentBlock. Definitions in unreachable blocks are ignored. This covers +// two cases: +// - Exactly one reachable definition, in currentBlock: it dominates all +// uses, so the stack value is the true reaching value. +// - Multiple reachable definitions, all in currentBlock: sequential +// redefinitions within the same block; later ones shadow earlier ones. // -// Any in-component definition outside currentBlock disqualifies the register: -// DFS ancestry does not imply domination at join points, so propagating such a -// value would be unsound. Definitions in other components (dead code, isolated -// loops) do not count and do not disqualify the register. +// Any reachable definition in a different block disqualifies the register, +// because DFS ancestry does not imply domination at join points. func (c *cpReachingConstants) define( reg *gen.RegisterInfo, imm *gen.ImmediateInfo, @@ -125,9 +111,8 @@ func (c *cpReachingConstants) define( return } - currentComponent := c.blockComponent[currentBlock] for _, def := range reg.Definitions { - if c.blockComponent[def.BasicBlockInfo] == currentComponent && def.BasicBlockInfo != currentBlock { + if c.reachable[def.BasicBlockInfo] && def.BasicBlockInfo != currentBlock { return } } @@ -180,9 +165,6 @@ func cpProcessInstruction( } // After substitution, record the reaching constant for each target. - // PropagateConstants is called after substitution so that instructions - // whose arguments just became constants (e.g. a move whose source was - // just replaced) can propagate the constant to their own targets. for _, def := range cpInstruction.PropagateConstants(instruction) { reaching.define(def.Register, def.Immediate, instruction.BasicBlockInfo) } @@ -193,47 +175,32 @@ func cpProcessInstruction( // ConstantPropagation replaces all uses of registers that are provably // assigned a constant immediate value with that immediate directly. // -// The pass uses DfsForest to traverse all basic blocks in a single unified -// DFS, covering every connected component of the CFG including unreachable -// blocks (isolated loops, dead code). Within each DFS tree the -// ancestor-before-descendant property ensures that any definition dominating -// a use is on the DFS stack when that use is processed. +// The pass performs a DFS from the entry block (block 0), visiting only +// reachable blocks. Unreachable blocks are skipped entirely. // -// define() only pushes a constant for a register when all of its definitions -// in the same connected component are confined to the current block. This -// keeps the analysis sound for both SSA and non-SSA code: registers with -// definitions spanning multiple blocks in the same component are skipped -// because DFS ancestry does not imply domination at join points. +// Within the DFS tree, define() only pushes a constant for a register when +// all of its definitions are confined to the current block. This keeps the +// analysis sound: registers defined in multiple blocks are skipped because +// DFS ancestry does not imply domination at join points. // // All instructions in the function must implement // ConstantPropagationSupportedInstruction. func ConstantPropagation(function *gen.FunctionInfo) core.ResultList { cfInfo := gen.NewFunctionControlFlowInfo(function) n := uint(len(cfInfo.BasicBlocks)) - forest := cfInfo.ControlFlowGraph.DfsForest() - - // Compute the component ID for each block: the index of its DFS tree root. - // Since parents always have a lower preorder than their children in a DFS - // tree, iterating in preorder lets us propagate component IDs in one pass. - componentOf := make([]uint, n) - for preorder := uint(0); preorder < n; preorder++ { - node := forest.PreOrderReversed[preorder] - if forest.Parent[node] == node { - componentOf[node] = node - } else { - componentOf[node] = componentOf[forest.Parent[node]] - } - } + dfs := cfInfo.ControlFlowGraph.Dfs(0) - blockComponent := make(map[*gen.BasicBlockInfo]uint, n) - for i, block := range cfInfo.BasicBlocks { - blockComponent[block] = componentOf[uint(i)] + reachable := make(map[*gen.BasicBlockInfo]bool, n) + for _, event := range dfs.Timeline { + if event < n { + reachable[cfInfo.BasicBlocks[event]] = true + } } - reaching := newCPReachingConstants(blockComponent) + reaching := newCPReachingConstants(reachable) results := core.ResultList{} - for _, event := range forest.Timeline { + for _, event := range dfs.Timeline { if event >= n { reaching.popBlock() continue diff --git a/opt/testdata/constant_propagation/cross_component_def.usm b/opt/testdata/constant_propagation/cross_component_def.usm deleted file mode 100644 index 9a3d00e..0000000 --- a/opt/testdata/constant_propagation/cross_component_def.usm +++ /dev/null @@ -1,40 +0,0 @@ -func @use $64 %reg - -; %k is defined in .second_isolated (#99) and used in .first_isolated. -; .first_isolated jumps to .second_isolated, which jumps to .noise. -; -; All three blocks belong to the same DFS component (rooted at .first_isolated). -; However, .first_isolated is the root and is therefore processed first in the -; DFS traversal — before .second_isolated defines %k. CP must not propagate -; the constant backwards to the usage in .first_isolated. -; -; Expected: #99 is propagated to the use inside .second_isolated (definition -; precedes the use in DFS traversal order), but the use in .first_isolated -; remains unchanged (processed before the definition is pushed). -func @input { -.entry - ret -.first_isolated - call @use %k - j .second_isolated -.noise - j .noise -.second_isolated - $64 %k = $64 #99 - call @use %k - j .noise -} - -func @expected { -.entry - ret -.first_isolated - call @use %k - j .second_isolated -.noise - j .noise -.second_isolated - $64 %k = $64 #99 - call @use $64 #99 - j .noise -} diff --git a/opt/testdata/constant_propagation/unreachable_redef.usm b/opt/testdata/constant_propagation/unreachable_redef.usm index e9d3d44..b1b3689 100644 --- a/opt/testdata/constant_propagation/unreachable_redef.usm +++ b/opt/testdata/constant_propagation/unreachable_redef.usm @@ -1,15 +1,11 @@ func @use $64 %reg ; %k has two definitions: #42 in .entry (reachable) and #0 in .isolated -; (an isolated self-loop, never jumped to from outside). The two definitions -; belong to different connected components, so each component's CP run sees -; only its own reachable definitions. +; (an isolated self-loop, never jumped to from outside). ; -; Component 1 (.entry + .end): only %k = #42 is reachable, so CP propagates -; #42 to "call @use %k" in .end. -; -; Component 2 (.isolated): only %k = #0 is reachable within this component, -; so CP propagates #0 to "call @use %k" inside .isolated. +; CP only processes reachable blocks (DFS from root). The .isolated block is +; skipped entirely, so %k = #42 propagates to .end, but nothing changes in +; .isolated. func @input { .entry $64 %k = $64 #42 @@ -31,7 +27,7 @@ func @expected { .isolated $64 %k = $64 #0 $64 %j = $64 #7 - call @use $64 #0 + call @use %k j .isolated .end call @use $64 #42