Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions gen/basic_block_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,19 @@ func (b *BasicBlockInfo) AppendBasicBlock(otherBlock *BasicBlockInfo) {
b.NextBlock = otherBlock
}

// InsertBeforeTerminator inserts an instruction before the last instruction
// (terminator) in the basic block. If the block is empty, the instruction is
// appended as the only instruction.
func (b *BasicBlockInfo) InsertBeforeTerminator(instruction *InstructionInfo) {
instruction.BasicBlockInfo = b
n := len(b.Instructions)
if n == 0 {
b.Instructions = append(b.Instructions, instruction)
return
}
b.Instructions = slices.Insert(b.Instructions, n-1, instruction)
}

func (b *BasicBlockInfo) AppendForwardEdge(otherBlock *BasicBlockInfo) {
b.ForwardEdges = append(b.ForwardEdges, otherBlock)
otherBlock.BackwardEdges = append(otherBlock.BackwardEdges, b)
Expand Down
6 changes: 6 additions & 0 deletions usm.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ var targets = transform.NewTargetCollection(
TargetName: "usm",
Transform: usmssa.TransformFileToSsaForm,
},
&transform.Transformation{
Names: []string{"out-of-ssa"},
Description: "Removes phi instructions by inserting copy instructions in predecessor blocks",
TargetName: "usm",
Transform: usmssa.TransformFileOutOfSsaForm,
},
&transform.Transformation{
Names: []string{"aarch64", "arm64"},
Description: "Converts the universal assembly to matching machine specific AArch64 assembly",
Expand Down
122 changes: 122 additions & 0 deletions usm/ssa/critical_edge_splitting.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package usmssa

import (
"fmt"
"slices"

"alon.kr/x/usm/gen"
ssaopt "alon.kr/x/usm/opt/ssa"
usmisa "alon.kr/x/usm/usm/isa"
)

func newJumpInstruction(target *gen.LabelInfo) *gen.InstructionInfo {
instr := gen.NewEmptyInstructionInfo(nil)
instr.SetInstruction(usmisa.NewJump())
instr.AppendArgument(gen.NewLabelArgumentInfo(target))
return instr
}

// findBlockBefore returns the block whose NextBlock is target, or nil.
func findBlockBefore(function *gen.FunctionInfo, target *gen.BasicBlockInfo) *gen.BasicBlockInfo {
for block := function.EntryBlock; block != nil; block = block.NextBlock {
if block.NextBlock == target {
return block
}
}
return nil
}

// splitCriticalEdge splits the critical edge pred→succ by inserting a new
// intermediate block. The new block is named ".ssa_split_<counter>".
func splitCriticalEdge(
function *gen.FunctionInfo,
pred *gen.BasicBlockInfo,
succ *gen.BasicBlockInfo,
splitCounter *int,
) {
// Create split block with a deterministic label.
splitLabelName := fmt.Sprintf(".ssa_split_%d", *splitCounter)
*splitCounter++
splitLabel := &gen.LabelInfo{Name: splitLabelName}
function.Labels.NewLabel(splitLabel)

splitBlock := gen.NewEmptyBasicBlockInfo(function)
splitBlock.SetLabel(splitLabel)
splitBlock.AppendInstruction(newJumpInstruction(succ.Label))

// Determine whether the edge is represented as an explicit label argument
// in the terminator, or as an implicit fall-through (pred.NextBlock == succ).
terminator := pred.Instructions[len(pred.Instructions)-1]
isExplicitBranch := false
for i, arg := range terminator.Arguments {
if labelArg, ok := arg.(*gen.LabelArgumentInfo); ok {
if labelArg.Label.BasicBlock == succ {
terminator.Arguments[i] = gen.NewLabelArgumentInfo(splitLabel)
isExplicitBranch = true
break
}
}
}

// Insert split block in the linked list.
if isExplicitBranch {
// Insert split block right before succ so the linear order is sensible.
blockBeforeSucc := findBlockBefore(function, succ)
if blockBeforeSucc != nil {
blockBeforeSucc.AppendBasicBlock(splitBlock)
} else {
// succ is the entry block; insert after pred as a fallback.
pred.AppendBasicBlock(splitBlock)
}
} else {
// Fall-through edge: insert split block between pred and succ so that
// pred's fall-through now goes to split block → succ.
pred.AppendBasicBlock(splitBlock)
}

// Update phi instructions in succ: replace references to pred with split block.
for _, instr := range succ.Instructions {
if _, isPhi := instr.Definition.(ssaopt.PhiInstructionDefinition); !isPhi {
break
}
for i, arg := range instr.Arguments {
if labelArg, ok := arg.(*gen.LabelArgumentInfo); ok {
if labelArg.Label.BasicBlock == pred {
instr.Arguments[i] = gen.NewLabelArgumentInfo(splitLabel)
}
}
}
}

// Update CFG edges: pred→succ becomes pred→split, split→succ.
pred.ForwardEdges = slices.DeleteFunc(
pred.ForwardEdges,
func(b *gen.BasicBlockInfo) bool { return b == succ },
)
succ.BackwardEdges = slices.DeleteFunc(
succ.BackwardEdges,
func(b *gen.BasicBlockInfo) bool { return b == pred },
)
pred.AppendForwardEdge(splitBlock)
splitBlock.AppendForwardEdge(succ)
}

// splitCriticalEdgesInFunction splits all critical edges in the function.
// A critical edge is an edge from a block with multiple successors to a block
// with multiple predecessors.
func splitCriticalEdgesInFunction(function *gen.FunctionInfo, splitCounter *int) {
blocks := function.CollectBasicBlocks()
for _, pred := range blocks {
if len(pred.ForwardEdges) <= 1 {
continue
}
// Snapshot forward edges before modifying.
succs := slices.Clone(pred.ForwardEdges)
for _, succ := range succs {
if len(succ.BackwardEdges) <= 1 {
continue
}
splitCriticalEdge(function, pred, succ, splitCounter)
}
}
}
82 changes: 82 additions & 0 deletions usm/ssa/parallel_copy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package usmssa

import (
"fmt"

"alon.kr/x/usm/gen"
usmisa "alon.kr/x/usm/usm/isa"
)

type parallelCopyEntry struct {
dst *gen.RegisterInfo
srcArg gen.ArgumentInfo
}

func newMoveInstruction(dst *gen.RegisterInfo, srcArg gen.ArgumentInfo) *gen.InstructionInfo {
instr := gen.NewEmptyInstructionInfo(nil)
instr.SetInstruction(usmisa.NewMove())
target := gen.NewTargetInfo(dst)
instr.AppendTarget(&target)
instr.AppendArgument(srcArg)
return instr
}

// sequentializeParallelCopies converts a set of parallel copies into a correct
// sequential list of move instructions. Cycles are broken using fresh
// temporaries named "%phi_tmp_<N>".
func sequentializeParallelCopies(
copies []parallelCopyEntry,
function *gen.FunctionInfo,
tmpCounter *int,
) []*gen.InstructionInfo {
if len(copies) == 0 {
return nil
}

// Build the set of destination registers.
dstSet := make(map[*gen.RegisterInfo]bool, len(copies))
for _, c := range copies {
dstSet[c.dst] = true
}

// Pre-save any source register that is also a destination register.
// This avoids incorrect overwrites when copies form a cycle (e.g. a swap).
saved := make(map[*gen.RegisterInfo]*gen.RegisterInfo)
var preInstructions []*gen.InstructionInfo

for _, c := range copies {
srcRegArg, ok := c.srcArg.(*gen.RegisterArgumentInfo)
if !ok {
continue // immediates cannot form cycles
}
srcReg := srcRegArg.Register
if !dstSet[srcReg] {
continue // this source is not overwritten by any copy
}
if _, alreadySaved := saved[srcReg]; alreadySaved {
continue // already saved in a previous iteration
}

tmpName := fmt.Sprintf("%%phi_tmp_%d", *tmpCounter)
*tmpCounter++
tmpReg := gen.NewRegisterInfo(tmpName, srcReg.Type)
function.Registers.NewRegister(tmpReg)
saved[srcReg] = tmpReg

preInstructions = append(preInstructions, newMoveInstruction(tmpReg, gen.NewRegisterArgumentInfo(srcReg)))
}

// Emit the actual copies, substituting temporaries where the source was saved.
var copyInstructions []*gen.InstructionInfo
for _, c := range copies {
srcArg := c.srcArg
if srcRegArg, ok := c.srcArg.(*gen.RegisterArgumentInfo); ok {
if tmpReg, wasSaved := saved[srcRegArg.Register]; wasSaved {
srcArg = gen.NewRegisterArgumentInfo(tmpReg)
}
}
copyInstructions = append(copyInstructions, newMoveInstruction(c.dst, srcArg))
}

return append(preInstructions, copyInstructions...)
}
101 changes: 101 additions & 0 deletions usm/ssa/ssa_destruction.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package usmssa

import (
"alon.kr/x/usm/core"
"alon.kr/x/usm/gen"
ssaopt "alon.kr/x/usm/opt/ssa"
"alon.kr/x/usm/transform"
)

// collectPhiInstructions returns the phi instructions at the start of a block.
// Phi instructions are always at the beginning of a block in SSA form.
func collectPhiInstructions(block *gen.BasicBlockInfo) []*gen.InstructionInfo {
var phis []*gen.InstructionInfo
for _, instr := range block.Instructions {
if _, isPhi := instr.Definition.(ssaopt.PhiInstructionDefinition); !isPhi {
break
}
phis = append(phis, instr)
}
return phis
}

// collectCopiesForPredecessor returns the parallel copy set for all phis in
// block that have an incoming value from pred.
func collectCopiesForPredecessor(
phis []*gen.InstructionInfo,
pred *gen.BasicBlockInfo,
) []parallelCopyEntry {
var copies []parallelCopyEntry
for _, phi := range phis {
dst := phi.Targets[0].Register
// Phi arguments are pairs: [label0, value0, label1, value1, ...]
for i := 0; i+1 < len(phi.Arguments); i += 2 {
labelArg, ok := phi.Arguments[i].(*gen.LabelArgumentInfo)
if !ok {
continue
}
if labelArg.Label.BasicBlock != pred {
continue
}
copies = append(copies, parallelCopyEntry{dst: dst, srcArg: phi.Arguments[i+1]})
break
}
}
return copies
}

// FunctionOutOfSsaForm removes all phi instructions from function, replacing
// them with copy instructions inserted in each predecessor block.
func FunctionOutOfSsaForm(function *gen.FunctionInfo) core.ResultList {
splitCounter := 0
tmpCounter := 0

splitCriticalEdgesInFunction(function, &splitCounter)

for block := function.EntryBlock; block != nil; block = block.NextBlock {
phis := collectPhiInstructions(block)
if len(phis) == 0 {
continue
}

// For each predecessor, build and insert the parallel copy set.
for _, pred := range block.BackwardEdges {
copies := collectCopiesForPredecessor(phis, pred)
instrs := sequentializeParallelCopies(copies, function, &tmpCounter)
for _, instr := range instrs {
pred.InsertBeforeTerminator(instr)
}
}

// Remove the phi instructions from this block.
for _, phi := range phis {
for _, target := range phi.Targets {
target.Register.RemoveDefinition(phi)
}
block.RemoveInstruction(phi)
}
}

return core.ResultList{}
}

// FileOutOfSsaForm applies FunctionOutOfSsaForm to every defined function.
func FileOutOfSsaForm(file *gen.FileInfo) core.ResultList {
results := core.ResultList{}
for _, function := range file.Functions {
if function.IsDefined() {
curResults := FunctionOutOfSsaForm(function)
results.Extend(&curResults)
}
}
return results
}

// TransformFileOutOfSsaForm is the transform-pipeline adapter.
func TransformFileOutOfSsaForm(
data *transform.TargetData,
) (*transform.TargetData, core.ResultList) {
results := FileOutOfSsaForm(data.Code)
return data, results
}
Loading
Loading