Implemented an assembler
All checks were successful
/ test (push) Successful in 28s

This commit is contained in:
Eduard Urbach 2025-06-24 12:55:26 +02:00
parent 2b703e9af2
commit 70c2da4a4d
Signed by: akyoto
GPG key ID: 49226B848C78F6C8
40 changed files with 821 additions and 117 deletions

12
lib/core/core_linux_arm.q Normal file
View file

@ -0,0 +1,12 @@
init() {
main.main()
exit()
}
exit() {
syscall(93, 0)
}
crash() {
syscall(93, 1)
}

12
lib/core/core_linux_x86.q Normal file
View file

@ -0,0 +1,12 @@
init() {
main.main()
exit()
}
exit() {
syscall(60, 0)
}
crash() {
syscall(60, 1)
}

12
lib/core/core_mac.q Normal file
View file

@ -0,0 +1,12 @@
init() {
main.main()
exit()
}
exit() {
syscall(0x2000001, 0)
}
crash() {
syscall(0x2000001, 1)
}

28
lib/core/core_windows.q Normal file
View file

@ -0,0 +1,28 @@
init() {
// kernel32.SetConsoleCP(cp.utf8)
// kernel32.SetConsoleOutputCP(cp.utf8)
main.main()
exit()
}
exit() {
// kernel32.ExitProcess(0)
}
crash() {
// kernel32.ExitProcess(1)
}
// const {
// cp {
// utf8 65001
// }
// }
// extern {
// kernel32 {
// SetConsoleCP(cp uint)
// SetConsoleOutputCP(cp uint)
// ExitProcess(code uint)
// }
// }

View file

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

View file

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

3
lib/io/write_mac.q Normal file
View file

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

3
lib/io/write_windows.q Normal file
View file

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

View file

@ -40,3 +40,8 @@ const (
const ( const (
ZR = SP // Zero register uses the same numerical value as SP ZR = SP // Zero register uses the same numerical value as SP
) )
var CPU = cpu.CPU{
Call: []cpu.Register{X0, X1, X2, X3, X4, X5, X6},
Syscall: []cpu.Register{X8, X0, X1, X2, X3, X4, X5},
}

4
src/asm/Address.go Normal file
View file

@ -0,0 +1,4 @@
package asm
// Address represents a memory address.
type Address = int

78
src/asm/Assembler.go Normal file
View file

@ -0,0 +1,78 @@
package asm
import (
"maps"
"git.urbach.dev/cli/q/src/build"
"git.urbach.dev/cli/q/src/data"
"git.urbach.dev/cli/q/src/elf"
"git.urbach.dev/cli/q/src/exe"
)
// Assembler contains a list of instructions.
type Assembler struct {
Data data.Data
Instructions []Instruction
}
// Append adds another instruction.
func (a *Assembler) Append(instr Instruction) {
a.Instructions = append(a.Instructions, instr)
}
// Compile compiles the instructions to machine code.
func (a *Assembler) Compile(b *build.Build) (code []byte, data []byte) {
data, dataLabels := a.Data.Finalize()
c := compiler{
code: make([]byte, 0, len(a.Instructions)*8),
data: data,
dataLabels: dataLabels,
labels: make(map[string]Address, 32),
}
switch b.Arch {
case build.ARM:
armc := compilerARM{compiler: &c}
for _, instr := range a.Instructions {
armc.Compile(instr)
}
case build.X86:
x86c := compilerX86{compiler: &c}
for _, instr := range a.Instructions {
x86c.Compile(instr)
}
}
x := exe.New(elf.HeaderEnd, b.FileAlign, b.MemoryAlign)
x.InitSections(c.code, c.data)
dataSectionOffset := x.Sections[1].MemoryOffset - x.Sections[0].MemoryOffset
for dataLabel, address := range dataLabels {
c.labels[dataLabel] = dataSectionOffset + address
}
for _, call := range c.deferred {
call()
}
return c.code, c.data
}
// Merge combines the contents of this assembler with another one.
func (a *Assembler) Merge(b *Assembler) {
maps.Copy(a.Data, b.Data)
a.Instructions = append(a.Instructions, b.Instructions...)
}
// SetData sets the data for the given label.
func (a *Assembler) SetData(label string, bytes []byte) {
if a.Data == nil {
a.Data = data.Data{}
}
a.Data.Insert(label, bytes)
}

34
src/asm/Instruction.go Normal file
View file

@ -0,0 +1,34 @@
package asm
import "git.urbach.dev/cli/q/src/cpu"
type Instruction interface{}
type Call struct {
Label string
}
type FunctionStart struct{}
type FunctionEnd struct{}
type Label struct {
Name string
}
type MoveRegisterLabel struct {
Destination cpu.Register
Label string
}
type MoveRegisterNumber struct {
Destination cpu.Register
Number int
}
type MoveRegisterRegister struct {
Destination cpu.Register
Source cpu.Register
}
type Return struct{}
type Syscall struct{}

13
src/asm/compiler.go Normal file
View file

@ -0,0 +1,13 @@
package asm
type compiler struct {
code []byte
data []byte
dataLabels map[string]Address
labels map[string]Address
deferred []func()
}
func (c *compiler) Defer(call func()) {
c.deferred = append(c.deferred, call)
}

63
src/asm/compilerARM.go Normal file
View file

@ -0,0 +1,63 @@
package asm
import (
"encoding/binary"
"git.urbach.dev/cli/q/src/arm"
)
type compilerARM struct {
*compiler
}
func (c *compiler) append(code uint32) {
c.code = binary.LittleEndian.AppendUint32(c.code, code)
}
func (c *compilerARM) Compile(instr Instruction) {
switch instr := instr.(type) {
case *Call:
start := len(c.code)
c.append(arm.Call(0))
c.Defer(func() {
address, exists := c.labels[instr.Label]
if !exists {
panic("unknown label: " + instr.Label)
}
offset := (address - start) / 4
binary.LittleEndian.PutUint32(c.code[start:start+4], arm.Call(offset))
})
case *FunctionStart:
c.append(arm.StorePair(arm.FP, arm.LR, arm.SP, -16))
c.append(arm.MoveRegisterRegister(arm.FP, arm.SP))
case *FunctionEnd:
c.append(arm.LoadPair(arm.FP, arm.LR, arm.SP, 16))
case *Label:
c.labels[instr.Name] = len(c.code)
case *MoveRegisterLabel:
start := len(c.code)
c.append(arm.LoadAddress(instr.Destination, 0))
c.Defer(func() {
address, exists := c.labels[instr.Label]
if !exists {
panic("unknown label: " + instr.Label)
}
offset := address - start
binary.LittleEndian.PutUint32(c.code[start:start+4], arm.LoadAddress(instr.Destination, offset))
})
case *MoveRegisterNumber:
c.code = arm.MoveRegisterNumber(c.code, instr.Destination, instr.Number)
case *MoveRegisterRegister:
c.append(arm.MoveRegisterRegister(instr.Destination, instr.Source))
case *Return:
c.append(arm.Return())
case *Syscall:
c.append(arm.Syscall())
}
}

54
src/asm/compilerX86.go Normal file
View file

@ -0,0 +1,54 @@
package asm
import (
"encoding/binary"
"git.urbach.dev/cli/q/src/x86"
)
type compilerX86 struct {
*compiler
}
func (c *compilerX86) Compile(instr Instruction) {
switch instr := instr.(type) {
case *Call:
c.code = x86.Call(c.code, 0)
end := len(c.code)
c.Defer(func() {
address, exists := c.labels[instr.Label]
if !exists {
panic("unknown label: " + instr.Label)
}
offset := address - end
binary.LittleEndian.PutUint32(c.code[end-4:end], uint32(offset))
})
case *Label:
c.labels[instr.Name] = len(c.code)
case *MoveRegisterLabel:
c.code = x86.LoadAddress(c.code, instr.Destination, 0)
end := len(c.code)
c.Defer(func() {
address, exists := c.labels[instr.Label]
if !exists {
panic("unknown label: " + instr.Label)
}
offset := address - end
binary.LittleEndian.PutUint32(c.code[end-4:end], uint32(offset))
})
case *MoveRegisterNumber:
c.code = x86.MoveRegisterNumber(c.code, instr.Destination, instr.Number)
case *MoveRegisterRegister:
c.code = x86.MoveRegisterRegister(c.code, instr.Destination, instr.Source)
case *Return:
c.code = x86.Return(c.code)
case *Syscall:
c.code = x86.Syscall(c.code)
}
}

View file

@ -27,7 +27,7 @@ func _build(args []string) int {
return 0 return 0
} }
err = linker.WriteExecutable(b, result) err = linker.WriteFile(b.Executable(), b, result)
return exit(err) return exit(err)
} }

View file

@ -16,8 +16,11 @@ func Compile(b *build.Build) (*core.Environment, error) {
return nil, err return nil, err
} }
if len(all.Files) == 0 { // Check for existence of `main`
return nil, NoInputFiles _, exists := all.Functions["main.main"]
if !exists {
return nil, MissingMainFunction
} }
compileFunctions(maps.Values(all.Functions)) compileFunctions(maps.Values(all.Functions))

View file

@ -19,7 +19,7 @@ func TestNoInputFiles(t *testing.T) {
b := build.New(".") b := build.New(".")
_, err := compiler.Compile(b) _, err := compiler.Compile(b)
assert.NotNil(t, err) assert.NotNil(t, err)
assert.True(t, errors.Is(err, compiler.NoInputFiles)) assert.True(t, errors.Is(err, compiler.MissingMainFunction))
} }
func TestHelloExample(t *testing.T) { func TestHelloExample(t *testing.T) {

View file

@ -3,5 +3,5 @@ package compiler
import "git.urbach.dev/cli/q/src/errors" import "git.urbach.dev/cli/q/src/errors"
var ( var (
NoInputFiles = errors.String("No input files") MissingMainFunction = errors.String("Missing main function")
) )

View file

@ -1,21 +1,28 @@
package core package core
import ( import (
"slices"
"git.urbach.dev/cli/q/src/asm"
"git.urbach.dev/cli/q/src/cpu" "git.urbach.dev/cli/q/src/cpu"
"git.urbach.dev/cli/q/src/ssa"
"git.urbach.dev/cli/q/src/token" "git.urbach.dev/cli/q/src/token"
) )
// Compile turns a function into machine code. // Compile turns a function into machine code.
func (f *Function) Compile() { func (f *Function) Compile() {
registerCount := 0 extra := 0
for _, input := range f.Input { for i, input := range f.Input {
f.Identifiers[input.Name] = f.AppendRegister(cpu.Register(registerCount)) if input.Name == "_" {
registerCount++ continue
}
f.Identifiers[input.Name] = f.AppendRegister(i + extra)
if input.TypeTokens[0].Kind == token.ArrayStart { if input.TypeTokens[0].Kind == token.ArrayStart {
f.Identifiers[input.Name+".length"] = f.AppendRegister(cpu.Register(registerCount)) extra++
registerCount++ f.Identifiers[input.Name+".length"] = f.AppendRegister(i + extra)
} }
} }
@ -28,4 +35,83 @@ func (f *Function) Compile() {
} }
f.Err = f.CheckDeadCode() f.Err = f.CheckDeadCode()
if f.Err != nil {
return
}
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:
f.mv(instr.Args[1:], f.CPU.Call)
switch arg := instr.Args[0].(type) {
case *ssa.Function:
f.Assembler.Instructions = append(f.Assembler.Instructions, &asm.Call{Label: arg.UniqueName})
}
case *ssa.Syscall:
f.mv(instr.Args, 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{})
}
switch f.Assembler.Instructions[len(f.Assembler.Instructions)-1].(type) {
case *asm.Return:
default:
f.Assembler.Append(&asm.Return{})
}
}
func (f *Function) mv(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.Assembler.SetData("data0", 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: "data0",
})
}
}
} }

View file

@ -15,6 +15,10 @@ func (f *Function) CompileInstruction(instr token.List) error {
expr := expression.Parse(instr) expr := expression.Parse(instr)
if expr == nil {
return nil
}
if expr.Token.Kind == token.Define { if expr.Token.Kind == token.Define {
name := expr.Children[0].String(f.File.Bytes) name := expr.Children[0].String(f.File.Bytes)
value, err := f.Evaluate(expr.Children[1]) value, err := f.Evaluate(expr.Children[1])

View file

@ -2,6 +2,7 @@ package core
import ( import (
"fmt" "fmt"
"slices"
"git.urbach.dev/cli/q/src/errors" "git.urbach.dev/cli/q/src/errors"
"git.urbach.dev/cli/q/src/expression" "git.urbach.dev/cli/q/src/expression"
@ -18,7 +19,16 @@ func (f *Function) Evaluate(expr *expression.Expression) (ssa.Value, error) {
value, exists := f.Identifiers[name] value, exists := f.Identifiers[name]
if !exists { if !exists {
return nil, errors.New(&UnknownIdentifier{Name: name}, f.File, expr.Token.Position) function, exists := f.All.Functions[f.File.Package+"."+name]
if !exists {
return nil, errors.New(&UnknownIdentifier{Name: name}, f.File, expr.Token.Position)
}
f.Dependencies.Add(function)
v := f.AppendFunction(function.UniqueName)
v.Source = expr.Token
return v, nil
} }
return value, nil return value, nil
@ -66,7 +76,7 @@ func (f *Function) Evaluate(expr *expression.Expression) (ssa.Value, error) {
args := make([]ssa.Value, len(children)) args := make([]ssa.Value, len(children))
for i, child := range children { for i, child := range slices.Backward(children) {
value, err := f.Evaluate(child) value, err := f.Evaluate(child)
if err != nil { if err != nil {
@ -93,8 +103,17 @@ func (f *Function) Evaluate(expr *expression.Expression) (ssa.Value, error) {
} }
case token.Dot: case token.Dot:
name := fmt.Sprintf("%s.%s", expr.Children[0].String(f.File.Bytes), expr.Children[1].String(f.File.Bytes)) left := expr.Children[0]
v := f.AppendFunction(name) 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]
if !exists {
return nil, errors.New(&UnknownIdentifier{Name: label}, f.File, left.Token.Position)
}
f.Dependencies.Add(function)
v := f.AppendFunction(function.UniqueName)
v.Source = expr.Children[1].Token v.Source = expr.Children[1].Token
return v, nil return v, nil
} }

View file

@ -3,7 +3,10 @@ package core
import ( import (
"fmt" "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/fs"
"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/token" "git.urbach.dev/cli/q/src/token"
) )
@ -11,14 +14,18 @@ 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
Name string Name string
UniqueName string UniqueName string
File *fs.File File *fs.File
Input []*Parameter Input []*Parameter
Output []*Parameter Output []*Parameter
Body token.List Body token.List
Identifiers map[string]ssa.Value Identifiers map[string]ssa.Value
Err error All *Environment
Dependencies set.Ordered[*Function]
Assembler asm.Assembler
CPU *cpu.CPU
Err error
} }
// NewFunction creates a new function. // NewFunction creates a new function.
@ -33,6 +40,24 @@ func NewFunction(name string, file *fs.File) *Function {
{Instructions: make([]ssa.Value, 0, 8)}, {Instructions: make([]ssa.Value, 0, 8)},
}, },
}, },
Assembler: asm.Assembler{
Instructions: make([]asm.Instruction, 0, 8),
},
}
}
// EachDependency recursively finds all the calls to other functions.
// It avoids calling the same function twice with the help of a hashmap.
func (f *Function) EachDependency(traversed map[*Function]bool, call func(*Function)) {
call(f)
traversed[f] = true
for dep := range f.Dependencies.All() {
if traversed[dep] {
continue
}
dep.EachDependency(traversed, call)
} }
} }
@ -41,6 +66,11 @@ func (f *Function) IsExtern() bool {
return f.Body == nil return f.Body == nil
} }
// IsLeaf returns true if the function doesn't call other functions.
func (f *Function) IsLeaf() bool {
return f.Dependencies.Count() == 0
}
// String returns the unique name. // String returns the unique name.
func (f *Function) String() string { func (f *Function) String() string {
return f.UniqueName return f.UniqueName

7
src/cpu/CPU.go Normal file
View file

@ -0,0 +1,7 @@
package cpu
// CPU represents the processor.
type CPU struct {
Call []Register
Syscall []Register
}

4
src/data/Data.go Normal file
View file

@ -0,0 +1,4 @@
package data
// Data saves slices of bytes referenced by labels.
type Data map[string][]byte

28
src/data/Data_test.go Normal file
View file

@ -0,0 +1,28 @@
package data_test
import (
"testing"
"git.urbach.dev/cli/q/src/data"
"git.urbach.dev/go/assert"
)
func TestInterning(t *testing.T) {
d := data.Data{}
d.Insert("label1", []byte("Hello"))
d.Insert("label2", []byte("ello"))
raw, positions := d.Finalize()
assert.DeepEqual(t, raw, []byte("Hello"))
assert.Equal(t, positions["label1"], 0)
assert.Equal(t, positions["label2"], 1)
}
func TestInterningReverse(t *testing.T) {
d := data.Data{}
d.Insert("label1", []byte("ello"))
d.Insert("label2", []byte("Hello"))
raw, positions := d.Finalize()
assert.DeepEqual(t, raw, []byte("Hello"))
assert.Equal(t, positions["label1"], 1)
assert.Equal(t, positions["label2"], 0)
}

41
src/data/Finalize.go Normal file
View file

@ -0,0 +1,41 @@
package data
import (
"bytes"
"sort"
)
// Finalize returns the final raw data slice and a map of labels with their respective indices.
// It will try to reuse existing data whenever possible.
func (data Data) Finalize() ([]byte, map[string]int) {
var (
keys = make([]string, 0, len(data))
positions = make(map[string]int, len(data))
capacity = 0
)
for key, value := range data {
keys = append(keys, key)
capacity += len(value)
}
sort.SliceStable(keys, func(i, j int) bool {
return len(data[keys[i]]) > len(data[keys[j]])
})
final := make([]byte, 0, capacity)
for _, key := range keys {
raw := data[key]
position := bytes.Index(final, raw)
if position != -1 {
positions[key] = position
} else {
positions[key] = len(final)
final = append(final, raw...)
}
}
return final, positions
}

6
src/data/Insert.go Normal file
View file

@ -0,0 +1,6 @@
package data
// Insert registers a slice of bytes for the given label.
func (data Data) Insert(label string, raw []byte) {
data[label] = raw
}

21
src/data/bench_test.go Normal file
View file

@ -0,0 +1,21 @@
package data_test
import (
"testing"
"git.urbach.dev/cli/q/src/data"
)
func BenchmarkFinalize(b *testing.B) {
d := data.Data{}
d.Insert("1", []byte("Beautiful is better than ugly."))
d.Insert("2", []byte("Explicit is better than implicit."))
d.Insert("3", []byte("Simple is better than complex."))
d.Insert("4", []byte("Complex is better than complicated."))
d.Insert("5", []byte("Flat is better than nested."))
d.Insert("6", []byte("Sparse is better than dense."))
for b.Loop() {
d.Finalize()
}
}

View file

@ -1,50 +0,0 @@
package linker
import (
"encoding/binary"
"os"
"git.urbach.dev/cli/q/src/arm"
"git.urbach.dev/cli/q/src/build"
"git.urbach.dev/cli/q/src/core"
"git.urbach.dev/cli/q/src/elf"
"git.urbach.dev/cli/q/src/x86"
)
// WriteExecutable writes an executable file to disk.
func WriteExecutable(b *build.Build, result *core.Environment) error {
executable := b.Executable()
file, err := os.Create(executable)
if err != nil {
return err
}
code := []byte{}
data := []byte{}
switch b.Arch {
case build.ARM:
code = arm.MoveRegisterNumber(code, arm.X8, 93)
code = arm.MoveRegisterNumber(code, arm.X0, 0)
code = binary.LittleEndian.AppendUint32(code, arm.Syscall())
case build.X86:
code = x86.MoveRegisterNumber(code, x86.R0, 60)
code = x86.MoveRegisterNumber(code, x86.R7, 0)
code = x86.Syscall(code)
}
switch b.OS {
case build.Linux:
elf.Write(file, b, code, data)
}
err = file.Close()
if err != nil {
return err
}
return os.Chmod(executable, 0755)
}

49
src/linker/WriteFile.go Normal file
View file

@ -0,0 +1,49 @@
package linker
import (
"os"
"git.urbach.dev/cli/q/src/asm"
"git.urbach.dev/cli/q/src/build"
"git.urbach.dev/cli/q/src/core"
"git.urbach.dev/cli/q/src/data"
"git.urbach.dev/cli/q/src/elf"
)
// WriteFile writes an executable file to disk.
func WriteFile(executable string, b *build.Build, env *core.Environment) error {
file, err := os.Create(executable)
if err != nil {
return err
}
init := env.Functions["core.init"]
traversed := make(map[*core.Function]bool, len(env.Functions))
final := asm.Assembler{
Instructions: make([]asm.Instruction, 0, 8),
Data: make(data.Data, 32),
}
// This will place the main function immediately after the entry point
// and also add everything the main function calls recursively.
init.EachDependency(traversed, func(f *core.Function) {
final.Merge(&f.Assembler)
})
code, data := final.Compile(b)
switch b.OS {
case build.Linux:
elf.Write(file, b, code, data)
}
err = file.Close()
if err != nil {
return err
}
return os.Chmod(executable, 0755)
}

View file

@ -11,7 +11,7 @@ import (
"git.urbach.dev/go/assert" "git.urbach.dev/go/assert"
) )
func TestWriteExecutable(t *testing.T) { func TestWriteFile(t *testing.T) {
tmpDir := filepath.Join(os.TempDir(), "q", "tests") tmpDir := filepath.Join(os.TempDir(), "q", "tests")
err := os.MkdirAll(tmpDir, 0755) err := os.MkdirAll(tmpDir, 0755)
assert.Nil(t, err) assert.Nil(t, err)
@ -29,10 +29,10 @@ func TestWriteExecutable(t *testing.T) {
assert.Nil(t, err) assert.Nil(t, err)
b.SetArch(build.ARM) b.SetArch(build.ARM)
err = linker.WriteExecutable(b, env) err = linker.WriteFile(b.Executable(), b, env)
assert.Nil(t, err) assert.Nil(t, err)
b.SetArch(build.X86) b.SetArch(build.X86)
err = linker.WriteExecutable(b, env) err = linker.WriteFile(b.Executable(), b, env)
assert.Nil(t, err) assert.Nil(t, err)
} }

View file

@ -1,9 +1,12 @@
package scanner package scanner
import ( import (
"path/filepath"
"git.urbach.dev/cli/q/src/build" "git.urbach.dev/cli/q/src/build"
"git.urbach.dev/cli/q/src/core" "git.urbach.dev/cli/q/src/core"
"git.urbach.dev/cli/q/src/fs" "git.urbach.dev/cli/q/src/fs"
"git.urbach.dev/cli/q/src/global"
) )
// Scan scans all the files included in the build. // Scan scans all the files included in the build.
@ -16,6 +19,7 @@ func Scan(b *build.Build) (*core.Environment, error) {
} }
go func() { go func() {
s.queueDirectory(filepath.Join(global.Library, "core"), "core")
s.queue(b.Files...) s.queue(b.Files...)
s.group.Wait() s.group.Wait()
close(s.functions) close(s.functions)
@ -36,6 +40,7 @@ func Scan(b *build.Build) (*core.Environment, error) {
continue continue
} }
f.All = all
all.Functions[f.UniqueName] = f all.Functions[f.UniqueName] = f
case file, ok := <-s.files: case file, ok := <-s.files:

View file

@ -1,9 +1,12 @@
package scanner package scanner
import ( import (
"git.urbach.dev/cli/q/src/arm"
"git.urbach.dev/cli/q/src/build"
"git.urbach.dev/cli/q/src/errors" "git.urbach.dev/cli/q/src/errors"
"git.urbach.dev/cli/q/src/fs" "git.urbach.dev/cli/q/src/fs"
"git.urbach.dev/cli/q/src/token" "git.urbach.dev/cli/q/src/token"
"git.urbach.dev/cli/q/src/x86"
) )
// scanFunction scans a function. // scanFunction scans a function.
@ -67,6 +70,13 @@ func (s *scanner) scanFunction(file *fs.File, tokens token.List, i int) (int, er
return i, errors.New(ExpectedFunctionDefinition, file, tokens[i].Position) return i, errors.New(ExpectedFunctionDefinition, file, tokens[i].Position)
} }
switch s.build.Arch {
case build.ARM:
function.CPU = &arm.CPU
case build.X86:
function.CPU = &x86.CPU
}
function.Body = tokens[bodyStart:i] function.Body = tokens[bodyStart:i]
s.functions <- function s.functions <- function
return i, nil return i, nil

38
src/set/Ordered.go Normal file
View file

@ -0,0 +1,38 @@
package set
import (
"iter"
"slices"
)
// Ordered is an ordered set.
type Ordered[T comparable] struct {
values []T
}
// Add adds a value to the set if it doesn't exist yet.
// It returns `false` if it already exists, `true` if it was added.
func (set *Ordered[T]) Add(value T) bool {
if slices.Contains(set.values, value) {
return false
}
set.values = append(set.values, value)
return true
}
// All returns an iterator over all the values in the set.
func (set *Ordered[T]) All() iter.Seq[T] {
return func(yield func(T) bool) {
for _, value := range set.values {
if !yield(value) {
return
}
}
}
}
// Count returns the number of elements in the set.
func (set *Ordered[T]) Count() int {
return len(set.values)
}

View file

@ -1,9 +1,5 @@
package ssa package ssa
import (
"git.urbach.dev/cli/q/src/cpu"
)
// IR is a list of basic blocks. // IR is a list of basic blocks.
type IR struct { type IR struct {
Blocks []*Block Blocks []*Block
@ -44,8 +40,8 @@ func (f *IR) AppendInt(x int) *Int {
} }
// AppendRegister adds a new register value to the last block. // AppendRegister adds a new register value to the last block.
func (f *IR) AppendRegister(reg cpu.Register) *Register { func (f *IR) AppendRegister(index int) *Parameter {
v := &Register{Register: reg} v := &Parameter{Index: uint8(index)}
f.Append(v) f.Append(v)
return v return v
} }

31
src/ssa/Parameter.go Normal file
View file

@ -0,0 +1,31 @@
package ssa
import "fmt"
type Parameter struct {
Index uint8
Liveness
HasToken
}
func (v *Parameter) Dependencies() []Value {
return nil
}
func (a *Parameter) Equals(v Value) bool {
b, sameType := v.(*Parameter)
if !sameType {
return false
}
return a.Index == b.Index
}
func (v *Parameter) IsConst() bool {
return true
}
func (v *Parameter) String() string {
return fmt.Sprintf("arg[%d]", v.Index)
}

View file

@ -1,31 +0,0 @@
package ssa
import "git.urbach.dev/cli/q/src/cpu"
type Register struct {
Register cpu.Register
Liveness
HasToken
}
func (v *Register) Dependencies() []Value {
return nil
}
func (a *Register) Equals(v Value) bool {
b, sameType := v.(*Register)
if !sameType {
return false
}
return a.Register == b.Register
}
func (v *Register) IsConst() bool {
return true
}
func (v *Register) String() string {
return v.Register.String()
}

View file

@ -20,3 +20,8 @@ const (
R14 R14
R15 R15
) )
var CPU = cpu.CPU{
Call: []cpu.Register{R0, R7, R6, R2, R10, R8, R9},
Syscall: []cpu.Register{R0, R7, R6, R2, R10, R8, R9},
}

81
tests/examples_test.go Normal file
View file

@ -0,0 +1,81 @@
package tests_test
import (
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"git.urbach.dev/cli/q/src/build"
"git.urbach.dev/cli/q/src/compiler"
"git.urbach.dev/cli/q/src/linker"
"git.urbach.dev/go/assert"
)
var examples = []struct {
Name string
Input string
Output string
ExitCode int
}{
{"hello", "", "Hello\n", 0},
}
func TestExamples(t *testing.T) {
for _, test := range examples {
directory := filepath.Join("..", "examples", test.Name)
run(t, directory, test.Input, test.Output, test.ExitCode)
}
}
func BenchmarkExamples(b *testing.B) {
for _, test := range examples {
b.Run(test.Name, func(b *testing.B) {
example := build.New(filepath.Join("..", "examples", test.Name))
for b.Loop() {
_, err := compiler.Compile(example)
assert.Nil(b, err)
}
})
}
}
// run builds and runs the file to check if the output matches the expected output.
func run(t *testing.T, path string, input string, expectedOutput string, expectedExitCode int) {
b := build.New(path)
env, err := compiler.Compile(b)
assert.Nil(t, err)
tmpDir := filepath.Join(os.TempDir(), "q", "tests")
err = os.MkdirAll(tmpDir, 0755)
assert.Nil(t, err)
executable := b.Executable()
executable = filepath.Join(tmpDir, filepath.Base(executable))
err = linker.WriteFile(executable, b, env)
assert.Nil(t, err)
stat, err := os.Stat(executable)
assert.Nil(t, err)
assert.True(t, stat.Size() > 0)
cmd := exec.Command(executable)
cmd.Stdin = strings.NewReader(input)
output, err := cmd.Output()
exitCode := 0
if err != nil {
exitError, ok := err.(*exec.ExitError)
if !ok {
t.Fatal(exitError)
}
exitCode = exitError.ExitCode()
}
assert.Equal(t, exitCode, expectedExitCode)
assert.DeepEqual(t, string(output), expectedOutput)
}