diff --git a/gen/basic_block_info.go b/gen/basic_block_info.go index 2d32c8d..e496191 100644 --- a/gen/basic_block_info.go +++ b/gen/basic_block_info.go @@ -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) diff --git a/usm.go b/usm.go index 3ebdc7a..7131159 100644 --- a/usm.go +++ b/usm.go @@ -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", diff --git a/usm/ssa/critical_edge_splitting.go b/usm/ssa/critical_edge_splitting.go new file mode 100644 index 0000000..ad088a9 --- /dev/null +++ b/usm/ssa/critical_edge_splitting.go @@ -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_". +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) + } + } +} diff --git a/usm/ssa/parallel_copy.go b/usm/ssa/parallel_copy.go new file mode 100644 index 0000000..600ce0e --- /dev/null +++ b/usm/ssa/parallel_copy.go @@ -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_". +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...) +} diff --git a/usm/ssa/ssa_destruction.go b/usm/ssa/ssa_destruction.go new file mode 100644 index 0000000..939565f --- /dev/null +++ b/usm/ssa/ssa_destruction.go @@ -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 +} diff --git a/usm/ssa/ssa_destruction_test.go b/usm/ssa/ssa_destruction_test.go new file mode 100644 index 0000000..bee3870 --- /dev/null +++ b/usm/ssa/ssa_destruction_test.go @@ -0,0 +1,67 @@ +package usmssa_test + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "alon.kr/x/usm/core" + "alon.kr/x/usm/gen" + "alon.kr/x/usm/lex" + "alon.kr/x/usm/parse" + usmmanagers "alon.kr/x/usm/usm/managers" + usmssa "alon.kr/x/usm/usm/ssa" + "github.com/stretchr/testify/assert" +) + +const ( + inputFuncName = "@input" + expectedFuncName = "@expected" +) + +func generateFileInfo(t *testing.T, source string) *gen.FileInfo { + t.Helper() + + srcView := core.NewSourceView(source) + tkns, err := lex.NewTokenizer().Tokenize(srcView) + assert.NoError(t, err) + + tknView := parse.NewTokenView(tkns) + fileNode, result := parse.NewFileParser().Parse(&tknView) + assert.Nil(t, result) + + ctx := usmmanagers.NewGenerationContext() + generator := gen.NewFileGenerator() + info, results := generator.Generate(ctx, srcView.Ctx(), fileNode) + assert.True(t, results.IsEmpty(), "Failed to generate file info") + + return info +} + +func TestSsaDestruction(t *testing.T) { + basePath := filepath.Join("testdata", "ssa_destruction") + entries, err := os.ReadDir(basePath) + assert.NoError(t, err) + assert.NotEmpty(t, entries) + + for _, entry := range entries { + testName := strings.TrimSuffix(entry.Name(), filepath.Ext(entry.Name())) + t.Run(testName, func(t *testing.T) { + content, err := os.ReadFile(filepath.Join(basePath, entry.Name())) + assert.NoError(t, err) + + file := generateFileInfo(t, string(content)) + inputFunc := file.GetFunction(inputFuncName) + assert.NotNil(t, inputFunc) + expectedFunc := file.GetFunction(expectedFuncName) + assert.NotNil(t, expectedFunc) + + results := usmssa.FunctionOutOfSsaForm(inputFunc) + assert.True(t, results.IsEmpty(), "SSA destruction returned errors") + + inputFunc.Name = expectedFuncName + assert.Equal(t, expectedFunc.String(), inputFunc.String()) + }) + } +} diff --git a/usm/ssa/testdata/ssa_destruction/critical_edge.usm b/usm/ssa/testdata/ssa_destruction/critical_edge.usm new file mode 100644 index 0000000..8c885e0 --- /dev/null +++ b/usm/ssa/testdata/ssa_destruction/critical_edge.usm @@ -0,0 +1,24 @@ +func $64 @input { +.entry + $64 %cond = $64 #1 + jz %cond .end +.mid + j .end +.end + $64 %x = phi .entry $64 #0 .mid $64 #1 + ret %x +} + +func $64 @expected { +.entry + $64 %cond = $64 #1 + jz %cond .ssa_split_0 +.mid + $64 %x = $64 #1 + j .end +.ssa_split_0 + $64 %x = $64 #0 + j .end +.end + ret %x +} diff --git a/usm/ssa/testdata/ssa_destruction/simple.usm b/usm/ssa/testdata/ssa_destruction/simple.usm new file mode 100644 index 0000000..c690e80 --- /dev/null +++ b/usm/ssa/testdata/ssa_destruction/simple.usm @@ -0,0 +1,26 @@ +func $64 @input { +.entry + $64 %n = $64 #1 + jz %n .zero +.nonzero + j .end +.zero + j .end +.end + $64 %m = phi .zero $64 #1 .nonzero $64 #0 + ret %m +} + +func $64 @expected { +.entry + $64 %n = $64 #1 + jz %n .zero +.nonzero + $64 %m = $64 #0 + j .end +.zero + $64 %m = $64 #1 + j .end +.end + ret %m +} diff --git a/usm/ssa/testdata/ssa_destruction/swap.usm b/usm/ssa/testdata/ssa_destruction/swap.usm new file mode 100644 index 0000000..b660edb --- /dev/null +++ b/usm/ssa/testdata/ssa_destruction/swap.usm @@ -0,0 +1,25 @@ +func $64 @input { +.entry + $64 %x = $64 #0 + $64 %y = $64 #1 + j .loop +.loop + $64 %a = phi .entry %x .loop %b + $64 %b = phi .entry %y .loop %a + j .loop +} + +func $64 @expected { +.entry + $64 %x = $64 #0 + $64 %y = $64 #1 + $64 %a = %x + $64 %b = %y + j .loop +.loop + $64 %phi_tmp_0 = %b + $64 %phi_tmp_1 = %a + $64 %a = %phi_tmp_0 + $64 %b = %phi_tmp_1 + j .loop +}