From 3301cf55425d1f4589715af9018d852b6ebf2ceb Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 2 Jul 2025 16:55:24 +0200 Subject: [PATCH] Improved ssa compiler --- lib/io/write_linux_arm.q | 4 +- lib/io/write_linux_x86.q | 4 +- lib/io/write_mac.q | 4 +- lib/io/write_windows.q | 2 +- src/arm/Registers.go | 1 + src/build/Arch.go | 16 +++++- src/build/SetArch.go | 15 ------ src/cli/help.txt | 1 - src/compiler/showSSA.go | 28 ++++++---- src/core/CheckDeadCode.go | 2 +- src/core/Compile.go | 42 ++++----------- src/core/Evaluate.go | 64 ++++++++++++++++------- src/core/Function.go | 20 ++++---- src/core/count.go | 8 --- src/core/errors.go | 15 ++++++ src/core/ssaToAsm.go | 46 ----------------- src/core/ssaValuesToRegisters.go | 52 ------------------- src/cpu/CPU.go | 1 + src/cpu/Register.go | 2 +- src/ssa/BinaryOp.go | 5 ++ src/ssa/Block.go | 2 +- src/ssa/Bytes.go | 7 ++- src/ssa/Bytes_test.go | 2 +- src/ssa/Call.go | 41 ++++++++++++--- src/ssa/Function.go | 6 +++ src/ssa/ID.go | 11 ++++ src/ssa/IR.go | 31 +++++------- src/ssa/Int.go | 5 ++ src/ssa/Liveness.go | 10 ++-- src/ssa/NoLiveness.go | 6 +++ src/ssa/Parameter.go | 5 ++ src/ssa/Return.go | 40 ++++++++++++--- src/ssa/Source.go | 12 +++-- src/ssa/Struct.go | 69 +++++++++++++++++++++++++ src/ssa/StructField.go | 45 ++++++++++++++++ src/ssa/Syscall.go | 36 ++++++++++--- src/ssa/Value.go | 23 ++++++--- src/ssa2asm/Compiler.go | 13 +++++ src/ssa2asm/Compiler_test.go | 25 +++++++++ src/ssa2asm/Count.go | 9 ++++ src/{core => ssa2asm}/CreateLabel.go | 4 +- src/ssa2asm/GenerateAssembly.go | 76 ++++++++++++++++++++++++++++ src/ssa2asm/ValueToRegister.go | 54 ++++++++++++++++++++ src/types/Common.go | 14 ++++- src/types/Field.go | 17 +++++++ src/types/Parse.go | 2 + src/types/Struct.go | 50 ++++++++++++++++++ src/types/types_test.go | 4 +- src/x86/Registers.go | 1 + 49 files changed, 690 insertions(+), 262 deletions(-) delete mode 100644 src/build/SetArch.go delete mode 100644 src/core/count.go delete mode 100644 src/core/ssaToAsm.go delete mode 100644 src/core/ssaValuesToRegisters.go create mode 100644 src/ssa/ID.go create mode 100644 src/ssa/NoLiveness.go create mode 100644 src/ssa/Struct.go create mode 100644 src/ssa/StructField.go create mode 100644 src/ssa2asm/Compiler.go create mode 100644 src/ssa2asm/Compiler_test.go create mode 100644 src/ssa2asm/Count.go rename src/{core => ssa2asm}/CreateLabel.go (83%) create mode 100644 src/ssa2asm/GenerateAssembly.go create mode 100644 src/ssa2asm/ValueToRegister.go create mode 100644 src/types/Field.go create mode 100644 src/types/Struct.go diff --git a/lib/io/write_linux_arm.q b/lib/io/write_linux_arm.q index 143b676..56c5ddc 100644 --- a/lib/io/write_linux_arm.q +++ b/lib/io/write_linux_arm.q @@ -1,3 +1,3 @@ -write(buffer []byte) -> (written int) { - return syscall(64, 1, buffer, len(buffer)) +write(buffer string) -> (written int) { + return syscall(64, 1, buffer.ptr, buffer.len) } \ No newline at end of file diff --git a/lib/io/write_linux_x86.q b/lib/io/write_linux_x86.q index 77c3f99..9e57a5e 100644 --- a/lib/io/write_linux_x86.q +++ b/lib/io/write_linux_x86.q @@ -1,3 +1,3 @@ -write(buffer []byte) -> (written int) { - return syscall(1, 1, buffer, len(buffer)) +write(buffer string) -> (written int) { + return syscall(1, 1, buffer.ptr, buffer.len) } \ No newline at end of file diff --git a/lib/io/write_mac.q b/lib/io/write_mac.q index cc67bb7..5d5bc8f 100644 --- a/lib/io/write_mac.q +++ b/lib/io/write_mac.q @@ -1,3 +1,3 @@ -write(buffer []byte) -> (written int) { - return syscall(0x2000004, 1, buffer, len(buffer)) +write(buffer string) -> (written int) { + return syscall(0x2000004, 1, buffer.ptr, buffer.len) } \ No newline at end of file diff --git a/lib/io/write_windows.q b/lib/io/write_windows.q index 2342b94..0b40b6a 100644 --- a/lib/io/write_windows.q +++ b/lib/io/write_windows.q @@ -1,4 +1,4 @@ -write(_ []byte) -> (written int) { +write(_ string) -> (written int) { return 0 } diff --git a/src/arm/Registers.go b/src/arm/Registers.go index 6945575..794c658 100644 --- a/src/arm/Registers.go +++ b/src/arm/Registers.go @@ -45,4 +45,5 @@ var CPU = cpu.CPU{ Call: []cpu.Register{X0, X1, X2, X3, X4, X5, X6}, Syscall: []cpu.Register{X8, X0, X1, X2, X3, X4, X5}, ExternCall: []cpu.Register{X0, X1, X2, X3, X4, X5, X6, X7}, + Return: []cpu.Register{X0, X1, X2}, } \ No newline at end of file diff --git a/src/build/Arch.go b/src/build/Arch.go index b9e3b84..e5a934b 100644 --- a/src/build/Arch.go +++ b/src/build/Arch.go @@ -7,4 +7,18 @@ const ( UnknownArch Arch = iota ARM X86 -) \ No newline at end of file +) + +// SetArch sets the architecture which also influences the default alignment. +func (build *Build) SetArch(arch Arch) { + build.Arch = arch + + switch arch { + case ARM: + build.MemoryAlign = 0x4000 + default: + build.MemoryAlign = 0x1000 + } + + build.FileAlign = build.MemoryAlign +} \ No newline at end of file diff --git a/src/build/SetArch.go b/src/build/SetArch.go deleted file mode 100644 index 92abdcb..0000000 --- a/src/build/SetArch.go +++ /dev/null @@ -1,15 +0,0 @@ -package build - -// SetArch sets the architecture which also influences the default alignment. -func (build *Build) SetArch(arch Arch) { - build.Arch = arch - - switch arch { - case ARM: - build.MemoryAlign = 0x4000 - default: - build.MemoryAlign = 0x1000 - } - - build.FileAlign = build.MemoryAlign -} \ No newline at end of file diff --git a/src/cli/help.txt b/src/cli/help.txt index f1aed0d..f34cd8f 100644 --- a/src/cli/help.txt +++ b/src/cli/help.txt @@ -5,7 +5,6 @@ Usage: Commands: build [directory | file] build an executable - --verbose, -v show everything run [directory | file] build and run the executable diff --git a/src/compiler/showSSA.go b/src/compiler/showSSA.go index 4b0bf92..4e5a178 100644 --- a/src/compiler/showSSA.go +++ b/src/compiler/showSSA.go @@ -2,6 +2,7 @@ package compiler import ( "fmt" + "strings" "git.urbach.dev/cli/q/src/core" "git.urbach.dev/go/color/ansi" @@ -10,18 +11,27 @@ import ( // showSSA shows the SSA IR. func showSSA(root *core.Function) { root.EachDependency(make(map[*core.Function]bool), func(f *core.Function) { - ansi.Yellow.Printf("%s:\n", f.UniqueName) + fmt.Print("# ") + ansi.Green.Print(f.UniqueName) + fmt.Print("\n\n") - for i, block := range f.Blocks { - if i != 0 { - fmt.Println("---") - } + for _, block := range f.Blocks { + ansi.Dim.Printf("| %-3s | %-30s | %-30s | %-4s |\n", "ID", "Raw", "Type", "Uses") + ansi.Dim.Printf("| %s | %s | %s | %s |\n", strings.Repeat("-", 3), strings.Repeat("-", 30), strings.Repeat("-", 30), strings.Repeat("-", 4)) for i, instr := range block.Instructions { - ansi.Dim.Printf("%-4d", i) - fmt.Printf("%-40s", instr.String()) - ansi.Cyan.Printf("%-30s", instr.Type().Name()) - ansi.Dim.Printf("%s\n", f.File.Bytes[instr.Start():instr.End()]) + ansi.Dim.Printf("| %%%-2d | ", i) + + if instr.IsConst() { + fmt.Printf("%-30s ", instr.Debug()) + } else { + ansi.Yellow.Printf("%-30s ", instr.Debug()) + } + + ansi.Dim.Print("|") + ansi.Dim.Printf(" %-30s |", instr.Type().Name()) + ansi.Dim.Printf(" %-4d |", instr.CountUsers()) + fmt.Println() } } diff --git a/src/core/CheckDeadCode.go b/src/core/CheckDeadCode.go index 6b31f64..3016dfd 100644 --- a/src/core/CheckDeadCode.go +++ b/src/core/CheckDeadCode.go @@ -7,7 +7,7 @@ import ( // CheckDeadCode checks for dead values. func (f *Function) CheckDeadCode() error { for instr := range f.Values { - if instr.IsConst() && instr.Alive() == 0 { + if instr.IsConst() && instr.CountUsers() == 0 { return errors.New(&UnusedValue{Value: instr.String()}, f.File, instr.Start()) } } diff --git a/src/core/Compile.go b/src/core/Compile.go index 2ca543a..158979d 100644 --- a/src/core/Compile.go +++ b/src/core/Compile.go @@ -1,48 +1,24 @@ package core -import ( - "git.urbach.dev/cli/q/src/ssa" - "git.urbach.dev/cli/q/src/types" -) +import "git.urbach.dev/cli/q/src/types" // Compile turns a function into machine code. func (f *Function) Compile() { - extra := 0 + offset := 0 for i, input := range f.Input { if input.Name == "_" { continue } - array, isArray := input.Typ.(*types.Array) - - if isArray { - pointer := &ssa.Parameter{ - Index: uint8(i + extra), - Name: input.Name, - Typ: &types.Pointer{To: array.Of}, - Source: input.Source, - } - - f.Append(pointer) - f.Identifiers[pointer.Name] = pointer - extra++ - - length := &ssa.Parameter{ - Index: uint8(i + extra), - Name: input.Name + ".len", - Typ: types.AnyInt, - Source: input.Source, - } - - f.Append(length) - f.Identifiers[length.Name] = length - continue - } - - input.Index = uint8(i + extra) + input.Index = uint8(offset + i) f.Append(input) f.Identifiers[input.Name] = input + structure, isStruct := input.Typ.(*types.Struct) + + if isStruct { + offset += len(structure.Fields) - 1 + } } for instr := range f.Body.Instructions { @@ -59,5 +35,5 @@ func (f *Function) Compile() { return } - f.ssaToAsm() + f.GenerateAssembly(f.IR, f.IsLeaf()) } \ No newline at end of file diff --git a/src/core/Evaluate.go b/src/core/Evaluate.go index 3a56c46..90fee68 100644 --- a/src/core/Evaluate.go +++ b/src/core/Evaluate.go @@ -27,8 +27,8 @@ func (f *Function) Evaluate(expr *expression.Expression) (ssa.Value, error) { } f.Dependencies.Add(function) - v := f.AppendFunction(function.UniqueName, function.Type) - v.Source = ssa.Source(expr.Source) + v := f.AppendFunction(function.UniqueName, function.Type, function.IsExtern()) + v.SetSource(expr.Source) return v, nil } @@ -42,14 +42,25 @@ func (f *Function) Evaluate(expr *expression.Expression) (ssa.Value, error) { } v := f.AppendInt(number) - v.Source = ssa.Source(expr.Source) + v.SetSource(expr.Source) return v, nil case token.String: data := expr.Token.Bytes(f.File.Bytes) data = Unescape(data) - v := f.AppendBytes(data) - v.Source = ssa.Source(expr.Source) + + length := f.AppendInt(len(data)) + length.SetSource(expr.Source) + + pointer := f.AppendBytes(data) + pointer.SetSource(expr.Source) + + v := f.Append(&ssa.Struct{ + Arguments: []ssa.Value{pointer, length}, + Typ: types.String, + }) + + v.SetSource(expr.Source) return v, nil } @@ -109,7 +120,7 @@ func (f *Function) Evaluate(expr *expression.Expression) (ssa.Value, error) { return nil, errors.New(&ParameterCountMismatch{Function: name, Count: len(parameters), ExpectedCount: len(fn.Input)}, f.File, expr.Source[0].Position) } - for i, param := range parameters { + for i, param := range slices.Backward(parameters) { if !types.Is(param.Type(), fn.Input[i].Typ) { return nil, errors.New(&TypeMismatch{ Encountered: param.Type().Name(), @@ -130,22 +141,39 @@ func (f *Function) Evaluate(expr *expression.Expression) (ssa.Value, error) { case token.Dot: left := expr.Children[0] right := expr.Children[1] - label := fmt.Sprintf("%s.%s", left.String(f.File.Bytes), right.String(f.File.Bytes)) + leftText := left.String(f.File.Bytes) + rightText := right.String(f.File.Bytes) + identifier, exists := f.Identifiers[leftText] + + if exists { + structure := identifier.Type().(*types.Struct) + field := structure.FieldByName(rightText) + + if field == nil { + return nil, errors.New(&UnknownStructField{StructName: structure.Name(), FieldName: rightText}, f.File, right.Token.Position) + } + + v := f.Append(&ssa.StructField{Struct: identifier, Field: field}) + v.SetSource(expr.Source) + return v, nil + } + + label := fmt.Sprintf("%s.%s", leftText, rightText) function, exists := f.All.Functions[label] - if !exists { - return nil, errors.New(&UnknownIdentifier{Name: label}, f.File, left.Token.Position) + if exists { + if function.IsExtern() { + f.Assembler.Libraries = f.Assembler.Libraries.Append(function.Package, function.Name) + } else { + f.Dependencies.Add(function) + } + + v := f.AppendFunction(function.UniqueName, function.Type, function.IsExtern()) + v.SetSource(expr.Source) + return v, nil } - if function.IsExtern() { - f.Assembler.Libraries = f.Assembler.Libraries.Append(function.Package, function.Name) - } else { - f.Dependencies.Add(function) - } - - v := f.AppendFunction(function.UniqueName, function.Type) - v.Source = ssa.Source(expr.Source) - return v, nil + return nil, errors.New(&UnknownIdentifier{Name: label}, f.File, left.Token.Position) } return nil, nil diff --git a/src/core/Function.go b/src/core/Function.go index 5125f76..71e14f7 100644 --- a/src/core/Function.go +++ b/src/core/Function.go @@ -4,10 +4,10 @@ import ( "fmt" "git.urbach.dev/cli/q/src/asm" - "git.urbach.dev/cli/q/src/cpu" "git.urbach.dev/cli/q/src/fs" "git.urbach.dev/cli/q/src/set" "git.urbach.dev/cli/q/src/ssa" + "git.urbach.dev/cli/q/src/ssa2asm" "git.urbach.dev/cli/q/src/token" "git.urbach.dev/cli/q/src/types" ) @@ -15,9 +15,9 @@ import ( // Function is the smallest unit of code. type Function struct { ssa.IR + ssa2asm.Compiler Name string Package string - UniqueName string File *fs.File Input []*ssa.Parameter Output []*ssa.Parameter @@ -25,19 +25,16 @@ type Function struct { Identifiers map[string]ssa.Value All *Environment Dependencies set.Ordered[*Function] - Assembler asm.Assembler - CPU *cpu.CPU Type *types.Function Err error - count count } // NewFunction creates a new function. func NewFunction(name string, pkg string, file *fs.File) *Function { return &Function{ - Name: name, - Package: pkg, - UniqueName: fmt.Sprintf("%s.%s", pkg, name), + Name: name, + Package: pkg, + File: file, Identifiers: make(map[string]ssa.Value, 8), IR: ssa.IR{ @@ -45,8 +42,11 @@ func NewFunction(name string, pkg string, file *fs.File) *Function { {Instructions: make([]ssa.Value, 0, 8)}, }, }, - Assembler: asm.Assembler{ - Instructions: make([]asm.Instruction, 0, 8), + Compiler: ssa2asm.Compiler{ + UniqueName: fmt.Sprintf("%s.%s", pkg, name), + Assembler: asm.Assembler{ + Instructions: make([]asm.Instruction, 0, 8), + }, }, } } diff --git a/src/core/count.go b/src/core/count.go deleted file mode 100644 index 98e9005..0000000 --- a/src/core/count.go +++ /dev/null @@ -1,8 +0,0 @@ -package core - -type counter = uint8 - -// count stores how often a certain statement appeared so we can generate a unique label from it. -type count struct { - data counter -} \ No newline at end of file diff --git a/src/core/errors.go b/src/core/errors.go index a1257fb..59d094d 100644 --- a/src/core/errors.go +++ b/src/core/errors.go @@ -63,6 +63,21 @@ func (err *UnknownIdentifier) Error() string { return fmt.Sprintf("Unknown identifier '%s'", err.Name) } +// UnknownStructField represents unknown struct fields. +type UnknownStructField struct { + StructName string + FieldName string + CorrectFieldName string +} + +func (err *UnknownStructField) Error() string { + if err.CorrectFieldName != "" { + return fmt.Sprintf("Unknown struct field '%s' in '%s', did you mean '%s'?", err.FieldName, err.StructName, err.CorrectFieldName) + } + + return fmt.Sprintf("Unknown struct field '%s' in '%s'", err.FieldName, err.StructName) +} + // UnusedValue error is created when a value is never used. type UnusedValue struct { Value string diff --git a/src/core/ssaToAsm.go b/src/core/ssaToAsm.go deleted file mode 100644 index 6a55f86..0000000 --- a/src/core/ssaToAsm.go +++ /dev/null @@ -1,46 +0,0 @@ -package core - -import ( - "git.urbach.dev/cli/q/src/asm" - "git.urbach.dev/cli/q/src/ssa" -) - -// ssaToAsm converts the SSA IR to assembler instructions. -func (f *Function) ssaToAsm() { - f.Assembler.Append(&asm.Label{Name: f.UniqueName}) - - if !f.IsLeaf() && f.UniqueName != "core.init" { - f.Assembler.Append(&asm.FunctionStart{}) - } - - for instr := range f.Values { - switch instr := instr.(type) { - case *ssa.Call: - arg := instr.Arguments[0].(*ssa.Function) - fn := f.All.Functions[arg.UniqueName] - - if fn.IsExtern() { - f.ssaValuesToRegisters(instr.Arguments[1:], f.CPU.ExternCall) - f.Assembler.Append(&asm.CallExtern{Library: fn.Package, Function: fn.Name}) - } else { - f.ssaValuesToRegisters(instr.Arguments[1:], f.CPU.Call) - f.Assembler.Append(&asm.Call{Label: fn.UniqueName}) - } - - case *ssa.Syscall: - f.ssaValuesToRegisters(instr.Arguments, f.CPU.Syscall) - f.Assembler.Append(&asm.Syscall{}) - - case *ssa.Return: - f.Assembler.Append(&asm.Return{}) - } - } - - if !f.IsLeaf() && f.UniqueName != "core.init" { - f.Assembler.Append(&asm.FunctionEnd{}) - } - - if f.UniqueName != "core.exit" { - f.Assembler.Append(&asm.Return{}) - } -} \ No newline at end of file diff --git a/src/core/ssaValuesToRegisters.go b/src/core/ssaValuesToRegisters.go deleted file mode 100644 index 2932070..0000000 --- a/src/core/ssaValuesToRegisters.go +++ /dev/null @@ -1,52 +0,0 @@ -package core - -import ( - "slices" - - "git.urbach.dev/cli/q/src/asm" - "git.urbach.dev/cli/q/src/cpu" - "git.urbach.dev/cli/q/src/ssa" -) - -// ssaValuesToRegisters generates assembler instructions to move the SSA values to the given registers. -func (f *Function) ssaValuesToRegisters(args []ssa.Value, registers []cpu.Register) { - extra := 0 - - for _, arg := range args { - switch arg.(type) { - case *ssa.Bytes: - extra++ - } - } - - for i, arg := range slices.Backward(args) { - switch arg := arg.(type) { - case *ssa.Int: - f.Assembler.Append(&asm.MoveRegisterNumber{ - Destination: registers[i+extra], - Number: arg.Int, - }) - case *ssa.Parameter: - f.Assembler.Append(&asm.MoveRegisterRegister{ - Destination: registers[i+extra], - Source: f.CPU.Call[arg.Index], - }) - case *ssa.Bytes: - f.count.data++ - label := f.CreateLabel("data", f.count.data) - f.Assembler.SetData(label.Name, arg.Bytes) - - f.Assembler.Append(&asm.MoveRegisterNumber{ - Destination: registers[i+extra], - Number: len(arg.Bytes), - }) - - extra-- - - f.Assembler.Append(&asm.MoveRegisterLabel{ - Destination: registers[i+extra], - Label: label.Name, - }) - } - } -} \ No newline at end of file diff --git a/src/cpu/CPU.go b/src/cpu/CPU.go index 30f81dc..30ef6cd 100644 --- a/src/cpu/CPU.go +++ b/src/cpu/CPU.go @@ -5,4 +5,5 @@ type CPU struct { Call []Register Syscall []Register ExternCall []Register + Return []Register } \ No newline at end of file diff --git a/src/cpu/Register.go b/src/cpu/Register.go index a2acccc..547c330 100644 --- a/src/cpu/Register.go +++ b/src/cpu/Register.go @@ -3,7 +3,7 @@ package cpu import "fmt" // Register represents the number of the register. -type Register uint8 +type Register int8 // String returns the human readable name of the register. func (r Register) String() string { diff --git a/src/ssa/BinaryOp.go b/src/ssa/BinaryOp.go index 401d8ce..d854d5d 100644 --- a/src/ssa/BinaryOp.go +++ b/src/ssa/BinaryOp.go @@ -12,6 +12,7 @@ type BinaryOp struct { Left Value Right Value Op token.Kind + Id Liveness Source } @@ -42,6 +43,10 @@ func (v *BinaryOp) IsConst() bool { return true } +func (v *BinaryOp) Debug() string { + return fmt.Sprintf("%%%d %s %%%d", v.Left.ID(), expression.Operators[v.Op].Symbol, v.Right.ID()) +} + func (v *BinaryOp) String() string { return fmt.Sprintf("%s %s %s", v.Left, expression.Operators[v.Op].Symbol, v.Right) } diff --git a/src/ssa/Block.go b/src/ssa/Block.go index 61b02f1..f0ff1e3 100644 --- a/src/ssa/Block.go +++ b/src/ssa/Block.go @@ -8,7 +8,7 @@ type Block struct { // Append adds a new instruction to the block. func (block *Block) Append(instr Value) Value { for _, dep := range instr.Dependencies() { - dep.AddUse(instr) + dep.AddUser(instr) } block.Instructions = append(block.Instructions, instr) diff --git a/src/ssa/Bytes.go b/src/ssa/Bytes.go index 4c1eedd..f7e158e 100644 --- a/src/ssa/Bytes.go +++ b/src/ssa/Bytes.go @@ -8,6 +8,7 @@ import ( ) type Bytes struct { + Id Bytes []byte Liveness Source @@ -31,10 +32,14 @@ func (v *Bytes) IsConst() bool { return true } +func (v *Bytes) Debug() string { + return v.String() +} + func (v *Bytes) String() string { return strconv.Quote(string(v.Bytes)) } func (v *Bytes) Type() types.Type { - return types.String + return types.CString } \ No newline at end of file diff --git a/src/ssa/Bytes_test.go b/src/ssa/Bytes_test.go index 9fae68d..eee39a1 100644 --- a/src/ssa/Bytes_test.go +++ b/src/ssa/Bytes_test.go @@ -19,5 +19,5 @@ func TestBytes(t *testing.T) { assert.False(t, hello.Equals(one)) assert.True(t, hello.Equals(helloDup)) assert.Equal(t, hello.String(), "\"Hello\"") - assert.True(t, types.Is(hello.Type(), types.String)) + assert.True(t, types.Is(hello.Type(), types.CString)) } \ No newline at end of file diff --git a/src/ssa/Call.go b/src/ssa/Call.go index f84631f..844f34c 100644 --- a/src/ssa/Call.go +++ b/src/ssa/Call.go @@ -1,13 +1,14 @@ package ssa import ( - "fmt" + "strconv" "strings" "git.urbach.dev/cli/q/src/types" ) type Call struct { + Id Arguments Liveness Source @@ -27,14 +28,42 @@ func (v *Call) IsConst() bool { return false } -func (v *Call) String() string { - args := make([]string, 0, len(v.Arguments)-1) +func (v *Call) Debug() string { + tmp := strings.Builder{} + tmp.WriteString("%") + tmp.WriteString(strconv.Itoa(v.Arguments[0].ID())) + tmp.WriteString("(") + args := v.Arguments[1:] - for _, arg := range v.Arguments[1:] { - args = append(args, arg.String()) + for i, arg := range args { + tmp.WriteString("%") + tmp.WriteString(strconv.Itoa(arg.ID())) + + if i != len(args)-1 { + tmp.WriteString(", ") + } } - return fmt.Sprintf("%s(%s)", v.Arguments[0].String(), strings.Join(args, ", ")) + tmp.WriteString(")") + return tmp.String() +} + +func (v *Call) String() string { + tmp := strings.Builder{} + tmp.WriteString(v.Arguments[0].String()) + tmp.WriteString("(") + args := v.Arguments[1:] + + for i, arg := range args { + tmp.WriteString(arg.String()) + + if i != len(args)-1 { + tmp.WriteString(", ") + } + } + + tmp.WriteString(")") + return tmp.String() } func (v *Call) Type() types.Type { diff --git a/src/ssa/Function.go b/src/ssa/Function.go index 8089e55..874b95e 100644 --- a/src/ssa/Function.go +++ b/src/ssa/Function.go @@ -5,6 +5,8 @@ import "git.urbach.dev/cli/q/src/types" type Function struct { UniqueName string Typ *types.Function + IsExtern bool + Id Liveness Source } @@ -27,6 +29,10 @@ func (v *Function) IsConst() bool { return true } +func (v *Function) Debug() string { + return v.String() +} + func (v *Function) String() string { return v.UniqueName } diff --git a/src/ssa/ID.go b/src/ssa/ID.go new file mode 100644 index 0000000..7751f3f --- /dev/null +++ b/src/ssa/ID.go @@ -0,0 +1,11 @@ +package ssa + +type Id int + +func (id Id) ID() int { + return int(id) +} + +func (id *Id) SetID(newId int) { + *id = Id(newId) +} \ No newline at end of file diff --git a/src/ssa/IR.go b/src/ssa/IR.go index b23ddc9..c2022d2 100644 --- a/src/ssa/IR.go +++ b/src/ssa/IR.go @@ -1,10 +1,13 @@ package ssa -import "git.urbach.dev/cli/q/src/types" +import ( + "git.urbach.dev/cli/q/src/types" +) // IR is a list of basic blocks. type IR struct { Blocks []*Block + nextId int } // AddBlock adds a new block to the function. @@ -31,35 +34,29 @@ func (f *IR) Append(instr Value) Value { } } + instr.SetID(f.nextId) + f.nextId++ return f.Blocks[len(f.Blocks)-1].Append(instr) } // AppendInt adds a new integer value to the last block. -func (f *IR) AppendInt(x int) *Int { - v := &Int{Int: x} - f.Append(v) - return v +func (f *IR) AppendInt(x int) Value { + return f.Append(&Int{Int: x}) } // AppendFunction adds a new function value to the last block. -func (f *IR) AppendFunction(name string, typ *types.Function) *Function { - v := &Function{UniqueName: name, Typ: typ} - f.Append(v) - return v +func (f *IR) AppendFunction(name string, typ *types.Function, extern bool) Value { + return f.Append(&Function{UniqueName: name, Typ: typ, IsExtern: extern}) } // AppendBytes adds a new byte slice value to the last block. -func (f *IR) AppendBytes(s []byte) *Bytes { - v := &Bytes{Bytes: s} - f.Append(v) - return v +func (f *IR) AppendBytes(s []byte) Value { + return f.Append(&Bytes{Bytes: s}) } // AppendString adds a new string value to the last block. -func (f *IR) AppendString(s string) *Bytes { - v := &Bytes{Bytes: []byte(s)} - f.Append(v) - return v +func (f *IR) AppendString(s string) Value { + return f.Append(&Bytes{Bytes: []byte(s)}) } // Values yields on each value. diff --git a/src/ssa/Int.go b/src/ssa/Int.go index 7f6f9fc..b5a0a1a 100644 --- a/src/ssa/Int.go +++ b/src/ssa/Int.go @@ -8,6 +8,7 @@ import ( type Int struct { Int int + Id Liveness Source } @@ -30,6 +31,10 @@ func (v *Int) IsConst() bool { return true } +func (v *Int) Debug() string { + return v.String() +} + func (v *Int) String() string { return fmt.Sprintf("%d", v.Int) } diff --git a/src/ssa/Liveness.go b/src/ssa/Liveness.go index ca544ea..8cfd8cd 100644 --- a/src/ssa/Liveness.go +++ b/src/ssa/Liveness.go @@ -1,13 +1,13 @@ package ssa type Liveness struct { - alive int + users []Value } -func (v *Liveness) AddUse(user Value) { - v.alive++ +func (v *Liveness) AddUser(user Value) { + v.users = append(v.users, user) } -func (v *Liveness) Alive() int { - return v.alive +func (v *Liveness) CountUsers() int { + return len(v.users) } \ No newline at end of file diff --git a/src/ssa/NoLiveness.go b/src/ssa/NoLiveness.go new file mode 100644 index 0000000..01f0b6e --- /dev/null +++ b/src/ssa/NoLiveness.go @@ -0,0 +1,6 @@ +package ssa + +type NoLiveness struct{} + +func (a *NoLiveness) AddUser(user Value) { panic("value does not have liveness") } +func (a *NoLiveness) CountUsers() int { return 0 } \ No newline at end of file diff --git a/src/ssa/Parameter.go b/src/ssa/Parameter.go index 93e25b9..7cd158c 100644 --- a/src/ssa/Parameter.go +++ b/src/ssa/Parameter.go @@ -10,6 +10,7 @@ type Parameter struct { Index uint8 Name string Typ types.Type + Id Liveness Source } @@ -32,6 +33,10 @@ func (v *Parameter) IsConst() bool { return true } +func (v *Parameter) Debug() string { + return v.String() +} + func (v *Parameter) String() string { return fmt.Sprintf("in[%d]", v.Index) } diff --git a/src/ssa/Return.go b/src/ssa/Return.go index e2e9a7c..70cce14 100644 --- a/src/ssa/Return.go +++ b/src/ssa/Return.go @@ -1,20 +1,19 @@ package ssa import ( - "fmt" + "strconv" "strings" "git.urbach.dev/cli/q/src/types" ) type Return struct { + Id Arguments Source + NoLiveness } -func (a *Return) AddUse(user Value) { panic("return is not a value") } -func (a *Return) Alive() int { return 0 } - func (a *Return) Equals(v Value) bool { b, sameType := v.(*Return) @@ -39,18 +38,43 @@ func (v *Return) IsConst() bool { return false } +func (v *Return) Debug() string { + if len(v.Arguments) == 0 { + return "return" + } + + tmp := strings.Builder{} + tmp.WriteString("return ") + + for i, arg := range v.Arguments { + tmp.WriteString("%") + tmp.WriteString(strconv.Itoa(arg.ID())) + + if i != len(v.Arguments)-1 { + tmp.WriteString(", ") + } + } + + return tmp.String() +} + func (v *Return) String() string { if len(v.Arguments) == 0 { return "return" } - args := make([]string, 0, len(v.Arguments)) + tmp := strings.Builder{} + tmp.WriteString("return ") - for _, arg := range v.Arguments { - args = append(args, arg.String()) + for i, arg := range v.Arguments { + tmp.WriteString(arg.String()) + + if i != len(v.Arguments)-1 { + tmp.WriteString(", ") + } } - return fmt.Sprintf("return %s", strings.Join(args, ", ")) + return tmp.String() } func (v *Return) Type() types.Type { diff --git a/src/ssa/Source.go b/src/ssa/Source.go index 28330f2..8f6f031 100644 --- a/src/ssa/Source.go +++ b/src/ssa/Source.go @@ -4,10 +4,14 @@ import "git.urbach.dev/cli/q/src/token" type Source token.List -func (v Source) Start() token.Position { - return v[0].Position -} - func (v Source) End() token.Position { return v[len(v)-1].End() +} + +func (v *Source) SetSource(source token.List) { + *v = Source(source) +} + +func (v Source) Start() token.Position { + return v[0].Position } \ No newline at end of file diff --git a/src/ssa/Struct.go b/src/ssa/Struct.go new file mode 100644 index 0000000..f333c5e --- /dev/null +++ b/src/ssa/Struct.go @@ -0,0 +1,69 @@ +package ssa + +import ( + "strconv" + "strings" + + "git.urbach.dev/cli/q/src/types" +) + +type Struct struct { + Typ *types.Struct + Id + Arguments + Liveness + Source +} + +func (a *Struct) Equals(v Value) bool { + b, sameType := v.(*Struct) + + if !sameType { + return false + } + + return a.Arguments.Equals(b.Arguments) +} + +func (v *Struct) IsConst() bool { + return true +} + +func (v *Struct) Debug() string { + tmp := strings.Builder{} + tmp.WriteString(v.Typ.Name()) + tmp.WriteString("{") + + for i, arg := range v.Arguments { + tmp.WriteString("%") + tmp.WriteString(strconv.Itoa(arg.ID())) + + if i != len(v.Arguments)-1 { + tmp.WriteString(", ") + } + } + + tmp.WriteString("}") + return tmp.String() +} + +func (v *Struct) String() string { + tmp := strings.Builder{} + tmp.WriteString(v.Typ.Name()) + tmp.WriteString("{") + + for i, arg := range v.Arguments { + tmp.WriteString(arg.String()) + + if i != len(v.Arguments)-1 { + tmp.WriteString(", ") + } + } + + tmp.WriteString("}") + return tmp.String() +} + +func (v *Struct) Type() types.Type { + return v.Typ +} \ No newline at end of file diff --git a/src/ssa/StructField.go b/src/ssa/StructField.go new file mode 100644 index 0000000..bc93946 --- /dev/null +++ b/src/ssa/StructField.go @@ -0,0 +1,45 @@ +package ssa + +import ( + "fmt" + + "git.urbach.dev/cli/q/src/types" +) + +type StructField struct { + Struct Value + Field *types.Field + Id + Liveness + Source +} + +func (v *StructField) Dependencies() []Value { + return []Value{v.Struct} +} + +func (a *StructField) Equals(v Value) bool { + b, sameType := v.(*StructField) + + if !sameType { + return false + } + + return a.Field == b.Field +} + +func (v *StructField) IsConst() bool { + return true +} + +func (v *StructField) Debug() string { + return fmt.Sprintf("%%%d.%s", v.Struct.ID(), v.Field) +} + +func (v *StructField) String() string { + return fmt.Sprintf("%s.%s", v.Struct, v.Field) +} + +func (v *StructField) Type() types.Type { + return v.Field.Type +} \ No newline at end of file diff --git a/src/ssa/Syscall.go b/src/ssa/Syscall.go index c13dfbc..0faebe4 100644 --- a/src/ssa/Syscall.go +++ b/src/ssa/Syscall.go @@ -1,13 +1,14 @@ package ssa import ( - "fmt" + "strconv" "strings" "git.urbach.dev/cli/q/src/types" ) type Syscall struct { + Id Arguments Liveness Source @@ -27,14 +28,37 @@ func (v *Syscall) IsConst() bool { return false } -func (v *Syscall) String() string { - args := make([]string, 0, len(v.Arguments)) +func (v *Syscall) Debug() string { + tmp := strings.Builder{} + tmp.WriteString("syscall(") - for _, arg := range v.Arguments { - args = append(args, arg.String()) + for i, arg := range v.Arguments { + tmp.WriteString("%") + tmp.WriteString(strconv.Itoa(arg.ID())) + + if i != len(v.Arguments)-1 { + tmp.WriteString(", ") + } } - return fmt.Sprintf("syscall(%s)", strings.Join(args, ", ")) + tmp.WriteString(")") + return tmp.String() +} + +func (v *Syscall) String() string { + tmp := strings.Builder{} + tmp.WriteString("syscall(") + + for i, arg := range v.Arguments { + tmp.WriteString(arg.String()) + + if i != len(v.Arguments)-1 { + tmp.WriteString(", ") + } + } + + tmp.WriteString(")") + return tmp.String() } func (v *Syscall) Type() types.Type { diff --git a/src/ssa/Value.go b/src/ssa/Value.go index db718b2..4cc1425 100644 --- a/src/ssa/Value.go +++ b/src/ssa/Value.go @@ -6,13 +6,24 @@ import ( ) type Value interface { - AddUse(Value) - Alive() int - Dependencies() []Value - End() token.Position - Equals(Value) bool + // Essentials + Debug() string + ID() int IsConst() bool + SetID(int) String() string - Start() token.Position Type() types.Type + + // Arguments + Dependencies() []Value + Equals(Value) bool + + // Liveness + AddUser(Value) + CountUsers() int + + // Source + SetSource(token.List) + Start() token.Position + End() token.Position } \ No newline at end of file diff --git a/src/ssa2asm/Compiler.go b/src/ssa2asm/Compiler.go new file mode 100644 index 0000000..ba6394e --- /dev/null +++ b/src/ssa2asm/Compiler.go @@ -0,0 +1,13 @@ +package ssa2asm + +import ( + "git.urbach.dev/cli/q/src/asm" + "git.urbach.dev/cli/q/src/cpu" +) + +type Compiler struct { + UniqueName string + Assembler asm.Assembler + CPU *cpu.CPU + Count Count +} \ No newline at end of file diff --git a/src/ssa2asm/Compiler_test.go b/src/ssa2asm/Compiler_test.go new file mode 100644 index 0000000..45271c5 --- /dev/null +++ b/src/ssa2asm/Compiler_test.go @@ -0,0 +1,25 @@ +package ssa2asm_test + +import ( + "testing" + + "git.urbach.dev/cli/q/src/build" + "git.urbach.dev/cli/q/src/compiler" + "git.urbach.dev/go/assert" +) + +func TestHelloExample(t *testing.T) { + b := build.New("../../examples/hello") + systems := []build.OS{build.Linux, build.Mac, build.Windows} + architectures := []build.Arch{build.ARM, build.X86} + + for _, os := range systems { + b.OS = os + + for _, arch := range architectures { + b.SetArch(arch) + _, err := compiler.Compile(b) + assert.Nil(t, err) + } + } +} \ No newline at end of file diff --git a/src/ssa2asm/Count.go b/src/ssa2asm/Count.go new file mode 100644 index 0000000..a7deb66 --- /dev/null +++ b/src/ssa2asm/Count.go @@ -0,0 +1,9 @@ +package ssa2asm + +// Counter is the data type for counters. +type Counter uint16 + +// Count stores how often a certain statement appeared so we can generate a unique label from it. +type Count struct { + Data Counter +} \ No newline at end of file diff --git a/src/core/CreateLabel.go b/src/ssa2asm/CreateLabel.go similarity index 83% rename from src/core/CreateLabel.go rename to src/ssa2asm/CreateLabel.go index 5bbc48f..f2c371c 100644 --- a/src/core/CreateLabel.go +++ b/src/ssa2asm/CreateLabel.go @@ -1,4 +1,4 @@ -package core +package ssa2asm import ( "strconv" @@ -8,7 +8,7 @@ import ( ) // CreateLabel creates a label that is tied to this function by using a suffix. -func (f *Function) CreateLabel(prefix string, count counter) *asm.Label { +func (f *Compiler) CreateLabel(prefix string, count Counter) *asm.Label { tmp := strings.Builder{} tmp.WriteString(prefix) tmp.WriteString(" ") diff --git a/src/ssa2asm/GenerateAssembly.go b/src/ssa2asm/GenerateAssembly.go new file mode 100644 index 0000000..4edb6fd --- /dev/null +++ b/src/ssa2asm/GenerateAssembly.go @@ -0,0 +1,76 @@ +package ssa2asm + +import ( + "slices" + "strings" + + "git.urbach.dev/cli/q/src/asm" + "git.urbach.dev/cli/q/src/ssa" +) + +// GenerateAssembly converts the SSA IR to assembler instructions. +func (f *Compiler) GenerateAssembly(ir ssa.IR, isLeaf bool) { + f.Assembler.Append(&asm.Label{Name: f.UniqueName}) + + if !isLeaf && f.UniqueName != "core.init" { + f.Assembler.Append(&asm.FunctionStart{}) + } + + for instr := range ir.Values { + switch instr := instr.(type) { + case *ssa.Call: + fn := instr.Arguments[0].(*ssa.Function) + args := instr.Arguments[1:] + + if fn.IsExtern { + for i := range slices.Backward(args) { + f.ValueToRegister(args[i], f.CPU.ExternCall[i]) + } + + dot := strings.IndexByte(fn.UniqueName, '.') + library := fn.UniqueName[:dot] + function := fn.UniqueName[dot+1:] + f.Assembler.Append(&asm.CallExtern{Library: library, Function: function}) + } else { + offset := 0 + + for i := range slices.Backward(args) { + structure, isStruct := args[i].(*ssa.Struct) + + if isStruct { + for _, field := range structure.Arguments { + f.ValueToRegister(field, f.CPU.Call[offset+i]) + i++ + } + } else { + f.ValueToRegister(args[i], f.CPU.Call[offset+i]) + } + } + + f.Assembler.Append(&asm.Call{Label: fn.UniqueName}) + } + + case *ssa.Return: + for i := range slices.Backward(instr.Arguments) { + f.ValueToRegister(instr.Arguments[i], f.CPU.Return[i]) + } + + f.Assembler.Append(&asm.Return{}) + + case *ssa.Syscall: + for i := range slices.Backward(instr.Arguments) { + f.ValueToRegister(instr.Arguments[i], f.CPU.Syscall[i]) + } + + f.Assembler.Append(&asm.Syscall{}) + } + } + + if !isLeaf && f.UniqueName != "core.init" { + f.Assembler.Append(&asm.FunctionEnd{}) + } + + if f.UniqueName != "core.exit" { + f.Assembler.Append(&asm.Return{}) + } +} \ No newline at end of file diff --git a/src/ssa2asm/ValueToRegister.go b/src/ssa2asm/ValueToRegister.go new file mode 100644 index 0000000..cd8bf1f --- /dev/null +++ b/src/ssa2asm/ValueToRegister.go @@ -0,0 +1,54 @@ +package ssa2asm + +import ( + "git.urbach.dev/cli/q/src/asm" + "git.urbach.dev/cli/q/src/cpu" + "git.urbach.dev/cli/q/src/ssa" +) + +// ValueToRegister moves a value into the given `destination` register. +func (f *Compiler) ValueToRegister(instr ssa.Value, destination cpu.Register) { + switch instr := instr.(type) { + case *ssa.Bytes: + f.Count.Data++ + label := f.CreateLabel("data", f.Count.Data) + f.Assembler.SetData(label.Name, instr.Bytes) + + f.Assembler.Append(&asm.MoveRegisterLabel{ + Destination: destination, + Label: label.Name, + }) + + case *ssa.Int: + f.Assembler.Append(&asm.MoveRegisterNumber{ + Destination: destination, + Number: instr.Int, + }) + + case *ssa.Parameter: + source := f.CPU.Call[instr.Index] + + if source == destination { + return + } + + f.Assembler.Append(&asm.MoveRegisterRegister{ + Destination: destination, + Source: source, + }) + + case *ssa.StructField: + parameter := instr.Struct.(*ssa.Parameter) + field := instr.Field + source := f.CPU.Call[parameter.Index+field.Index] + + if source == destination { + return + } + + f.Assembler.Append(&asm.MoveRegisterRegister{ + Destination: destination, + Source: source, + }) + } +} \ No newline at end of file diff --git a/src/types/Common.go b/src/types/Common.go index 07796b9..b80991a 100644 --- a/src/types/Common.go +++ b/src/types/Common.go @@ -12,7 +12,6 @@ var ( Int8 = &Base{name: "int8", size: 1} Float64 = &Base{name: "float64", size: 8} Float32 = &Base{name: "float32", size: 4} - String = &Array{Of: Byte} UInt64 = &Base{name: "uint64", size: 8} UInt32 = &Base{name: "uint32", size: 4} UInt16 = &Base{name: "uint16", size: 2} @@ -20,6 +19,19 @@ var ( Void = &Base{name: "void", size: 0} ) +var ( + CString = &Pointer{To: Byte} + String = &Struct{ + Package: "", + UniqueName: "string", + name: "string", + Fields: []*Field{ + {Name: "ptr", Type: CString, Index: 0, Offset: 0}, + {Name: "len", Type: Int, Index: 1, Offset: 8}, + }, + } +) + var ( Byte = UInt8 Float = Float64 diff --git a/src/types/Field.go b/src/types/Field.go new file mode 100644 index 0000000..370c858 --- /dev/null +++ b/src/types/Field.go @@ -0,0 +1,17 @@ +package types + +import "git.urbach.dev/cli/q/src/token" + +// Field is a memory region in a data structure. +type Field struct { + Name string + Type Type + Position token.Position + Index uint8 + Offset uint8 +} + +// String returns the name of the struct. +func (f *Field) String() string { + return f.Name +} \ No newline at end of file diff --git a/src/types/Parse.go b/src/types/Parse.go index 00fd0af..c112045 100644 --- a/src/types/Parse.go +++ b/src/types/Parse.go @@ -45,6 +45,8 @@ func Parse[T ~[]token.Token](tokens T, source []byte) Type { } switch tokens[0].String(source) { + case "string": + return String case "int": return Int case "int64": diff --git a/src/types/Struct.go b/src/types/Struct.go new file mode 100644 index 0000000..f6ac7e0 --- /dev/null +++ b/src/types/Struct.go @@ -0,0 +1,50 @@ +package types + +// Struct is a structure in memory whose regions are addressable with named fields. +type Struct struct { + Package string + UniqueName string + Fields []*Field + name string +} + +// NewStruct creates a new struct. +func NewStruct(pkg string, name string) *Struct { + return &Struct{ + Package: pkg, + UniqueName: pkg + "." + name, + name: name, + } +} + +// AddField adds a new field to the end of the struct. +func (s *Struct) AddField(field *Field) { + s.Fields = append(s.Fields, field) +} + +// FieldByName returns the field with the given name if it exists. +func (s *Struct) FieldByName(name string) *Field { + for _, field := range s.Fields { + if field.Name == name { + return field + } + } + + return nil +} + +// Name returns the name of the struct. +func (s *Struct) Name() string { + return s.name +} + +// Size returns the total size in bytes. +func (s *Struct) Size() int { + sum := 0 + + for _, field := range s.Fields { + sum += field.Type.Size() + } + + return sum +} \ No newline at end of file diff --git a/src/types/types_test.go b/src/types/types_test.go index 769a24b..1d6a985 100644 --- a/src/types/types_test.go +++ b/src/types/types_test.go @@ -13,7 +13,7 @@ func TestName(t *testing.T) { assert.Equal(t, types.AnyPointer.Name(), "*any") assert.Equal(t, (&types.Pointer{To: types.Int}).Name(), "*int64") assert.Equal(t, (&types.Array{Of: types.Int}).Name(), "[]int64") - assert.Equal(t, types.String.Name(), "[]uint8") + assert.Equal(t, types.String.Name(), "string") } func TestSize(t *testing.T) { @@ -24,7 +24,7 @@ func TestSize(t *testing.T) { assert.Equal(t, types.Int64.Size(), 8) assert.Equal(t, types.AnyArray.Size(), 8) assert.Equal(t, types.AnyPointer.Size(), 8) - assert.Equal(t, types.String.Size(), 8) + assert.Equal(t, types.String.Size(), 16) assert.Equal(t, (&types.Pointer{To: types.Int}).Size(), 8) } diff --git a/src/x86/Registers.go b/src/x86/Registers.go index 1c02bc8..4e13458 100644 --- a/src/x86/Registers.go +++ b/src/x86/Registers.go @@ -25,4 +25,5 @@ var CPU = cpu.CPU{ Call: []cpu.Register{R0, R7, R6, R2, R10, R8, R9}, Syscall: []cpu.Register{R0, R7, R6, R2, R10, R8, R9}, ExternCall: []cpu.Register{R1, R2, R8, R9}, + Return: []cpu.Register{R0, R7, R6}, } \ No newline at end of file