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. 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. 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. 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`. 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 instructions as the individual lines in your source code, but instructions can also span over multiple lines. 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 ## Tests
```shell ```shell
go test -coverpkg=./... go test ./... -v
``` ```
## Benchmarks ## Benchmarks
```shell ```shell
go test -bench=. -benchmem go test ./tests -bench=. -benchmem
``` ```
## License ## License

View File

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

View File

@ -5,7 +5,7 @@ main() {
} }
print(address, length) { print(address, length) {
write(length-2, address, length) write(1, address, length)
} }
write(fd, 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 ( import (
"path/filepath" "path/filepath"
"strings" "strings"
"git.akyoto.dev/cli/q/src/build/core"
"git.akyoto.dev/cli/q/src/build/scanner"
) )
// Build describes a compiler build. // Build describes a compiler build.
@ -18,18 +21,28 @@ func New(files ...string) *Build {
} }
// Run parses the input files and generates an executable file. // Run parses the input files and generates an executable file.
func (build *Build) Run() (Result, error) { func (build *Build) Run() (core.Result, error) {
functions, errors := scan(build.Files) functions, errors := scanner.Scan(build.Files)
return compile(functions, errors) return core.Compile(functions, errors)
} }
// Executable returns the path to the executable. // Executable returns the path to the executable.
func (build *Build) Executable() string { func (build *Build) Executable() string {
directory, _ := filepath.Abs(build.Files[0]) path, _ := filepath.Abs(build.Files[0])
if strings.HasSuffix(directory, ".q") { if strings.HasSuffix(path, ".q") {
directory = filepath.Dir(directory) 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 ( var (
CallRegisters = SyscallRegisters 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} SyscallRegisters = []cpu.Register{RAX, RDI, RSI, RDX, R10, R8, R9}
ReturnValueRegisters = []cpu.Register{RAX, RCX, R11} ReturnValueRegisters = []cpu.Register{RAX, RCX, R11}
) )

View File

@ -63,6 +63,9 @@ func (a Assembler) Finalize() ([]byte, []byte) {
}, },
}) })
case COMMENT:
continue
case JUMP: case JUMP:
code = x64.Jump8(code, 0x00) code = x64.Jump8(code, 0x00)
size := 1 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. // Call calls a function whose position is identified by a label.
func (a *Assembler) Call(name string) { func (a *Assembler) Call(name string) {
a.Instructions = append(a.Instructions, Instruction{ a.Instructions = append(a.Instructions, Instruction{

View File

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

View File

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

View File

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

View File

@ -8,17 +8,28 @@ import (
"git.akyoto.dev/go/assert" "git.akyoto.dev/go/assert"
) )
func TestBuild(t *testing.T) { func TestBuildDirectory(t *testing.T) {
b := build.New("../../examples/hello") b := build.New("../../examples/hello")
_, err := b.Run() _, err := b.Run()
assert.Nil(t, err) 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") b := build.New("../../examples/hello")
assert.Equal(t, filepath.Base(b.Executable()), "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) { func TestNonExisting(t *testing.T) {
b := build.New("does-not-exist") b := build.New("does-not-exist")
_, err := b.Run() _, 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 ( var (
Assembler = false Assembler = false
Verbose = false Comments = false
Dry = false
) )

View File

@ -1,13 +1,11 @@
package build package core
import ( import (
"sync" "git.akyoto.dev/cli/q/src/build/errors"
"git.akyoto.dev/cli/q/src/errors"
) )
// compile waits for the scan to finish and compiles all functions. // Compile waits for the scan to finish and compiles all functions.
func compile(functions <-chan *Function, errs <-chan error) (Result, error) { func Compile(functions <-chan *Function, errs <-chan error) (Result, error) {
result := Result{} result := Result{}
allFunctions := map[string]*Function{} 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 { for _, function := range allFunctions {
if function.err != nil { if function.err != nil {
return result, function.err return result, function.err
@ -42,6 +42,7 @@ func compile(functions <-chan *Function, errs <-chan error) (Result, error) {
result.InstructionCount += len(function.assembler.Instructions) result.InstructionCount += len(function.assembler.Instructions)
} }
// Check for existence of `main`
main, exists := allFunctions["main"] main, exists := allFunctions["main"]
if !exists { if !exists {
@ -52,19 +53,3 @@ func compile(functions <-chan *Function, errs <-chan error) (Result, error) {
result.Functions = allFunctions result.Functions = allFunctions
return result, nil 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 ( import (
"git.akyoto.dev/cli/q/src/build/config"
"git.akyoto.dev/cli/q/src/build/cpu" "git.akyoto.dev/cli/q/src/build/cpu"
"git.akyoto.dev/cli/q/src/build/expression" "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. // 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. // 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. // After the function call, they must be restored in reverse order.
func (f *Function) CompileFunctionCall(expr *expression.Expression) error { func (f *Function) CompileCall(expr *expression.Expression) error {
funcName := expr.Children[0].Token.Text() _, err := f.EvaluateCall(expr)
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
return err return err
} }
@ -49,15 +26,9 @@ func (f *Function) CompileSyscall(expr *expression.Expression) error {
// ExpressionsToRegisters moves multiple expressions into the specified registers. // ExpressionsToRegisters moves multiple expressions into the specified registers.
func (f *Function) ExpressionsToRegisters(expressions []*expression.Expression, registers []cpu.Register) error { func (f *Function) ExpressionsToRegisters(expressions []*expression.Expression, registers []cpu.Register) error {
for _, register := range registers { for i := len(registers) - 1; i >= 0; i-- {
f.SaveRegister(register) f.SaveRegister(registers[i])
} err := f.EvaluateTo(expressions[i], registers[i])
for i := len(expressions) - 1; i >= 0; i-- {
expression := expressions[i]
register := registers[i]
err := f.ExpressionToRegister(expression, register)
if err != nil { if err != nil {
return err return err

View File

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

View File

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

View File

@ -1,4 +1,4 @@
package build package core
import ( import (
"git.akyoto.dev/cli/q/src/build/ast" "git.akyoto.dev/cli/q/src/build/ast"
@ -12,5 +12,5 @@ func (f *Function) CompileReturn(node *ast.Return) error {
return nil 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 ( import (
"strconv" "strconv"
"git.akyoto.dev/cli/q/src/build/ast" "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/cpu"
"git.akyoto.dev/cli/q/src/build/errors"
"git.akyoto.dev/cli/q/src/build/expression" "git.akyoto.dev/cli/q/src/build/expression"
"git.akyoto.dev/cli/q/src/build/token" "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. // 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 { 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() { if value.IsLeaf() {
return f.ExecuteLeaf(operation, register, value.Token) 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) f.cpu.Use(temporary)
defer f.cpu.Free(temporary) defer f.cpu.Free(temporary)
err := f.ExpressionToRegister(value, temporary) err := f.EvaluateTo(value, temporary)
if err != nil { if err != nil {
return err 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. // 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) defer f.useVariable(variable)
return f.ExecuteRegisterRegister(operation, register, variable.Register) return f.opRegisterRegister(operation, register, variable.Register)
case token.Number: case token.Number:
value := operand.Text() value := operand.Text()
@ -67,7 +62,7 @@ func (f *Function) ExecuteLeaf(operation token.Token, register cpu.Register, ope
return err return err
} }
return f.ExecuteRegisterNumber(operation, register, number) return f.opRegisterNumber(operation, register, number)
} }
return errors.New(errors.NotImplemented, f.File, operation.Position) return errors.New(errors.NotImplemented, f.File, operation.Position)

View File

@ -1,21 +1,46 @@
package build package core
import ( import (
"fmt" "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/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/fs"
"git.akyoto.dev/cli/q/src/build/token" "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 { type Function struct {
File *fs.File
Name string Name string
File *fs.File
Body token.List 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. // 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. // CompileAST compiles an abstract syntax tree.
func (f *Function) CompileAST(tree ast.AST) error { func (f *Function) CompileAST(tree ast.AST) error {
for _, node := range tree { for _, node := range tree {
if config.Verbose { err := f.CompileASTNode(node)
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)
if err != nil { if err != nil {
return err return err
@ -62,14 +76,17 @@ func (f *Function) CompileAST(tree ast.AST) error {
return nil return nil
} }
// CompileNode compiles a node in the AST. // CompileASTNode compiles a node in the AST.
func (f *Function) CompileNode(node ast.Node) error { func (f *Function) CompileASTNode(node ast.Node) error {
switch node := node.(type) { switch node := node.(type) {
case *ast.Assign:
return f.CompileAssign(node)
case *ast.Call: case *ast.Call:
return f.CompileFunctionCall(node.Expression) return f.CompileCall(node.Expression)
case *ast.Define: case *ast.Define:
return f.CompileVariableDefinition(node) return f.CompileDefinition(node)
case *ast.Return: case *ast.Return:
return f.CompileReturn(node) return f.CompileReturn(node)

View File

@ -1,8 +1,12 @@
package build package core
import ( import (
"bufio"
"os"
"git.akyoto.dev/cli/q/src/build/arch/x64" "git.akyoto.dev/cli/q/src/build/arch/x64"
"git.akyoto.dev/cli/q/src/build/asm" "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" "git.akyoto.dev/cli/q/src/build/os/linux"
) )
@ -13,8 +17,8 @@ type Result struct {
InstructionCount int InstructionCount int
} }
// Finalize generates the final machine code. // finalize generates the final machine code.
func (r *Result) Finalize() ([]byte, []byte) { func (r *Result) finalize() ([]byte, []byte) {
// This will be the entry point of the executable. // This will be the entry point of the executable.
// The only job of the entry function is to call `main` and exit cleanly. // 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 // 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 // This will place the main function immediately after the entry point
// and also add everything the main function calls recursively. // 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) final.Merge(f.assembler)
}) })
@ -38,9 +42,9 @@ func (r *Result) Finalize() ([]byte, []byte) {
return code, data 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. // 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) call(caller)
traversed[caller] = true traversed[caller] = true
@ -60,12 +64,40 @@ func (r *Result) EachFunction(caller *Function, traversed map[*Function]bool, ca
continue continue
} }
r.EachFunction(callee, traversed, call) r.eachFunction(callee, traversed, call)
} }
} }
// Write write the final executable to disk. // PrintInstructions prints out the generated instructions.
func (r *Result) Write(path string) error { func (r *Result) PrintInstructions() {
code, data := r.Finalize() r.eachFunction(r.Main, map[*Function]bool{}, func(f *Function) {
return Write(path, code, data) 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 ( import (
"fmt"
"git.akyoto.dev/cli/q/src/build/asm" "git.akyoto.dev/cli/q/src/build/asm"
"git.akyoto.dev/cli/q/src/build/config" "git.akyoto.dev/cli/q/src/build/config"
"git.akyoto.dev/cli/q/src/build/cpu" "git.akyoto.dev/cli/q/src/build/cpu"
@ -25,18 +27,13 @@ func (f *Function) SaveRegister(register cpu.Register) {
return return
} }
newRegister, exists := f.cpu.FindFree(f.cpu.General) newRegister := f.cpu.MustUseFree(f.cpu.General)
if !exists { if config.Comments {
panic("no free registers") f.assembler.Comment(fmt.Sprintf("save %s to %s", register, newRegister))
}
if config.Verbose {
f.Logf("moving %s from %s to %s (alive: %d)", variable.Name, variable.Register, newRegister, variable.Alive)
} }
f.assembler.RegisterRegister(asm.MOVE, newRegister, register) f.assembler.RegisterRegister(asm.MOVE, newRegister, register)
f.cpu.Free(register) f.cpu.Free(register)
f.cpu.Use(newRegister)
variable.Register = newRegister variable.Register = newRegister
} }

View File

@ -1,13 +1,12 @@
package build package core
import ( import (
"strconv" "strconv"
"git.akyoto.dev/cli/q/src/build/asm" "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/cpu"
"git.akyoto.dev/cli/q/src/build/errors"
"git.akyoto.dev/cli/q/src/build/token" "git.akyoto.dev/cli/q/src/build/token"
"git.akyoto.dev/cli/q/src/errors"
) )
// TokenToRegister moves a token into a register. // 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] constant, exists := f.definitions[name]
if exists { if exists {
if config.Verbose { return f.EvaluateTo(constant.Value, register)
f.Logf("constant %s = %s", constant.Name, constant.Value)
}
return f.ExpressionToRegister(constant.Value, register)
} }
variable, exists := f.variables[name] variable, exists := f.variables[name]

View File

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

View File

@ -1,14 +1,14 @@
package build package core
import ( import (
"git.akyoto.dev/cli/q/src/build/asm" "git.akyoto.dev/cli/q/src/build/asm"
"git.akyoto.dev/cli/q/src/build/cpu" "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/build/token"
"git.akyoto.dev/cli/q/src/errors"
) )
// ExecuteRegisterNumber performs an operation on a register and a number. // opRegisterNumber performs an operation on a register and a number.
func (f *Function) ExecuteRegisterNumber(operation token.Token, register cpu.Register, number int) error { func (f *Function) opRegisterNumber(operation token.Token, register cpu.Register, number int) error {
switch operation.Text() { switch operation.Text() {
case "+", "+=": case "+", "+=":
f.assembler.RegisterNumber(asm.ADD, register, number) f.assembler.RegisterNumber(asm.ADD, register, number)
@ -32,8 +32,8 @@ func (f *Function) ExecuteRegisterNumber(operation token.Token, register cpu.Reg
return nil return nil
} }
// ExecuteRegisterRegister performs an operation on two registers. // opRegisterRegister performs an operation on two registers.
func (f *Function) ExecuteRegisterRegister(operation token.Token, destination cpu.Register, source cpu.Register) error { func (f *Function) opRegisterRegister(operation token.Token, destination cpu.Register, source cpu.Register) error {
switch operation.Text() { switch operation.Text() {
case "+", "+=": case "+", "+=":
f.assembler.RegisterRegister(asm.ADD, destination, source) 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 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 ( import (
"os" "os"
@ -7,17 +7,16 @@ import (
"sync" "sync"
"git.akyoto.dev/cli/q/src/build/arch/x64" "git.akyoto.dev/cli/q/src/build/arch/x64"
"git.akyoto.dev/cli/q/src/build/asm" "git.akyoto.dev/cli/q/src/build/core"
"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/expression"
"git.akyoto.dev/cli/q/src/build/fs" "git.akyoto.dev/cli/q/src/build/fs"
"git.akyoto.dev/cli/q/src/build/token" "git.akyoto.dev/cli/q/src/build/token"
"git.akyoto.dev/cli/q/src/errors"
) )
// scan scans the directory. // Scan scans the directory.
func scan(files []string) (<-chan *Function, <-chan error) { func Scan(files []string) (<-chan *core.Function, <-chan error) {
functions := make(chan *Function) functions := make(chan *core.Function)
errors := make(chan error) errors := make(chan error)
go func() { go func() {
@ -30,7 +29,7 @@ func scan(files []string) (<-chan *Function, <-chan error) {
} }
// scanFiles scans the list of files without channel allocations. // 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{} wg := sync.WaitGroup{}
for _, file := range files { for _, file := range files {
@ -81,7 +80,7 @@ func scanFiles(files []string, functions chan<- *Function, errors chan<- error)
} }
// scanFile scans a single file. // 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) contents, err := os.ReadFile(path)
if err != nil { if err != nil {
@ -236,49 +235,34 @@ func scanFile(path string, functions chan<- *Function) error {
return errors.New(errors.ExpectedFunctionDefinition, file, tokens[i].Position) return errors.New(errors.ExpectedFunctionDefinition, file, tokens[i].Position)
} }
function := &Function{ name := tokens[nameStart].Text()
Name: tokens[nameStart].Text(), body := tokens[bodyStart:i]
File: file, function := core.NewFunction(name, file, body)
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{}),
},
}
parameters := tokens[paramsStart:paramsEnd] parameters := tokens[paramsStart:paramsEnd]
count := 0
err := expression.EachParameter(parameters, func(tokens token.List) error { 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() name := tokens[0].Text()
register := x64.CallRegisters[len(function.variables)] register := x64.CallRegisters[count]
uses := countIdentifier(function.Body, name) uses := core.CountIdentifier(function.Body, name)
if uses == 0 { if uses == 0 {
return errors.New(&errors.UnusedVariable{Name: name}, file, tokens[0].Position) return errors.New(&errors.UnusedVariable{Name: name}, file, tokens[0].Position)
} }
variable := &Variable{ variable := &core.Variable{
Name: name, Name: name,
Register: register, Register: register,
Alive: uses, Alive: uses,
} }
function.addVariable(variable) function.AddVariable(variable)
count++
return nil return nil
}
return errors.New(errors.NotImplemented, file, tokens[0].Position)
}) })
if err != nil { if err != nil {

View File

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

View File

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

View File

@ -1,4 +1,4 @@
package main_test package tests_test
import ( import (
"testing" "testing"
@ -8,7 +8,7 @@ import (
) )
func BenchmarkEmpty(b *testing.B) { 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++ { for i := 0; i < b.N; i++ {
_, err := compiler.Run() _, err := compiler.Run()
@ -17,7 +17,7 @@ func BenchmarkEmpty(b *testing.B) {
} }
func BenchmarkExpressions(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++ { for i := 0; i < b.N; i++ {
_, err := compiler.Run() _, err := compiler.Run()

View File

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