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) {
return syscall(64, 1, buffer, len(buffer))
write(buffer string) -> (written int) {
return syscall(64, 1, buffer.ptr, buffer.len)
}

View file

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

View file

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

View file

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

View file

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

View file

@ -8,3 +8,17 @@ const (
ARM
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:
build [directory | file] build an executable
--verbose, -v show everything
run [directory | file] build and run the executable

View file

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

View file

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

View file

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

View file

@ -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,23 +141,40 @@ 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))
function, exists := f.All.Functions[label]
leftText := left.String(f.File.Bytes)
rightText := right.String(f.File.Bytes)
identifier, exists := f.Identifiers[leftText]
if !exists {
return nil, errors.New(&UnknownIdentifier{Name: label}, f.File, left.Token.Position)
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 {
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)
v := f.AppendFunction(function.UniqueName, function.Type, function.IsExtern())
v.SetSource(expr.Source)
return v, nil
}
return nil, errors.New(&UnknownIdentifier{Name: label}, f.File, left.Token.Position)
}
return nil, nil
}

View file

@ -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,11 +25,8 @@ 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.
@ -37,7 +34,7 @@ func NewFunction(name string, pkg string, file *fs.File) *Function {
return &Function{
Name: name,
Package: pkg,
UniqueName: fmt.Sprintf("%s.%s", pkg, name),
File: file,
Identifiers: make(map[string]ssa.Value, 8),
IR: ssa.IR{
@ -45,9 +42,12 @@ func NewFunction(name string, pkg string, file *fs.File) *Function {
{Instructions: make([]ssa.Value, 0, 8)},
},
},
Compiler: ssa2asm.Compiler{
UniqueName: fmt.Sprintf("%s.%s", pkg, name),
Assembler: asm.Assembler{
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)
}
// 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

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
Syscall []Register
ExternCall []Register
Return []Register
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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
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.

View file

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

View file

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

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

View file

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

View file

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

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
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 {

View file

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

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 (
"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(" ")

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

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) {
case "string":
return String
case "int":
return Int
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.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)
}

View file

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