Refactored code structure

This commit is contained in:
2024-07-03 11:39:24 +02:00
parent ed03f6a802
commit feebfe65bb
54 changed files with 583 additions and 450 deletions

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
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 {
name := tokens[0].Text()
register := x64.CallRegisters[len(function.variables)]
uses := countIdentifier(function.Body, name)
if uses == 0 {
return errors.New(&errors.UnusedVariable{Name: name}, file, tokens[0].Position)
}
variable := &Variable{
Name: name,
Register: register,
Alive: uses,
}
function.addVariable(variable)
return nil
if len(tokens) != 1 {
return errors.New(errors.NotImplemented, file, tokens[0].Position)
}
return errors.New(errors.NotImplemented, file, tokens[0].Position)
name := tokens[0].Text()
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 := &core.Variable{
Name: name,
Register: register,
Alive: uses,
}
function.AddVariable(variable)
count++
return nil
})
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
}