Improved ssa compiler
All checks were successful
/ test (push) Successful in 15s

This commit is contained in:
Eduard Urbach 2025-07-02 16:55:24 +02:00
parent c9c6b94c18
commit 3301cf5542
Signed by: akyoto
GPG key ID: 49226B848C78F6C8
49 changed files with 690 additions and 262 deletions

View file

@ -1,3 +1,3 @@
write(buffer []byte) -> (written int) { write(buffer string) -> (written int) {
return syscall(64, 1, buffer, len(buffer)) return syscall(64, 1, buffer.ptr, buffer.len)
} }

View file

@ -1,3 +1,3 @@
write(buffer []byte) -> (written int) { write(buffer string) -> (written int) {
return syscall(1, 1, buffer, len(buffer)) return syscall(1, 1, buffer.ptr, buffer.len)
} }

View file

@ -1,3 +1,3 @@
write(buffer []byte) -> (written int) { write(buffer string) -> (written int) {
return syscall(0x2000004, 1, buffer, len(buffer)) return syscall(0x2000004, 1, buffer.ptr, buffer.len)
} }

View file

@ -1,4 +1,4 @@
write(_ []byte) -> (written int) { write(_ string) -> (written int) {
return 0 return 0
} }

View file

@ -45,4 +45,5 @@ var CPU = cpu.CPU{
Call: []cpu.Register{X0, X1, X2, X3, X4, X5, X6}, Call: []cpu.Register{X0, X1, X2, X3, X4, X5, X6},
Syscall: []cpu.Register{X8, X0, X1, X2, X3, X4, X5}, Syscall: []cpu.Register{X8, X0, X1, X2, X3, X4, X5},
ExternCall: []cpu.Register{X0, X1, X2, X3, X4, X5, X6, X7}, ExternCall: []cpu.Register{X0, X1, X2, X3, X4, X5, X6, X7},
Return: []cpu.Register{X0, X1, X2},
} }

View file

@ -8,3 +8,17 @@ const (
ARM ARM
X86 X86
) )
// 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
}

View file

@ -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
}

View file

@ -5,7 +5,6 @@ Usage:
Commands: Commands:
build [directory | file] build an executable build [directory | file] build an executable
--verbose, -v show everything --verbose, -v show everything
run [directory | file] build and run the executable run [directory | file] build and run the executable

View file

@ -2,6 +2,7 @@ package compiler
import ( import (
"fmt" "fmt"
"strings"
"git.urbach.dev/cli/q/src/core" "git.urbach.dev/cli/q/src/core"
"git.urbach.dev/go/color/ansi" "git.urbach.dev/go/color/ansi"
@ -10,18 +11,27 @@ import (
// showSSA shows the SSA IR. // showSSA shows the SSA IR.
func showSSA(root *core.Function) { func showSSA(root *core.Function) {
root.EachDependency(make(map[*core.Function]bool), func(f *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 { for _, block := range f.Blocks {
if i != 0 { ansi.Dim.Printf("| %-3s | %-30s | %-30s | %-4s |\n", "ID", "Raw", "Type", "Uses")
fmt.Println("---") 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 { for i, instr := range block.Instructions {
ansi.Dim.Printf("%-4d", i) ansi.Dim.Printf("| %%%-2d | ", i)
fmt.Printf("%-40s", instr.String())
ansi.Cyan.Printf("%-30s", instr.Type().Name()) if instr.IsConst() {
ansi.Dim.Printf("%s\n", f.File.Bytes[instr.Start():instr.End()]) 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()
} }
} }

View file

@ -7,7 +7,7 @@ import (
// CheckDeadCode checks for dead values. // CheckDeadCode checks for dead values.
func (f *Function) CheckDeadCode() error { func (f *Function) CheckDeadCode() error {
for instr := range f.Values { 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()) return errors.New(&UnusedValue{Value: instr.String()}, f.File, instr.Start())
} }
} }

View file

@ -1,48 +1,24 @@
package core package core
import ( import "git.urbach.dev/cli/q/src/types"
"git.urbach.dev/cli/q/src/ssa"
"git.urbach.dev/cli/q/src/types"
)
// Compile turns a function into machine code. // Compile turns a function into machine code.
func (f *Function) Compile() { func (f *Function) Compile() {
extra := 0 offset := 0
for i, input := range f.Input { for i, input := range f.Input {
if input.Name == "_" { if input.Name == "_" {
continue continue
} }
array, isArray := input.Typ.(*types.Array) input.Index = uint8(offset + i)
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)
f.Append(input) f.Append(input)
f.Identifiers[input.Name] = 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 { for instr := range f.Body.Instructions {
@ -59,5 +35,5 @@ func (f *Function) Compile() {
return return
} }
f.ssaToAsm() f.GenerateAssembly(f.IR, f.IsLeaf())
} }

View file

@ -27,8 +27,8 @@ func (f *Function) Evaluate(expr *expression.Expression) (ssa.Value, error) {
} }
f.Dependencies.Add(function) f.Dependencies.Add(function)
v := f.AppendFunction(function.UniqueName, function.Type) v := f.AppendFunction(function.UniqueName, function.Type, function.IsExtern())
v.Source = ssa.Source(expr.Source) v.SetSource(expr.Source)
return v, nil return v, nil
} }
@ -42,14 +42,25 @@ func (f *Function) Evaluate(expr *expression.Expression) (ssa.Value, error) {
} }
v := f.AppendInt(number) v := f.AppendInt(number)
v.Source = ssa.Source(expr.Source) v.SetSource(expr.Source)
return v, nil return v, nil
case token.String: case token.String:
data := expr.Token.Bytes(f.File.Bytes) data := expr.Token.Bytes(f.File.Bytes)
data = Unescape(data) 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 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) 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) { if !types.Is(param.Type(), fn.Input[i].Typ) {
return nil, errors.New(&TypeMismatch{ return nil, errors.New(&TypeMismatch{
Encountered: param.Type().Name(), Encountered: param.Type().Name(),
@ -130,23 +141,40 @@ func (f *Function) Evaluate(expr *expression.Expression) (ssa.Value, error) {
case token.Dot: case token.Dot:
left := expr.Children[0] left := expr.Children[0]
right := expr.Children[1] 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)
function, exists := f.All.Functions[label] rightText := right.String(f.File.Bytes)
identifier, exists := f.Identifiers[leftText]
if !exists { if exists {
return nil, errors.New(&UnknownIdentifier{Name: label}, f.File, left.Token.Position) 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 {
if function.IsExtern() { if function.IsExtern() {
f.Assembler.Libraries = f.Assembler.Libraries.Append(function.Package, function.Name) f.Assembler.Libraries = f.Assembler.Libraries.Append(function.Package, function.Name)
} else { } else {
f.Dependencies.Add(function) f.Dependencies.Add(function)
} }
v := f.AppendFunction(function.UniqueName, function.Type) v := f.AppendFunction(function.UniqueName, function.Type, function.IsExtern())
v.Source = ssa.Source(expr.Source) v.SetSource(expr.Source)
return v, nil return v, nil
} }
return nil, errors.New(&UnknownIdentifier{Name: label}, f.File, left.Token.Position)
}
return nil, nil return nil, nil
} }

View file

@ -4,10 +4,10 @@ import (
"fmt" "fmt"
"git.urbach.dev/cli/q/src/asm" "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/fs"
"git.urbach.dev/cli/q/src/set" "git.urbach.dev/cli/q/src/set"
"git.urbach.dev/cli/q/src/ssa" "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/token"
"git.urbach.dev/cli/q/src/types" "git.urbach.dev/cli/q/src/types"
) )
@ -15,9 +15,9 @@ import (
// Function is the smallest unit of code. // Function is the smallest unit of code.
type Function struct { type Function struct {
ssa.IR ssa.IR
ssa2asm.Compiler
Name string Name string
Package string Package string
UniqueName string
File *fs.File File *fs.File
Input []*ssa.Parameter Input []*ssa.Parameter
Output []*ssa.Parameter Output []*ssa.Parameter
@ -25,11 +25,8 @@ type Function struct {
Identifiers map[string]ssa.Value Identifiers map[string]ssa.Value
All *Environment All *Environment
Dependencies set.Ordered[*Function] Dependencies set.Ordered[*Function]
Assembler asm.Assembler
CPU *cpu.CPU
Type *types.Function Type *types.Function
Err error Err error
count count
} }
// NewFunction creates a new function. // NewFunction creates a new function.
@ -37,7 +34,7 @@ func NewFunction(name string, pkg string, file *fs.File) *Function {
return &Function{ return &Function{
Name: name, Name: name,
Package: pkg, Package: pkg,
UniqueName: fmt.Sprintf("%s.%s", pkg, name),
File: file, File: file,
Identifiers: make(map[string]ssa.Value, 8), Identifiers: make(map[string]ssa.Value, 8),
IR: ssa.IR{ IR: ssa.IR{
@ -45,9 +42,12 @@ func NewFunction(name string, pkg string, file *fs.File) *Function {
{Instructions: make([]ssa.Value, 0, 8)}, {Instructions: make([]ssa.Value, 0, 8)},
}, },
}, },
Compiler: ssa2asm.Compiler{
UniqueName: fmt.Sprintf("%s.%s", pkg, name),
Assembler: asm.Assembler{ Assembler: asm.Assembler{
Instructions: make([]asm.Instruction, 0, 8), Instructions: make([]asm.Instruction, 0, 8),
}, },
},
} }
} }

View file

@ -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
}

View file

@ -63,6 +63,21 @@ func (err *UnknownIdentifier) Error() string {
return fmt.Sprintf("Unknown identifier '%s'", err.Name) 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. // UnusedValue error is created when a value is never used.
type UnusedValue struct { type UnusedValue struct {
Value string Value string

View file

@ -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{})
}
}

View file

@ -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,
})
}
}
}

View file

@ -5,4 +5,5 @@ type CPU struct {
Call []Register Call []Register
Syscall []Register Syscall []Register
ExternCall []Register ExternCall []Register
Return []Register
} }

View file

@ -3,7 +3,7 @@ package cpu
import "fmt" import "fmt"
// Register represents the number of the register. // Register represents the number of the register.
type Register uint8 type Register int8
// String returns the human readable name of the register. // String returns the human readable name of the register.
func (r Register) String() string { func (r Register) String() string {

View file

@ -12,6 +12,7 @@ type BinaryOp struct {
Left Value Left Value
Right Value Right Value
Op token.Kind Op token.Kind
Id
Liveness Liveness
Source Source
} }
@ -42,6 +43,10 @@ func (v *BinaryOp) IsConst() bool {
return true 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 { func (v *BinaryOp) String() string {
return fmt.Sprintf("%s %s %s", v.Left, expression.Operators[v.Op].Symbol, v.Right) return fmt.Sprintf("%s %s %s", v.Left, expression.Operators[v.Op].Symbol, v.Right)
} }

View file

@ -8,7 +8,7 @@ type Block struct {
// Append adds a new instruction to the block. // Append adds a new instruction to the block.
func (block *Block) Append(instr Value) Value { func (block *Block) Append(instr Value) Value {
for _, dep := range instr.Dependencies() { for _, dep := range instr.Dependencies() {
dep.AddUse(instr) dep.AddUser(instr)
} }
block.Instructions = append(block.Instructions, instr) block.Instructions = append(block.Instructions, instr)

View file

@ -8,6 +8,7 @@ import (
) )
type Bytes struct { type Bytes struct {
Id
Bytes []byte Bytes []byte
Liveness Liveness
Source Source
@ -31,10 +32,14 @@ func (v *Bytes) IsConst() bool {
return true return true
} }
func (v *Bytes) Debug() string {
return v.String()
}
func (v *Bytes) String() string { func (v *Bytes) String() string {
return strconv.Quote(string(v.Bytes)) return strconv.Quote(string(v.Bytes))
} }
func (v *Bytes) Type() types.Type { func (v *Bytes) Type() types.Type {
return types.String return types.CString
} }

View file

@ -19,5 +19,5 @@ func TestBytes(t *testing.T) {
assert.False(t, hello.Equals(one)) assert.False(t, hello.Equals(one))
assert.True(t, hello.Equals(helloDup)) assert.True(t, hello.Equals(helloDup))
assert.Equal(t, hello.String(), "\"Hello\"") assert.Equal(t, hello.String(), "\"Hello\"")
assert.True(t, types.Is(hello.Type(), types.String)) assert.True(t, types.Is(hello.Type(), types.CString))
} }

View file

@ -1,13 +1,14 @@
package ssa package ssa
import ( import (
"fmt" "strconv"
"strings" "strings"
"git.urbach.dev/cli/q/src/types" "git.urbach.dev/cli/q/src/types"
) )
type Call struct { type Call struct {
Id
Arguments Arguments
Liveness Liveness
Source Source
@ -27,14 +28,42 @@ func (v *Call) IsConst() bool {
return false return false
} }
func (v *Call) String() string { func (v *Call) Debug() string {
args := make([]string, 0, len(v.Arguments)-1) 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:] { for i, arg := range args {
args = append(args, arg.String()) 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 { func (v *Call) Type() types.Type {

View file

@ -5,6 +5,8 @@ import "git.urbach.dev/cli/q/src/types"
type Function struct { type Function struct {
UniqueName string UniqueName string
Typ *types.Function Typ *types.Function
IsExtern bool
Id
Liveness Liveness
Source Source
} }
@ -27,6 +29,10 @@ func (v *Function) IsConst() bool {
return true return true
} }
func (v *Function) Debug() string {
return v.String()
}
func (v *Function) String() string { func (v *Function) String() string {
return v.UniqueName return v.UniqueName
} }

11
src/ssa/ID.go Normal file
View file

@ -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)
}

View file

@ -1,10 +1,13 @@
package ssa 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. // IR is a list of basic blocks.
type IR struct { type IR struct {
Blocks []*Block Blocks []*Block
nextId int
} }
// AddBlock adds a new block to the function. // 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) return f.Blocks[len(f.Blocks)-1].Append(instr)
} }
// AppendInt adds a new integer value to the last block. // AppendInt adds a new integer value to the last block.
func (f *IR) AppendInt(x int) *Int { func (f *IR) AppendInt(x int) Value {
v := &Int{Int: x} return f.Append(&Int{Int: x})
f.Append(v)
return v
} }
// AppendFunction adds a new function value to the last block. // AppendFunction adds a new function value to the last block.
func (f *IR) AppendFunction(name string, typ *types.Function) *Function { func (f *IR) AppendFunction(name string, typ *types.Function, extern bool) Value {
v := &Function{UniqueName: name, Typ: typ} return f.Append(&Function{UniqueName: name, Typ: typ, IsExtern: extern})
f.Append(v)
return v
} }
// AppendBytes adds a new byte slice value to the last block. // AppendBytes adds a new byte slice value to the last block.
func (f *IR) AppendBytes(s []byte) *Bytes { func (f *IR) AppendBytes(s []byte) Value {
v := &Bytes{Bytes: s} return f.Append(&Bytes{Bytes: s})
f.Append(v)
return v
} }
// AppendString adds a new string value to the last block. // AppendString adds a new string value to the last block.
func (f *IR) AppendString(s string) *Bytes { func (f *IR) AppendString(s string) Value {
v := &Bytes{Bytes: []byte(s)} return f.Append(&Bytes{Bytes: []byte(s)})
f.Append(v)
return v
} }
// Values yields on each value. // Values yields on each value.

View file

@ -8,6 +8,7 @@ import (
type Int struct { type Int struct {
Int int Int int
Id
Liveness Liveness
Source Source
} }
@ -30,6 +31,10 @@ func (v *Int) IsConst() bool {
return true return true
} }
func (v *Int) Debug() string {
return v.String()
}
func (v *Int) String() string { func (v *Int) String() string {
return fmt.Sprintf("%d", v.Int) return fmt.Sprintf("%d", v.Int)
} }

View file

@ -1,13 +1,13 @@
package ssa package ssa
type Liveness struct { type Liveness struct {
alive int users []Value
} }
func (v *Liveness) AddUse(user Value) { func (v *Liveness) AddUser(user Value) {
v.alive++ v.users = append(v.users, user)
} }
func (v *Liveness) Alive() int { func (v *Liveness) CountUsers() int {
return v.alive return len(v.users)
} }

6
src/ssa/NoLiveness.go Normal file
View file

@ -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 }

View file

@ -10,6 +10,7 @@ type Parameter struct {
Index uint8 Index uint8
Name string Name string
Typ types.Type Typ types.Type
Id
Liveness Liveness
Source Source
} }
@ -32,6 +33,10 @@ func (v *Parameter) IsConst() bool {
return true return true
} }
func (v *Parameter) Debug() string {
return v.String()
}
func (v *Parameter) String() string { func (v *Parameter) String() string {
return fmt.Sprintf("in[%d]", v.Index) return fmt.Sprintf("in[%d]", v.Index)
} }

View file

@ -1,20 +1,19 @@
package ssa package ssa
import ( import (
"fmt" "strconv"
"strings" "strings"
"git.urbach.dev/cli/q/src/types" "git.urbach.dev/cli/q/src/types"
) )
type Return struct { type Return struct {
Id
Arguments Arguments
Source 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 { func (a *Return) Equals(v Value) bool {
b, sameType := v.(*Return) b, sameType := v.(*Return)
@ -39,18 +38,43 @@ func (v *Return) IsConst() bool {
return false 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 { func (v *Return) String() string {
if len(v.Arguments) == 0 { if len(v.Arguments) == 0 {
return "return" return "return"
} }
args := make([]string, 0, len(v.Arguments)) tmp := strings.Builder{}
tmp.WriteString("return ")
for _, arg := range v.Arguments { for i, arg := range v.Arguments {
args = append(args, arg.String()) 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 { func (v *Return) Type() types.Type {

View file

@ -4,10 +4,14 @@ import "git.urbach.dev/cli/q/src/token"
type Source token.List type Source token.List
func (v Source) Start() token.Position {
return v[0].Position
}
func (v Source) End() token.Position { func (v Source) End() token.Position {
return v[len(v)-1].End() 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
}

69
src/ssa/Struct.go Normal file
View file

@ -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
}

45
src/ssa/StructField.go Normal file
View file

@ -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
}

View file

@ -1,13 +1,14 @@
package ssa package ssa
import ( import (
"fmt" "strconv"
"strings" "strings"
"git.urbach.dev/cli/q/src/types" "git.urbach.dev/cli/q/src/types"
) )
type Syscall struct { type Syscall struct {
Id
Arguments Arguments
Liveness Liveness
Source Source
@ -27,14 +28,37 @@ func (v *Syscall) IsConst() bool {
return false return false
} }
func (v *Syscall) String() string { func (v *Syscall) Debug() string {
args := make([]string, 0, len(v.Arguments)) tmp := strings.Builder{}
tmp.WriteString("syscall(")
for _, arg := range v.Arguments { for i, arg := range v.Arguments {
args = append(args, arg.String()) 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 { func (v *Syscall) Type() types.Type {

View file

@ -6,13 +6,24 @@ import (
) )
type Value interface { type Value interface {
AddUse(Value) // Essentials
Alive() int Debug() string
Dependencies() []Value ID() int
End() token.Position
Equals(Value) bool
IsConst() bool IsConst() bool
SetID(int)
String() string String() string
Start() token.Position
Type() types.Type Type() types.Type
// Arguments
Dependencies() []Value
Equals(Value) bool
// Liveness
AddUser(Value)
CountUsers() int
// Source
SetSource(token.List)
Start() token.Position
End() token.Position
} }

13
src/ssa2asm/Compiler.go Normal file
View file

@ -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
}

View file

@ -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)
}
}
}

9
src/ssa2asm/Count.go Normal file
View file

@ -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
}

View file

@ -1,4 +1,4 @@
package core package ssa2asm
import ( import (
"strconv" "strconv"
@ -8,7 +8,7 @@ import (
) )
// CreateLabel creates a label that is tied to this function by using a suffix. // 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 := strings.Builder{}
tmp.WriteString(prefix) tmp.WriteString(prefix)
tmp.WriteString(" ") tmp.WriteString(" ")

View file

@ -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{})
}
}

View file

@ -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,
})
}
}

View file

@ -12,7 +12,6 @@ var (
Int8 = &Base{name: "int8", size: 1} Int8 = &Base{name: "int8", size: 1}
Float64 = &Base{name: "float64", size: 8} Float64 = &Base{name: "float64", size: 8}
Float32 = &Base{name: "float32", size: 4} Float32 = &Base{name: "float32", size: 4}
String = &Array{Of: Byte}
UInt64 = &Base{name: "uint64", size: 8} UInt64 = &Base{name: "uint64", size: 8}
UInt32 = &Base{name: "uint32", size: 4} UInt32 = &Base{name: "uint32", size: 4}
UInt16 = &Base{name: "uint16", size: 2} UInt16 = &Base{name: "uint16", size: 2}
@ -20,6 +19,19 @@ var (
Void = &Base{name: "void", size: 0} 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 ( var (
Byte = UInt8 Byte = UInt8
Float = Float64 Float = Float64

17
src/types/Field.go Normal file
View file

@ -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
}

View file

@ -45,6 +45,8 @@ func Parse[T ~[]token.Token](tokens T, source []byte) Type {
} }
switch tokens[0].String(source) { switch tokens[0].String(source) {
case "string":
return String
case "int": case "int":
return Int return Int
case "int64": case "int64":

50
src/types/Struct.go Normal file
View file

@ -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
}

View file

@ -13,7 +13,7 @@ func TestName(t *testing.T) {
assert.Equal(t, types.AnyPointer.Name(), "*any") assert.Equal(t, types.AnyPointer.Name(), "*any")
assert.Equal(t, (&types.Pointer{To: types.Int}).Name(), "*int64") assert.Equal(t, (&types.Pointer{To: types.Int}).Name(), "*int64")
assert.Equal(t, (&types.Array{Of: 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) { 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.Int64.Size(), 8)
assert.Equal(t, types.AnyArray.Size(), 8) assert.Equal(t, types.AnyArray.Size(), 8)
assert.Equal(t, types.AnyPointer.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) assert.Equal(t, (&types.Pointer{To: types.Int}).Size(), 8)
} }

View file

@ -25,4 +25,5 @@ var CPU = cpu.CPU{
Call: []cpu.Register{R0, R7, R6, R2, R10, R8, R9}, Call: []cpu.Register{R0, R7, R6, R2, R10, R8, R9},
Syscall: []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}, ExternCall: []cpu.Register{R1, R2, R8, R9},
Return: []cpu.Register{R0, R7, R6},
} }