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..87f871d --- /dev/null +++ b/opt/constant_propagation.go @@ -0,0 +1,218 @@ +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 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. + registerPushes stack.Stack[uint] + + // Maps each *RegisterInfo to its index in registerStacks. Entries are + // added on demand in define(). + registersToIndex map[*gen.RegisterInfo]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(reachable map[*gen.BasicBlockInfo]bool) cpReachingConstants { + return cpReachingConstants{ + registerStacks: []stack.Stack[*gen.ImmediateInfo]{}, + registerPushes: stack.New[uint](), + registersToIndex: make(map[*gen.RegisterInfo]uint), + reachable: reachable, + } +} + +// 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 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 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, + currentBlock *gen.BasicBlockInfo, +) { + if imm == nil { + return + } + + for _, def := range reg.Definitions { + if c.reachable[def.BasicBlockInfo] && def.BasicBlockInfo != currentBlock { + return + } + } + + idx, ok := c.registersToIndex[reg] + if !ok { + idx = uint(len(c.registerStacks)) + c.registersToIndex[reg] = idx + c.registerStacks = append(c.registerStacks, stack.New[*gen.ImmediateInfo]()) + } + 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. + 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 performs a DFS from the entry block (block 0), visiting only +// reachable blocks. Unreachable blocks are skipped entirely. +// +// 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)) + dfs := cfInfo.ControlFlowGraph.Dfs(0) + + reachable := make(map[*gen.BasicBlockInfo]bool, n) + for _, event := range dfs.Timeline { + if event < n { + reachable[cfInfo.BasicBlocks[event]] = true + } + } + + reaching := newCPReachingConstants(reachable) + results := core.ResultList{} + + for _, event := range dfs.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..b1b3689 --- /dev/null +++ b/opt/testdata/constant_propagation/unreachable_redef.usm @@ -0,0 +1,35 @@ +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). +; +; 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 + 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 %k + 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) + }) +}