diff --git a/lib/core/core_linux_arm.q b/lib/core/core_linux_arm.q new file mode 100644 index 0000000..f20e626 --- /dev/null +++ b/lib/core/core_linux_arm.q @@ -0,0 +1,12 @@ +init() { + main.main() + exit() +} + +exit() { + syscall(93, 0) +} + +crash() { + syscall(93, 1) +} \ No newline at end of file diff --git a/lib/core/core_linux_x86.q b/lib/core/core_linux_x86.q new file mode 100644 index 0000000..9c8f519 --- /dev/null +++ b/lib/core/core_linux_x86.q @@ -0,0 +1,12 @@ +init() { + main.main() + exit() +} + +exit() { + syscall(60, 0) +} + +crash() { + syscall(60, 1) +} \ No newline at end of file diff --git a/lib/core/core_mac.q b/lib/core/core_mac.q new file mode 100644 index 0000000..91618b4 --- /dev/null +++ b/lib/core/core_mac.q @@ -0,0 +1,12 @@ +init() { + main.main() + exit() +} + +exit() { + syscall(0x2000001, 0) +} + +crash() { + syscall(0x2000001, 1) +} \ No newline at end of file diff --git a/lib/core/core_windows.q b/lib/core/core_windows.q new file mode 100644 index 0000000..e0f564d --- /dev/null +++ b/lib/core/core_windows.q @@ -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) +// } +// } \ No newline at end of file diff --git a/lib/io/write_linux_arm.q b/lib/io/write_linux_arm.q index 9743267..143b676 100644 --- a/lib/io/write_linux_arm.q +++ b/lib/io/write_linux_arm.q @@ -1,3 +1,3 @@ write(buffer []byte) -> (written int) { - return syscall(64, 0, buffer, len(buffer)) + return syscall(64, 1, buffer, len(buffer)) } \ No newline at end of file diff --git a/lib/io/write_linux_x86.q b/lib/io/write_linux_x86.q index 633cfbe..77c3f99 100644 --- a/lib/io/write_linux_x86.q +++ b/lib/io/write_linux_x86.q @@ -1,3 +1,3 @@ write(buffer []byte) -> (written int) { - return syscall(1, 0, buffer, len(buffer)) + return syscall(1, 1, buffer, len(buffer)) } \ No newline at end of file diff --git a/lib/io/write_mac.q b/lib/io/write_mac.q new file mode 100644 index 0000000..cc67bb7 --- /dev/null +++ b/lib/io/write_mac.q @@ -0,0 +1,3 @@ +write(buffer []byte) -> (written int) { + return syscall(0x2000004, 1, buffer, len(buffer)) +} \ No newline at end of file diff --git a/lib/io/write_windows.q b/lib/io/write_windows.q new file mode 100644 index 0000000..ef06d49 --- /dev/null +++ b/lib/io/write_windows.q @@ -0,0 +1,3 @@ +write(_ []byte) -> (written int) { + return 0 +} \ No newline at end of file diff --git a/src/arm/Registers.go b/src/arm/Registers.go index 368850b..92e0241 100644 --- a/src/arm/Registers.go +++ b/src/arm/Registers.go @@ -39,4 +39,9 @@ const ( const ( ZR = SP // Zero register uses the same numerical value as SP -) \ No newline at end of file +) + +var CPU = cpu.CPU{ + Call: []cpu.Register{X0, X1, X2, X3, X4, X5, X6}, + Syscall: []cpu.Register{X8, X0, X1, X2, X3, X4, X5}, +} \ No newline at end of file diff --git a/src/asm/Address.go b/src/asm/Address.go new file mode 100644 index 0000000..cfa5279 --- /dev/null +++ b/src/asm/Address.go @@ -0,0 +1,4 @@ +package asm + +// Address represents a memory address. +type Address = int \ No newline at end of file diff --git a/src/asm/Assembler.go b/src/asm/Assembler.go new file mode 100644 index 0000000..91b4ed1 --- /dev/null +++ b/src/asm/Assembler.go @@ -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) +} \ No newline at end of file diff --git a/src/asm/Instruction.go b/src/asm/Instruction.go new file mode 100644 index 0000000..743ab89 --- /dev/null +++ b/src/asm/Instruction.go @@ -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{} \ No newline at end of file diff --git a/src/asm/compiler.go b/src/asm/compiler.go new file mode 100644 index 0000000..8e0ef18 --- /dev/null +++ b/src/asm/compiler.go @@ -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) +} \ No newline at end of file diff --git a/src/asm/compilerARM.go b/src/asm/compilerARM.go new file mode 100644 index 0000000..2715ee4 --- /dev/null +++ b/src/asm/compilerARM.go @@ -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()) + } +} \ No newline at end of file diff --git a/src/asm/compilerX86.go b/src/asm/compilerX86.go new file mode 100644 index 0000000..429e2dd --- /dev/null +++ b/src/asm/compilerX86.go @@ -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) + } +} \ No newline at end of file diff --git a/src/cli/build.go b/src/cli/build.go index 3bdd6c6..d58e60c 100644 --- a/src/cli/build.go +++ b/src/cli/build.go @@ -27,7 +27,7 @@ func _build(args []string) int { return 0 } - err = linker.WriteExecutable(b, result) + err = linker.WriteFile(b.Executable(), b, result) return exit(err) } diff --git a/src/compiler/Compile.go b/src/compiler/Compile.go index 749bdce..91e908c 100644 --- a/src/compiler/Compile.go +++ b/src/compiler/Compile.go @@ -16,8 +16,11 @@ func Compile(b *build.Build) (*core.Environment, error) { return nil, err } - if len(all.Files) == 0 { - return nil, NoInputFiles + // Check for existence of `main` + _, exists := all.Functions["main.main"] + + if !exists { + return nil, MissingMainFunction } compileFunctions(maps.Values(all.Functions)) diff --git a/src/compiler/Compile_test.go b/src/compiler/Compile_test.go index 789ca11..d788d06 100644 --- a/src/compiler/Compile_test.go +++ b/src/compiler/Compile_test.go @@ -19,7 +19,7 @@ func TestNoInputFiles(t *testing.T) { b := build.New(".") _, err := compiler.Compile(b) 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) { diff --git a/src/compiler/errors.go b/src/compiler/errors.go index f2144c1..f2908a4 100644 --- a/src/compiler/errors.go +++ b/src/compiler/errors.go @@ -3,5 +3,5 @@ package compiler import "git.urbach.dev/cli/q/src/errors" var ( - NoInputFiles = errors.String("No input files") + MissingMainFunction = errors.String("Missing main function") ) \ No newline at end of file diff --git a/src/core/Compile.go b/src/core/Compile.go index 3c0437a..3d14db1 100644 --- a/src/core/Compile.go +++ b/src/core/Compile.go @@ -1,21 +1,28 @@ 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" "git.urbach.dev/cli/q/src/token" ) // Compile turns a function into machine code. func (f *Function) Compile() { - registerCount := 0 + extra := 0 - for _, input := range f.Input { - f.Identifiers[input.Name] = f.AppendRegister(cpu.Register(registerCount)) - registerCount++ + for i, input := range f.Input { + if input.Name == "_" { + continue + } + + f.Identifiers[input.Name] = f.AppendRegister(i + extra) if input.TypeTokens[0].Kind == token.ArrayStart { - f.Identifiers[input.Name+".length"] = f.AppendRegister(cpu.Register(registerCount)) - registerCount++ + extra++ + f.Identifiers[input.Name+".length"] = f.AppendRegister(i + extra) } } @@ -28,4 +35,83 @@ func (f *Function) Compile() { } 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", + }) + } + } } \ No newline at end of file diff --git a/src/core/CompileInstruction.go b/src/core/CompileInstruction.go index aaecc9e..95281b2 100644 --- a/src/core/CompileInstruction.go +++ b/src/core/CompileInstruction.go @@ -15,6 +15,10 @@ func (f *Function) CompileInstruction(instr token.List) error { expr := expression.Parse(instr) + if expr == nil { + return nil + } + if expr.Token.Kind == token.Define { name := expr.Children[0].String(f.File.Bytes) value, err := f.Evaluate(expr.Children[1]) diff --git a/src/core/Evaluate.go b/src/core/Evaluate.go index 1b63506..9073f41 100644 --- a/src/core/Evaluate.go +++ b/src/core/Evaluate.go @@ -2,6 +2,7 @@ package core import ( "fmt" + "slices" "git.urbach.dev/cli/q/src/errors" "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] 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 @@ -66,7 +76,7 @@ func (f *Function) Evaluate(expr *expression.Expression) (ssa.Value, error) { args := make([]ssa.Value, len(children)) - for i, child := range children { + for i, child := range slices.Backward(children) { value, err := f.Evaluate(child) if err != nil { @@ -93,8 +103,17 @@ func (f *Function) Evaluate(expr *expression.Expression) (ssa.Value, error) { } case token.Dot: - name := fmt.Sprintf("%s.%s", expr.Children[0].String(f.File.Bytes), expr.Children[1].String(f.File.Bytes)) - v := f.AppendFunction(name) + 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] + + 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 return v, nil } diff --git a/src/core/Function.go b/src/core/Function.go index 8a0fd60..15c55b4 100644 --- a/src/core/Function.go +++ b/src/core/Function.go @@ -3,7 +3,10 @@ package core 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/token" ) @@ -11,14 +14,18 @@ import ( // Function is the smallest unit of code. type Function struct { ssa.IR - Name string - UniqueName string - File *fs.File - Input []*Parameter - Output []*Parameter - Body token.List - Identifiers map[string]ssa.Value - Err error + Name string + UniqueName string + File *fs.File + Input []*Parameter + Output []*Parameter + Body token.List + Identifiers map[string]ssa.Value + All *Environment + Dependencies set.Ordered[*Function] + Assembler asm.Assembler + CPU *cpu.CPU + Err error } // NewFunction creates a new function. @@ -33,6 +40,24 @@ func NewFunction(name string, file *fs.File) *Function { {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 } +// 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. func (f *Function) String() string { return f.UniqueName diff --git a/src/cpu/CPU.go b/src/cpu/CPU.go new file mode 100644 index 0000000..d13220d --- /dev/null +++ b/src/cpu/CPU.go @@ -0,0 +1,7 @@ +package cpu + +// CPU represents the processor. +type CPU struct { + Call []Register + Syscall []Register +} \ No newline at end of file diff --git a/src/data/Data.go b/src/data/Data.go new file mode 100644 index 0000000..460bbd3 --- /dev/null +++ b/src/data/Data.go @@ -0,0 +1,4 @@ +package data + +// Data saves slices of bytes referenced by labels. +type Data map[string][]byte \ No newline at end of file diff --git a/src/data/Data_test.go b/src/data/Data_test.go new file mode 100644 index 0000000..189f8b8 --- /dev/null +++ b/src/data/Data_test.go @@ -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) +} \ No newline at end of file diff --git a/src/data/Finalize.go b/src/data/Finalize.go new file mode 100644 index 0000000..a604b86 --- /dev/null +++ b/src/data/Finalize.go @@ -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 +} \ No newline at end of file diff --git a/src/data/Insert.go b/src/data/Insert.go new file mode 100644 index 0000000..a0d0819 --- /dev/null +++ b/src/data/Insert.go @@ -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 +} \ No newline at end of file diff --git a/src/data/bench_test.go b/src/data/bench_test.go new file mode 100644 index 0000000..137cf44 --- /dev/null +++ b/src/data/bench_test.go @@ -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() + } +} \ No newline at end of file diff --git a/src/linker/WriteExecutable.go b/src/linker/WriteExecutable.go deleted file mode 100644 index f41f0f9..0000000 --- a/src/linker/WriteExecutable.go +++ /dev/null @@ -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) -} \ No newline at end of file diff --git a/src/linker/WriteFile.go b/src/linker/WriteFile.go new file mode 100644 index 0000000..0cf74b4 --- /dev/null +++ b/src/linker/WriteFile.go @@ -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) +} \ No newline at end of file diff --git a/src/linker/WriteExecutable_test.go b/src/linker/WriteFile_test.go similarity index 84% rename from src/linker/WriteExecutable_test.go rename to src/linker/WriteFile_test.go index 62c2541..87eb4d9 100644 --- a/src/linker/WriteExecutable_test.go +++ b/src/linker/WriteFile_test.go @@ -11,7 +11,7 @@ import ( "git.urbach.dev/go/assert" ) -func TestWriteExecutable(t *testing.T) { +func TestWriteFile(t *testing.T) { tmpDir := filepath.Join(os.TempDir(), "q", "tests") err := os.MkdirAll(tmpDir, 0755) assert.Nil(t, err) @@ -29,10 +29,10 @@ func TestWriteExecutable(t *testing.T) { assert.Nil(t, err) b.SetArch(build.ARM) - err = linker.WriteExecutable(b, env) + err = linker.WriteFile(b.Executable(), b, env) assert.Nil(t, err) b.SetArch(build.X86) - err = linker.WriteExecutable(b, env) + err = linker.WriteFile(b.Executable(), b, env) assert.Nil(t, err) } \ No newline at end of file diff --git a/src/scanner/Scan.go b/src/scanner/Scan.go index 3781a02..79d2d01 100644 --- a/src/scanner/Scan.go +++ b/src/scanner/Scan.go @@ -1,9 +1,12 @@ package scanner import ( + "path/filepath" + "git.urbach.dev/cli/q/src/build" "git.urbach.dev/cli/q/src/core" "git.urbach.dev/cli/q/src/fs" + "git.urbach.dev/cli/q/src/global" ) // Scan scans all the files included in the build. @@ -16,6 +19,7 @@ func Scan(b *build.Build) (*core.Environment, error) { } go func() { + s.queueDirectory(filepath.Join(global.Library, "core"), "core") s.queue(b.Files...) s.group.Wait() close(s.functions) @@ -36,6 +40,7 @@ func Scan(b *build.Build) (*core.Environment, error) { continue } + f.All = all all.Functions[f.UniqueName] = f case file, ok := <-s.files: diff --git a/src/scanner/scanFunction.go b/src/scanner/scanFunction.go index 0b58898..1a90692 100644 --- a/src/scanner/scanFunction.go +++ b/src/scanner/scanFunction.go @@ -1,9 +1,12 @@ package scanner 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/fs" "git.urbach.dev/cli/q/src/token" + "git.urbach.dev/cli/q/src/x86" ) // 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) } + switch s.build.Arch { + case build.ARM: + function.CPU = &arm.CPU + case build.X86: + function.CPU = &x86.CPU + } + function.Body = tokens[bodyStart:i] s.functions <- function return i, nil diff --git a/src/set/Ordered.go b/src/set/Ordered.go new file mode 100644 index 0000000..674c6ff --- /dev/null +++ b/src/set/Ordered.go @@ -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) +} \ No newline at end of file diff --git a/src/ssa/IR.go b/src/ssa/IR.go index f7feae3..8a7cb6b 100644 --- a/src/ssa/IR.go +++ b/src/ssa/IR.go @@ -1,9 +1,5 @@ package ssa -import ( - "git.urbach.dev/cli/q/src/cpu" -) - // IR is a list of basic blocks. type IR struct { Blocks []*Block @@ -44,8 +40,8 @@ func (f *IR) AppendInt(x int) *Int { } // AppendRegister adds a new register value to the last block. -func (f *IR) AppendRegister(reg cpu.Register) *Register { - v := &Register{Register: reg} +func (f *IR) AppendRegister(index int) *Parameter { + v := &Parameter{Index: uint8(index)} f.Append(v) return v } diff --git a/src/ssa/Parameter.go b/src/ssa/Parameter.go new file mode 100644 index 0000000..e5f8cb0 --- /dev/null +++ b/src/ssa/Parameter.go @@ -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) +} \ No newline at end of file diff --git a/src/ssa/Register.go b/src/ssa/Register.go deleted file mode 100644 index 2ae96f4..0000000 --- a/src/ssa/Register.go +++ /dev/null @@ -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() -} \ No newline at end of file diff --git a/src/x86/Registers.go b/src/x86/Registers.go index daea8a2..7933258 100644 --- a/src/x86/Registers.go +++ b/src/x86/Registers.go @@ -19,4 +19,9 @@ const ( R13 R14 R15 -) \ No newline at end of file +) + +var CPU = cpu.CPU{ + Call: []cpu.Register{R0, R7, R6, R2, R10, R8, R9}, + Syscall: []cpu.Register{R0, R7, R6, R2, R10, R8, R9}, +} \ No newline at end of file diff --git a/tests/examples_test.go b/tests/examples_test.go new file mode 100644 index 0000000..913a7a7 --- /dev/null +++ b/tests/examples_test.go @@ -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) +} \ No newline at end of file