Refactored code structure

This commit is contained in:
Eduard Urbach 2024-07-03 11:39:24 +02:00
parent ed03f6a802
commit feebfe65bb
Signed by: akyoto
GPG Key ID: C874F672B1AF20C0
54 changed files with 583 additions and 450 deletions

View File

@ -83,22 +83,30 @@ Each function will then be translated to generic assembler instructions.
All the functions that are required to run the program will be added to the final assembler.
The final assembler resolves label addresses, optimizes the performance and generates the specific x86-64 machine code from the generic instruction set.
### [src/build/Function.go](src/build/Function.go)
### [src/build/core/Function.go](src/build/core/Function.go)
This is the "heart" of the compiler.
Each function runs `f.Compile()` which organizes the source code into instructions that are then compiled via `f.CompileInstruction`.
You can think of instructions as the individual lines in your source code, but instructions can also span over multiple lines.
Each function runs `f.Compile` which organizes the source code into an abstract syntax tree that is then compiled via `f.CompileAST`.
You can think of AST nodes as the individual statements in your source code.
### [src/build/ast/Parse.go](src/build/ast/Parse.go)
This is what generates the AST from tokens.
### [src/build/expression/Parse.go](src/build/expression/Parse.go)
This is what generates expressions from tokens.
## Tests
```shell
go test -coverpkg=./...
go test ./... -v
```
## Benchmarks
```shell
go test -bench=. -benchmem
go test ./tests -bench=. -benchmem
```
## License

View File

@ -1,8 +1,7 @@
main() {
x := f(2, 3)
syscall(60, x)
syscall(60, f(1) + f(2) + f(3))
}
f(x, y) {
return (x + y) * (x + y)
f(x) {
return x + 1
}

View File

@ -5,7 +5,7 @@ main() {
}
print(address, length) {
write(length-2, address, length)
write(1, address, length)
}
write(fd, address, length) {

View File

@ -1,19 +0,0 @@
package build
import (
"git.akyoto.dev/cli/q/src/build/expression"
"git.akyoto.dev/cli/q/src/errors"
)
// CompileAssignment compiles an assignment.
func (f *Function) CompileAssignment(expr *expression.Expression) error {
name := expr.Children[0].Token.Text()
variable, exists := f.variables[name]
if !exists {
return errors.New(&errors.UnknownIdentifier{Name: name}, f.File, expr.Children[0].Token.Position)
}
defer f.useVariable(variable)
return f.Execute(expr.Token, variable.Register, expr.Children[1])
}

View File

@ -3,6 +3,9 @@ package build
import (
"path/filepath"
"strings"
"git.akyoto.dev/cli/q/src/build/core"
"git.akyoto.dev/cli/q/src/build/scanner"
)
// Build describes a compiler build.
@ -18,18 +21,28 @@ func New(files ...string) *Build {
}
// Run parses the input files and generates an executable file.
func (build *Build) Run() (Result, error) {
functions, errors := scan(build.Files)
return compile(functions, errors)
func (build *Build) Run() (core.Result, error) {
functions, errors := scanner.Scan(build.Files)
return core.Compile(functions, errors)
}
// Executable returns the path to the executable.
func (build *Build) Executable() string {
directory, _ := filepath.Abs(build.Files[0])
path, _ := filepath.Abs(build.Files[0])
if strings.HasSuffix(directory, ".q") {
directory = filepath.Dir(directory)
if strings.HasSuffix(path, ".q") {
return fromFileName(path)
} else {
return fromDirectoryName(path)
}
}
return filepath.Join(directory, filepath.Base(directory))
// fromDirectoryName returns the executable path based on the directory name.
func fromDirectoryName(path string) string {
return filepath.Join(path, filepath.Base(path))
}
// fromFileName returns the executable path based on the file name.
func fromFileName(path string) string {
return filepath.Join(filepath.Dir(path), strings.TrimSuffix(filepath.Base(path), ".q"))
}

View File

@ -1,48 +0,0 @@
package build
import (
"git.akyoto.dev/cli/q/src/build/asm"
"git.akyoto.dev/cli/q/src/build/ast"
"git.akyoto.dev/cli/q/src/build/config"
"git.akyoto.dev/cli/q/src/build/cpu"
"git.akyoto.dev/cli/q/src/build/expression"
)
// ExpressionToRegister moves the result of an expression into the given register.
func (f *Function) ExpressionToRegister(root *expression.Expression, register cpu.Register) error {
if config.Verbose {
f.Logf("%s to register %s", root, register)
}
operation := root.Token
if root.IsLeaf() {
return f.TokenToRegister(operation, register)
}
if ast.IsFunctionCall(root) {
err := f.CompileFunctionCall(root)
if err != nil {
return err
}
if register != f.cpu.Return[0] {
f.assembler.RegisterRegister(asm.MOVE, register, f.cpu.Return[0])
}
return nil
}
left := root.Children[0]
right := root.Children[1]
err := f.ExpressionToRegister(left, register)
if err != nil {
return err
}
f.SaveRegister(register)
return f.Execute(operation, register, right)
}

View File

@ -1 +0,0 @@
package build

View File

@ -1,29 +0,0 @@
package build
import (
"bufio"
"os"
"git.akyoto.dev/cli/q/src/build/elf"
)
// Write writes the executable file to disk.
func Write(filePath string, code []byte, data []byte) error {
file, err := os.Create(filePath)
if err != nil {
return err
}
buffer := bufio.NewWriter(file)
executable := elf.New(code, data)
executable.Write(buffer)
buffer.Flush()
err = file.Close()
if err != nil {
return err
}
return os.Chmod(filePath, 0755)
}

View File

@ -23,7 +23,7 @@ const (
var (
CallRegisters = SyscallRegisters
GeneralRegisters = []cpu.Register{RBX, RBP, R12, R13, R14, R15}
GeneralRegisters = []cpu.Register{RCX, RBX, RBP, R11, R12, R13, R14, R15}
SyscallRegisters = []cpu.Register{RAX, RDI, RSI, RDX, R10, R8, R9}
ReturnValueRegisters = []cpu.Register{RAX, RCX, R11}
)

View File

@ -63,6 +63,9 @@ func (a Assembler) Finalize() ([]byte, []byte) {
},
})
case COMMENT:
continue
case JUMP:
code = x64.Jump8(code, 0x00)
size := 1

View File

@ -44,6 +44,16 @@ func (a *Assembler) Label(name string) {
})
}
// Comment adds a comment at the current position.
func (a *Assembler) Comment(text string) {
a.Instructions = append(a.Instructions, Instruction{
Mnemonic: COMMENT,
Data: &Label{
Name: text,
},
})
}
// Call calls a function whose position is identified by a label.
func (a *Assembler) Call(name string) {
a.Instructions = append(a.Instructions, Instruction{

View File

@ -6,6 +6,7 @@ const (
NONE Mnemonic = iota
ADD
CALL
COMMENT
DIV
JUMP
MUL
@ -23,40 +24,31 @@ func (m Mnemonic) String() string {
switch m {
case ADD:
return "add"
case CALL:
return "call"
case COMMENT:
return "comment"
case DIV:
return "div"
case JUMP:
return "jump"
case LABEL:
return "label"
case MOVE:
return "move"
case MUL:
return "mul"
case POP:
return "pop"
case PUSH:
return "push"
case RETURN:
return "return"
case SUB:
return "sub"
case SYSCALL:
return "syscall"
default:
return ""
}
return "NONE"
}

View File

@ -1,36 +1,54 @@
package ast
import (
"fmt"
"git.akyoto.dev/cli/q/src/build/expression"
"git.akyoto.dev/cli/q/src/build/token"
)
type Node interface{}
type Node fmt.Stringer
type AST []Node
type Assign struct {
Value *expression.Expression
Name token.Token
Operator token.Token
}
func (node *Assign) String() string {
return fmt.Sprintf("(= %s %s)", node.Name.Text(), node.Value)
}
type Call struct {
Expression *expression.Expression
}
func (node *Call) String() string {
return node.Expression.String()
}
type Define struct {
Value *expression.Expression
Name token.Token
}
type If struct {
Condition *expression.Expression
Body AST
func (node *Define) String() string {
return fmt.Sprintf("(= %s %s)", node.Name.Text(), node.Value)
}
type Loop struct {
Body AST
}
func (node *Loop) String() string {
return fmt.Sprintf("(loop %s)", node.Body)
}
type Return struct {
Value *expression.Expression
}
func (node *Return) String() string {
return fmt.Sprintf("(return %s)", node.Value)
}

View File

@ -1,10 +1,10 @@
package ast
import (
"git.akyoto.dev/cli/q/src/build/errors"
"git.akyoto.dev/cli/q/src/build/expression"
"git.akyoto.dev/cli/q/src/build/keyword"
"git.akyoto.dev/cli/q/src/build/token"
"git.akyoto.dev/cli/q/src/errors"
)
// Parse generates an AST from a list of tokens.
@ -75,7 +75,7 @@ func toASTNode(tokens token.List) (Node, error) {
name := expr.Children[0].Token
value := expr.Children[1]
return &Assign{Name: name, Value: value}, nil
return &Assign{Name: name, Value: value, Operator: expr.Token}, nil
case IsFunctionCall(expr):
return &Call{Expression: expr}, nil

View File

@ -8,17 +8,28 @@ import (
"git.akyoto.dev/go/assert"
)
func TestBuild(t *testing.T) {
func TestBuildDirectory(t *testing.T) {
b := build.New("../../examples/hello")
_, err := b.Run()
assert.Nil(t, err)
}
func TestExecutable(t *testing.T) {
func TestBuildFile(t *testing.T) {
b := build.New("../../examples/hello/hello.q")
_, err := b.Run()
assert.Nil(t, err)
}
func TestExecutableFromDirectory(t *testing.T) {
b := build.New("../../examples/hello")
assert.Equal(t, filepath.Base(b.Executable()), "hello")
}
func TestExecutableFromFile(t *testing.T) {
b := build.New("../../examples/hello/hello.q")
assert.Equal(t, filepath.Base(b.Executable()), "hello")
}
func TestNonExisting(t *testing.T) {
b := build.New("does-not-exist")
_, err := b.Run()

View File

@ -1,81 +0,0 @@
package build
import (
"fmt"
"git.akyoto.dev/cli/q/src/build/asm"
"git.akyoto.dev/cli/q/src/build/ast"
"git.akyoto.dev/cli/q/src/build/cpu"
"git.akyoto.dev/go/color/ansi"
)
// compiler is the data structure we embed in each function to preserve compilation state.
type compiler struct {
err error
definitions map[string]*Definition
variables map[string]*Variable
functions map[string]*Function
finished chan struct{}
assembler asm.Assembler
debug []debug
cpu cpu.CPU
count counter
sideEffects int
}
// counter stores how often a certain statement appeared so we can generate a unique label from it.
type counter struct {
loop int
}
// debug is used to look up the source code at the given position.
type debug struct {
source ast.Node
position int
}
// PrintInstructions shows the assembly instructions.
func (c *compiler) PrintInstructions() {
ansi.Dim.Println("╭──────────────────────────────────────╮")
for i, x := range c.assembler.Instructions {
instruction := c.sourceAt(i)
if instruction != nil {
ansi.Dim.Println("├──────────────────────────────────────┤")
}
ansi.Dim.Print("│ ")
if x.Mnemonic == asm.LABEL {
ansi.Yellow.Printf("%-36s", x.Data.String()+":")
} else {
ansi.Green.Printf("%-8s", x.Mnemonic.String())
if x.Data != nil {
fmt.Printf("%-28s", x.Data.String())
} else {
fmt.Printf("%-28s", "")
}
}
ansi.Dim.Print(" │\n")
}
ansi.Dim.Println("╰──────────────────────────────────────╯")
}
// sourceAt retrieves the source code at the given position or `nil`.
func (c *compiler) sourceAt(position int) ast.Node {
for _, record := range c.debug {
if record.position == position {
return record.source
}
if record.position > position {
return nil
}
}
return nil
}

View File

@ -9,5 +9,6 @@ const (
var (
Assembler = false
Verbose = false
Comments = false
Dry = false
)

View File

@ -1,13 +1,11 @@
package build
package core
import (
"sync"
"git.akyoto.dev/cli/q/src/errors"
"git.akyoto.dev/cli/q/src/build/errors"
)
// compile waits for the scan to finish and compiles all functions.
func compile(functions <-chan *Function, errs <-chan error) (Result, error) {
// Compile waits for the scan to finish and compiles all functions.
func Compile(functions <-chan *Function, errs <-chan error) (Result, error) {
result := Result{}
allFunctions := map[string]*Function{}
@ -32,8 +30,10 @@ func compile(functions <-chan *Function, errs <-chan error) (Result, error) {
}
}
compileFunctions(allFunctions)
// Start parallel compilation
CompileAllFunctions(allFunctions)
// Report errors if any occurred
for _, function := range allFunctions {
if function.err != nil {
return result, function.err
@ -42,6 +42,7 @@ func compile(functions <-chan *Function, errs <-chan error) (Result, error) {
result.InstructionCount += len(function.assembler.Instructions)
}
// Check for existence of `main`
main, exists := allFunctions["main"]
if !exists {
@ -52,19 +53,3 @@ func compile(functions <-chan *Function, errs <-chan error) (Result, error) {
result.Functions = allFunctions
return result, nil
}
// compileFunctions starts a goroutine for each function compilation and waits for completion.
func compileFunctions(functions map[string]*Function) {
wg := sync.WaitGroup{}
for _, function := range functions {
wg.Add(1)
go func() {
defer wg.Done()
function.Compile()
}()
}
wg.Wait()
}

View File

@ -0,0 +1,19 @@
package core
import "sync"
// CompileAllFunctions starts a goroutine for each function compilation and waits for completion.
func CompileAllFunctions(functions map[string]*Function) {
wg := sync.WaitGroup{}
for _, function := range functions {
wg.Add(1)
go func() {
defer wg.Done()
function.Compile()
}()
}
wg.Wait()
}

View File

@ -0,0 +1,19 @@
package core
import (
"git.akyoto.dev/cli/q/src/build/ast"
"git.akyoto.dev/cli/q/src/build/errors"
)
// CompileAssign compiles an assign statement.
func (f *Function) CompileAssign(node *ast.Assign) error {
name := node.Name.Text()
variable, exists := f.variables[name]
if !exists {
return errors.New(&errors.UnknownIdentifier{Name: name}, f.File, node.Name.Position)
}
defer f.useVariable(variable)
return f.Execute(node.Operator, variable.Register, node.Value)
}

View File

@ -1,39 +1,16 @@
package build
package core
import (
"git.akyoto.dev/cli/q/src/build/config"
"git.akyoto.dev/cli/q/src/build/cpu"
"git.akyoto.dev/cli/q/src/build/expression"
)
// CompileFunctionCall executes a function call.
// CompileCall executes a function call.
// All call registers must hold the correct parameter values before the function invocation.
// Registers that are in use must be saved if they are modified by the function.
// After the function call, they must be restored in reverse order.
func (f *Function) CompileFunctionCall(expr *expression.Expression) error {
funcName := expr.Children[0].Token.Text()
if funcName == "syscall" {
return f.CompileSyscall(expr)
}
function := f.functions[funcName]
if function != f {
function.Wait()
}
parameters := expr.Children[1:]
registers := f.cpu.Call[:len(parameters)]
err := f.ExpressionsToRegisters(parameters, registers)
if config.Verbose {
f.Logf("call: %s", funcName)
}
f.assembler.Call(funcName)
f.sideEffects += function.sideEffects
func (f *Function) CompileCall(expr *expression.Expression) error {
_, err := f.EvaluateCall(expr)
return err
}
@ -49,15 +26,9 @@ func (f *Function) CompileSyscall(expr *expression.Expression) error {
// ExpressionsToRegisters moves multiple expressions into the specified registers.
func (f *Function) ExpressionsToRegisters(expressions []*expression.Expression, registers []cpu.Register) error {
for _, register := range registers {
f.SaveRegister(register)
}
for i := len(expressions) - 1; i >= 0; i-- {
expression := expressions[i]
register := registers[i]
err := f.ExpressionToRegister(expression, register)
for i := len(registers) - 1; i >= 0; i-- {
f.SaveRegister(registers[i])
err := f.EvaluateTo(expressions[i], registers[i])
if err != nil {
return err

View File

@ -1,22 +1,24 @@
package build
package core
import (
"fmt"
"git.akyoto.dev/cli/q/src/build/ast"
"git.akyoto.dev/cli/q/src/build/config"
"git.akyoto.dev/cli/q/src/build/errors"
"git.akyoto.dev/cli/q/src/build/expression"
"git.akyoto.dev/cli/q/src/build/token"
"git.akyoto.dev/cli/q/src/errors"
)
// CompileVariableDefinition compiles a variable definition.
func (f *Function) CompileVariableDefinition(node *ast.Define) error {
// CompileDefinition compiles a variable definition.
func (f *Function) CompileDefinition(node *ast.Define) error {
name := node.Name.Text()
if f.identifierExists(name) {
return errors.New(&errors.VariableAlreadyExists{Name: name}, f.File, node.Name.Position)
}
uses := countIdentifier(f.Body, name) - 1
uses := CountIdentifier(f.Body, name) - 1
if uses == 0 {
return errors.New(&errors.UnusedVariable{Name: name}, f.File, node.Name.Position)
@ -48,29 +50,46 @@ func (f *Function) CompileVariableDefinition(node *ast.Define) error {
return f.storeVariableInRegister(name, value, uses)
}
func (f *Function) addVariable(variable *Variable) {
if config.Verbose {
f.Logf("%s occupies %s (alive: %d)", variable.Name, variable.Register, variable.Alive)
func (f *Function) AddVariable(variable *Variable) {
if config.Comments {
f.assembler.Comment(fmt.Sprintf("%s = %s (%s, %d uses)", variable.Name, variable.Value, variable.Register, variable.Alive))
}
f.variables[variable.Name] = variable
f.cpu.Use(variable.Register)
}
func (f *Function) addTemporary(root *expression.Expression) *Variable {
f.count.tmps++
name := fmt.Sprintf("t%d", f.count.tmps)
register := f.cpu.MustUseFree(f.cpu.General)
tmp := &Variable{
Name: name,
Value: root,
Alive: 1,
Register: register,
}
f.variables[name] = tmp
if config.Comments {
f.assembler.Comment(fmt.Sprintf("%s = %s (%s)", name, root, register))
}
return tmp
}
func (f *Function) useVariable(variable *Variable) {
variable.Alive--
if config.Verbose {
f.Logf("%s occupying %s was used (alive: %d)", variable.Name, variable.Register, variable.Alive)
}
if variable.Alive < 0 {
panic("incorrect number of variable use calls")
}
if variable.Alive == 0 {
if config.Verbose {
f.Logf("%s is no longer used, free register: %s", variable.Name, variable.Register)
if config.Comments {
f.assembler.Comment(fmt.Sprintf("%s died (%s)", variable.Name, variable.Register))
}
f.cpu.Free(variable.Register)
@ -102,9 +121,9 @@ func (f *Function) storeVariableInRegister(name string, value *expression.Expres
panic("no free registers")
}
err := f.ExpressionToRegister(value, reg)
err := f.EvaluateTo(value, reg)
f.addVariable(&Variable{
f.AddVariable(&Variable{
Name: name,
Register: reg,
Alive: uses,
@ -113,7 +132,7 @@ func (f *Function) storeVariableInRegister(name string, value *expression.Expres
return err
}
func countIdentifier(tokens token.List, name string) int {
func CountIdentifier(tokens token.List, name string) int {
count := 0
for _, t := range tokens {

View File

@ -1,4 +1,4 @@
package build
package core
import (
"fmt"

View File

@ -1,4 +1,4 @@
package build
package core
import (
"git.akyoto.dev/cli/q/src/build/ast"
@ -12,5 +12,5 @@ func (f *Function) CompileReturn(node *ast.Return) error {
return nil
}
return f.ExpressionToRegister(node.Value, f.cpu.Return[0])
return f.EvaluateTo(node.Value, f.cpu.Return[0])
}

123
src/build/core/Evaluate.go Normal file
View File

@ -0,0 +1,123 @@
package core
import (
"git.akyoto.dev/cli/q/src/build/asm"
"git.akyoto.dev/cli/q/src/build/ast"
"git.akyoto.dev/cli/q/src/build/cpu"
"git.akyoto.dev/cli/q/src/build/expression"
"git.akyoto.dev/cli/q/src/build/token"
)
// Evaluate evaluates the result of an expression and saves it into a temporary register.
func (f *Function) Evaluate(root *expression.Expression) (*Variable, error) {
if root.IsLeaf() {
return f.EvaluateLeaf(root)
}
if ast.IsFunctionCall(root) {
return f.EvaluateCall(root)
}
left := root.Children[0]
right := root.Children[1]
tmpLeft, err := f.Evaluate(left)
if err != nil {
return nil, err
}
tmpLeftExpr := expression.NewLeaf(token.Token{
Kind: token.Identifier,
Position: left.Token.Position,
Bytes: []byte(tmpLeft.Name),
})
tmpLeftExpr.Parent = root
root.Children[0].Parent = nil
root.Children[0] = tmpLeftExpr
tmpRight, err := f.Evaluate(right)
if err != nil {
return nil, err
}
tmpRightExpr := expression.NewLeaf(token.Token{
Kind: token.Identifier,
Position: left.Token.Position,
Bytes: []byte(tmpRight.Name),
})
tmpRightExpr.Parent = root
root.Children[1].Parent = nil
root.Children[1] = tmpRightExpr
tmp := f.addTemporary(root)
f.assembler.RegisterRegister(asm.MOVE, tmp.Register, tmpLeft.Register)
f.useVariable(tmpLeft)
err = f.opRegisterRegister(root.Token, tmp.Register, tmpRight.Register)
f.useVariable(tmpRight)
return tmp, err
}
func (f *Function) EvaluateCall(root *expression.Expression) (*Variable, error) {
funcName := root.Children[0].Token.Text()
parameters := root.Children[1:]
registers := f.cpu.Call[:len(parameters)]
isSyscall := funcName == "syscall"
if isSyscall {
registers = f.cpu.Syscall[:len(parameters)]
}
err := f.ExpressionsToRegisters(parameters, registers)
for _, register := range f.cpu.General {
if !f.cpu.IsFree(register) {
f.assembler.Register(asm.PUSH, register)
}
}
if isSyscall {
f.assembler.Syscall()
} else {
f.assembler.Call(funcName)
}
for i := len(f.cpu.General) - 1; i >= 0; i-- {
register := f.cpu.General[i]
if !f.cpu.IsFree(register) {
f.assembler.Register(asm.POP, register)
}
}
tmp := f.addTemporary(root)
f.assembler.RegisterRegister(asm.MOVE, tmp.Register, f.cpu.Return[0])
return tmp, err
}
func (f *Function) EvaluateLeaf(root *expression.Expression) (*Variable, error) {
tmp := f.addTemporary(root)
err := f.TokenToRegister(root.Token, tmp.Register)
return tmp, err
}
func (f *Function) EvaluateTo(root *expression.Expression, register cpu.Register) error {
tmp, err := f.Evaluate(root)
if err != nil {
return err
}
if register != tmp.Register {
f.assembler.RegisterRegister(asm.MOVE, register, tmp.Register)
}
f.useVariable(tmp)
return nil
}

View File

@ -1,22 +1,17 @@
package build
package core
import (
"strconv"
"git.akyoto.dev/cli/q/src/build/ast"
"git.akyoto.dev/cli/q/src/build/config"
"git.akyoto.dev/cli/q/src/build/cpu"
"git.akyoto.dev/cli/q/src/build/errors"
"git.akyoto.dev/cli/q/src/build/expression"
"git.akyoto.dev/cli/q/src/build/token"
"git.akyoto.dev/cli/q/src/errors"
)
// Execute executes an operation on a register with a value operand.
func (f *Function) Execute(operation token.Token, register cpu.Register, value *expression.Expression) error {
if config.Verbose {
f.Logf("execute: %s on register %s with value %s", operation.Text(), register, value)
}
if value.IsLeaf() {
return f.ExecuteLeaf(operation, register, value.Token)
}
@ -36,13 +31,13 @@ func (f *Function) Execute(operation token.Token, register cpu.Register, value *
f.cpu.Use(temporary)
defer f.cpu.Free(temporary)
err := f.ExpressionToRegister(value, temporary)
err := f.EvaluateTo(value, temporary)
if err != nil {
return err
}
return f.ExecuteRegisterRegister(operation, register, temporary)
return f.opRegisterRegister(operation, register, temporary)
}
// ExecuteLeaf performs an operation on a register with the given leaf operand.
@ -57,7 +52,7 @@ func (f *Function) ExecuteLeaf(operation token.Token, register cpu.Register, ope
}
defer f.useVariable(variable)
return f.ExecuteRegisterRegister(operation, register, variable.Register)
return f.opRegisterRegister(operation, register, variable.Register)
case token.Number:
value := operand.Text()
@ -67,7 +62,7 @@ func (f *Function) ExecuteLeaf(operation token.Token, register cpu.Register, ope
return err
}
return f.ExecuteRegisterNumber(operation, register, number)
return f.opRegisterNumber(operation, register, number)
}
return errors.New(errors.NotImplemented, f.File, operation.Position)

View File

@ -1,21 +1,46 @@
package build
package core
import (
"fmt"
"git.akyoto.dev/cli/q/src/build/arch/x64"
"git.akyoto.dev/cli/q/src/build/asm"
"git.akyoto.dev/cli/q/src/build/ast"
"git.akyoto.dev/cli/q/src/build/config"
"git.akyoto.dev/cli/q/src/build/cpu"
"git.akyoto.dev/cli/q/src/build/errors"
"git.akyoto.dev/cli/q/src/build/fs"
"git.akyoto.dev/cli/q/src/build/token"
"git.akyoto.dev/cli/q/src/errors"
)
// Function represents a function.
// Function represents the smallest unit of code.
type Function struct {
File *fs.File
Name string
File *fs.File
Body token.List
compiler
state
}
// NewFunction creates a new function.
func NewFunction(name string, file *fs.File, body token.List) *Function {
return &Function{
Name: name,
File: file,
Body: body,
state: state{
assembler: asm.Assembler{
Instructions: make([]asm.Instruction, 0, 32),
},
cpu: cpu.CPU{
Call: x64.CallRegisters,
General: x64.GeneralRegisters,
Syscall: x64.SyscallRegisters,
Return: x64.ReturnValueRegisters,
},
definitions: map[string]*Definition{},
variables: map[string]*Variable{},
finished: make(chan struct{}),
},
}
}
// Compile turns a function into machine code.
@ -41,18 +66,7 @@ func (f *Function) CompileTokens(tokens token.List) error {
// CompileAST compiles an abstract syntax tree.
func (f *Function) CompileAST(tree ast.AST) error {
for _, node := range tree {
if config.Verbose {
f.Logf("%T %s", node, node)
}
if config.Assembler {
f.debug = append(f.debug, debug{
position: len(f.assembler.Instructions),
source: node,
})
}
err := f.CompileNode(node)
err := f.CompileASTNode(node)
if err != nil {
return err
@ -62,14 +76,17 @@ func (f *Function) CompileAST(tree ast.AST) error {
return nil
}
// CompileNode compiles a node in the AST.
func (f *Function) CompileNode(node ast.Node) error {
// CompileASTNode compiles a node in the AST.
func (f *Function) CompileASTNode(node ast.Node) error {
switch node := node.(type) {
case *ast.Assign:
return f.CompileAssign(node)
case *ast.Call:
return f.CompileFunctionCall(node.Expression)
return f.CompileCall(node.Expression)
case *ast.Define:
return f.CompileVariableDefinition(node)
return f.CompileDefinition(node)
case *ast.Return:
return f.CompileReturn(node)

View File

@ -1,8 +1,12 @@
package build
package core
import (
"bufio"
"os"
"git.akyoto.dev/cli/q/src/build/arch/x64"
"git.akyoto.dev/cli/q/src/build/asm"
"git.akyoto.dev/cli/q/src/build/elf"
"git.akyoto.dev/cli/q/src/build/os/linux"
)
@ -13,8 +17,8 @@ type Result struct {
InstructionCount int
}
// Finalize generates the final machine code.
func (r *Result) Finalize() ([]byte, []byte) {
// finalize generates the final machine code.
func (r *Result) finalize() ([]byte, []byte) {
// This will be the entry point of the executable.
// The only job of the entry function is to call `main` and exit cleanly.
// The reason we call `main` instead of using `main` itself is to place
@ -30,7 +34,7 @@ func (r *Result) Finalize() ([]byte, []byte) {
// This will place the main function immediately after the entry point
// and also add everything the main function calls recursively.
r.EachFunction(r.Main, map[*Function]bool{}, func(f *Function) {
r.eachFunction(r.Main, map[*Function]bool{}, func(f *Function) {
final.Merge(f.assembler)
})
@ -38,9 +42,9 @@ func (r *Result) Finalize() ([]byte, []byte) {
return code, data
}
// EachFunction recursively finds all the calls to external functions.
// eachFunction recursively finds all the calls to external functions.
// It avoids calling the same function twice with the help of a hashmap.
func (r *Result) EachFunction(caller *Function, traversed map[*Function]bool, call func(*Function)) {
func (r *Result) eachFunction(caller *Function, traversed map[*Function]bool, call func(*Function)) {
call(caller)
traversed[caller] = true
@ -60,12 +64,40 @@ func (r *Result) EachFunction(caller *Function, traversed map[*Function]bool, ca
continue
}
r.EachFunction(callee, traversed, call)
r.eachFunction(callee, traversed, call)
}
}
// Write write the final executable to disk.
func (r *Result) Write(path string) error {
code, data := r.Finalize()
return Write(path, code, data)
// PrintInstructions prints out the generated instructions.
func (r *Result) PrintInstructions() {
r.eachFunction(r.Main, map[*Function]bool{}, func(f *Function) {
f.PrintInstructions()
})
}
// Write writes an executable file to disk.
func (r *Result) Write(path string) error {
code, data := r.finalize()
return write(path, code, data)
}
// write writes an executable file to disk.
func write(path string, code []byte, data []byte) error {
file, err := os.Create(path)
if err != nil {
return err
}
buffer := bufio.NewWriter(file)
executable := elf.New(code, data)
executable.Write(buffer)
buffer.Flush()
err = file.Close()
if err != nil {
return err
}
return os.Chmod(path, 0755)
}

View File

@ -1,6 +1,8 @@
package build
package core
import (
"fmt"
"git.akyoto.dev/cli/q/src/build/asm"
"git.akyoto.dev/cli/q/src/build/config"
"git.akyoto.dev/cli/q/src/build/cpu"
@ -25,18 +27,13 @@ func (f *Function) SaveRegister(register cpu.Register) {
return
}
newRegister, exists := f.cpu.FindFree(f.cpu.General)
newRegister := f.cpu.MustUseFree(f.cpu.General)
if !exists {
panic("no free registers")
}
if config.Verbose {
f.Logf("moving %s from %s to %s (alive: %d)", variable.Name, variable.Register, newRegister, variable.Alive)
if config.Comments {
f.assembler.Comment(fmt.Sprintf("save %s to %s", register, newRegister))
}
f.assembler.RegisterRegister(asm.MOVE, newRegister, register)
f.cpu.Free(register)
f.cpu.Use(newRegister)
variable.Register = newRegister
}

View File

@ -1,13 +1,12 @@
package build
package core
import (
"strconv"
"git.akyoto.dev/cli/q/src/build/asm"
"git.akyoto.dev/cli/q/src/build/config"
"git.akyoto.dev/cli/q/src/build/cpu"
"git.akyoto.dev/cli/q/src/build/errors"
"git.akyoto.dev/cli/q/src/build/token"
"git.akyoto.dev/cli/q/src/errors"
)
// TokenToRegister moves a token into a register.
@ -19,11 +18,7 @@ func (f *Function) TokenToRegister(t token.Token, register cpu.Register) error {
constant, exists := f.definitions[name]
if exists {
if config.Verbose {
f.Logf("constant %s = %s", constant.Name, constant.Value)
}
return f.ExpressionToRegister(constant.Value, register)
return f.EvaluateTo(constant.Value, register)
}
variable, exists := f.variables[name]

View File

@ -1,4 +1,4 @@
package build
package core
import (
"git.akyoto.dev/cli/q/src/build/cpu"
@ -7,6 +7,7 @@ import (
// Variable represents a named register.
type Variable struct {
Value *expression.Expression
Name string
Register cpu.Register
Alive int

View File

@ -1,14 +1,14 @@
package build
package core
import (
"git.akyoto.dev/cli/q/src/build/asm"
"git.akyoto.dev/cli/q/src/build/cpu"
"git.akyoto.dev/cli/q/src/build/errors"
"git.akyoto.dev/cli/q/src/build/token"
"git.akyoto.dev/cli/q/src/errors"
)
// ExecuteRegisterNumber performs an operation on a register and a number.
func (f *Function) ExecuteRegisterNumber(operation token.Token, register cpu.Register, number int) error {
// opRegisterNumber performs an operation on a register and a number.
func (f *Function) opRegisterNumber(operation token.Token, register cpu.Register, number int) error {
switch operation.Text() {
case "+", "+=":
f.assembler.RegisterNumber(asm.ADD, register, number)
@ -32,8 +32,8 @@ func (f *Function) ExecuteRegisterNumber(operation token.Token, register cpu.Reg
return nil
}
// ExecuteRegisterRegister performs an operation on two registers.
func (f *Function) ExecuteRegisterRegister(operation token.Token, destination cpu.Register, source cpu.Register) error {
// opRegisterRegister performs an operation on two registers.
func (f *Function) opRegisterRegister(operation token.Token, destination cpu.Register, source cpu.Register) error {
switch operation.Text() {
case "+", "+=":
f.assembler.RegisterRegister(asm.ADD, destination, source)

58
src/build/core/state.go Normal file
View File

@ -0,0 +1,58 @@
package core
import (
"fmt"
"git.akyoto.dev/cli/q/src/build/asm"
"git.akyoto.dev/cli/q/src/build/cpu"
"git.akyoto.dev/go/color/ansi"
)
// state is the data structure we embed in each function to preserve compilation state.
type state struct {
err error
definitions map[string]*Definition
variables map[string]*Variable
functions map[string]*Function
finished chan struct{}
assembler asm.Assembler
cpu cpu.CPU
count counter
sideEffects int
}
// counter stores how often a certain statement appeared so we can generate a unique label from it.
type counter struct {
loop int
tmps int
}
// PrintInstructions shows the assembly instructions.
func (s *state) PrintInstructions() {
ansi.Dim.Println("╭────────────────────────────────────────────────╮")
for _, x := range s.assembler.Instructions {
ansi.Dim.Print("│ ")
switch x.Mnemonic {
case asm.LABEL:
ansi.Yellow.Printf("%-46s", x.Data.String()+":")
case asm.COMMENT:
ansi.Dim.Printf("%-46s", x.Data.String())
default:
ansi.Green.Printf("%-8s", x.Mnemonic.String())
if x.Data != nil {
fmt.Printf("%-38s", x.Data.String())
} else {
fmt.Printf("%-38s", "")
}
}
ansi.Dim.Print(" │\n")
}
ansi.Dim.Println("╰────────────────────────────────────────────────╯")
}

View File

@ -30,3 +30,14 @@ func (c *CPU) FindFree(registers []Register) (Register, bool) {
return 0, false
}
func (c *CPU) MustUseFree(registers []Register) Register {
register, exists := c.FindFree(registers)
if !exists {
panic("no free registers")
}
c.Use(register)
return register
}

View File

@ -1,4 +1,4 @@
package build
package scanner
import (
"os"
@ -7,17 +7,16 @@ import (
"sync"
"git.akyoto.dev/cli/q/src/build/arch/x64"
"git.akyoto.dev/cli/q/src/build/asm"
"git.akyoto.dev/cli/q/src/build/cpu"
"git.akyoto.dev/cli/q/src/build/core"
"git.akyoto.dev/cli/q/src/build/errors"
"git.akyoto.dev/cli/q/src/build/expression"
"git.akyoto.dev/cli/q/src/build/fs"
"git.akyoto.dev/cli/q/src/build/token"
"git.akyoto.dev/cli/q/src/errors"
)
// scan scans the directory.
func scan(files []string) (<-chan *Function, <-chan error) {
functions := make(chan *Function)
// Scan scans the directory.
func Scan(files []string) (<-chan *core.Function, <-chan error) {
functions := make(chan *core.Function)
errors := make(chan error)
go func() {
@ -30,7 +29,7 @@ func scan(files []string) (<-chan *Function, <-chan error) {
}
// scanFiles scans the list of files without channel allocations.
func scanFiles(files []string, functions chan<- *Function, errors chan<- error) {
func scanFiles(files []string, functions chan<- *core.Function, errors chan<- error) {
wg := sync.WaitGroup{}
for _, file := range files {
@ -81,7 +80,7 @@ func scanFiles(files []string, functions chan<- *Function, errors chan<- error)
}
// scanFile scans a single file.
func scanFile(path string, functions chan<- *Function) error {
func scanFile(path string, functions chan<- *core.Function) error {
contents, err := os.ReadFile(path)
if err != nil {
@ -236,49 +235,34 @@ func scanFile(path string, functions chan<- *Function) error {
return errors.New(errors.ExpectedFunctionDefinition, file, tokens[i].Position)
}
function := &Function{
Name: tokens[nameStart].Text(),
File: file,
Body: tokens[bodyStart:i],
compiler: compiler{
assembler: asm.Assembler{
Instructions: make([]asm.Instruction, 0, 32),
},
cpu: cpu.CPU{
Call: x64.CallRegisters,
General: x64.GeneralRegisters,
Syscall: x64.SyscallRegisters,
Return: x64.ReturnValueRegisters,
},
definitions: map[string]*Definition{},
variables: map[string]*Variable{},
finished: make(chan struct{}),
},
}
name := tokens[nameStart].Text()
body := tokens[bodyStart:i]
function := core.NewFunction(name, file, body)
parameters := tokens[paramsStart:paramsEnd]
count := 0
err := expression.EachParameter(parameters, func(tokens token.List) error {
if len(tokens) == 1 {
if len(tokens) != 1 {
return errors.New(errors.NotImplemented, file, tokens[0].Position)
}
name := tokens[0].Text()
register := x64.CallRegisters[len(function.variables)]
uses := countIdentifier(function.Body, name)
register := x64.CallRegisters[count]
uses := core.CountIdentifier(function.Body, name)
if uses == 0 {
return errors.New(&errors.UnusedVariable{Name: name}, file, tokens[0].Position)
}
variable := &Variable{
variable := &core.Variable{
Name: name,
Register: register,
Alive: uses,
}
function.addVariable(variable)
function.AddVariable(variable)
count++
return nil
}
return errors.New(errors.NotImplemented, file, tokens[0].Position)
})
if err != nil {

View File

@ -9,22 +9,21 @@ import (
"git.akyoto.dev/cli/q/src/build/config"
)
// Build builds an executable.
// Build parses the arguments and creates a build.
func Build(args []string) int {
b := build.New()
dry := false
for i := 0; i < len(args); i++ {
switch args[i] {
case "--dry":
dry = true
case "--assembler", "-a":
case "-a", "--assembler":
config.Assembler = true
case "--verbose", "-v":
config.Verbose = true
case "-c", "--comments":
config.Comments = true
case "-d", "--dry":
config.Dry = true
case "-v", "--verbose":
config.Assembler = true
config.Comments = true
default:
if strings.HasPrefix(args[i], "-") {
fmt.Printf("Unknown parameter: %s\n", args[i])
@ -39,6 +38,11 @@ func Build(args []string) int {
b.Files = append(b.Files, ".")
}
return run(b)
}
// run starts the build by running the compiler and then writing the result to disk.
func run(b *build.Build) int {
result, err := b.Run()
if err != nil {
@ -47,12 +51,10 @@ func Build(args []string) int {
}
if config.Assembler {
result.EachFunction(result.Main, map[*build.Function]bool{}, func(f *build.Function) {
f.PrintInstructions()
})
result.PrintInstructions()
}
if dry {
if config.Dry {
return 0
}

View File

@ -18,6 +18,7 @@ func Help(w io.Writer, code int) int {
build options:
--assembler, -a Show assembler instructions.
--verbose, -v Show verbose output.`)
--comments, -c Show assembler comments.
--verbose, -v Show everything.`)
return code
}

View File

@ -1,4 +1,4 @@
package main_test
package tests_test
import (
"testing"
@ -8,7 +8,7 @@ import (
)
func BenchmarkEmpty(b *testing.B) {
compiler := build.New("tests/benchmarks/empty.q")
compiler := build.New("benchmarks/empty.q")
for i := 0; i < b.N; i++ {
_, err := compiler.Run()
@ -17,7 +17,7 @@ func BenchmarkEmpty(b *testing.B) {
}
func BenchmarkExpressions(b *testing.B) {
compiler := build.New("tests/benchmarks/expressions.q")
compiler := build.New("benchmarks/expressions.q")
for i := 0; i < b.N; i++ {
_, err := compiler.Run()

View File

@ -1,4 +1,4 @@
package main_test
package tests_test
import (
"path/filepath"
@ -6,7 +6,7 @@ import (
"testing"
"git.akyoto.dev/cli/q/src/build"
"git.akyoto.dev/cli/q/src/errors"
"git.akyoto.dev/cli/q/src/build/errors"
"git.akyoto.dev/go/assert"
)
@ -41,7 +41,7 @@ func TestErrors(t *testing.T) {
name := strings.TrimSuffix(test.File, ".q")
t.Run(name, func(t *testing.T) {
b := build.New(filepath.Join("tests", "errors", test.File))
b := build.New(filepath.Join("errors", test.File))
_, err := b.Run()
assert.NotNil(t, err)
assert.Contains(t, err.Error(), test.ExpectedError.Error())

23
tests/examples_test.go Normal file
View File

@ -0,0 +1,23 @@
package tests_test
import (
"path/filepath"
"testing"
)
func TestExamples(t *testing.T) {
var tests = []struct {
Name string
ExpectedOutput string
ExpectedExitCode int
}{
{"hello", "", 9},
{"write", "ELF", 0},
}
for _, test := range tests {
t.Run(test.Name, func(t *testing.T) {
run(t, filepath.Join("..", "examples", test.Name), test.ExpectedOutput, test.ExpectedExitCode)
})
}
}

View File

@ -0,0 +1,7 @@
main() {
syscall(60, f(1) + f(2) + f(3))
}
f(x) {
return x + 1
}

View File

@ -1,38 +1,35 @@
package main_test
package tests_test
import (
"os"
"os/exec"
"path/filepath"
"testing"
"git.akyoto.dev/cli/q/src/build"
"git.akyoto.dev/go/assert"
)
func TestExamples(t *testing.T) {
var examples = []struct {
func TestPrograms(t *testing.T) {
var tests = []struct {
Name string
ExpectedOutput string
ExpectedExitCode int
}{
{"hello", "", 25},
{"write", "ELF", 0},
{"successive-calls.q", "", 9},
}
for _, example := range examples {
example := example
t.Run(example.Name, func(t *testing.T) {
runExample(t, example.Name, example.ExpectedOutput, example.ExpectedExitCode)
for _, test := range tests {
t.Run(test.Name, func(t *testing.T) {
run(t, filepath.Join("programs", test.Name), test.ExpectedOutput, test.ExpectedExitCode)
})
}
}
// runExample builds and runs the example to check if the output matches the expected output.
func runExample(t *testing.T, name string, expectedOutput string, expectedExitCode int) {
b := build.New("examples/" + name)
// run builds and runs the file to check if the output matches the expected output.
func run(t *testing.T, name string, expectedOutput string, expectedExitCode int) {
b := build.New(name)
assert.True(t, len(b.Executable()) > 0)
defer os.Remove(b.Executable())
t.Run("Compile", func(t *testing.T) {
result, err := b.Run()