Refactored code structure
This commit is contained in:
parent
ed03f6a802
commit
feebfe65bb
18
README.md
18
README.md
@ -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
|
||||
|
@ -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
|
||||
}
|
@ -5,7 +5,7 @@ main() {
|
||||
}
|
||||
|
||||
print(address, length) {
|
||||
write(length-2, address, length)
|
||||
write(1, address, length)
|
||||
}
|
||||
|
||||
write(fd, address, length) {
|
||||
|
@ -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])
|
||||
}
|
@ -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"))
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
@ -1 +0,0 @@
|
||||
package build
|
@ -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)
|
||||
}
|
@ -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}
|
||||
)
|
||||
|
@ -63,6 +63,9 @@ func (a Assembler) Finalize() ([]byte, []byte) {
|
||||
},
|
||||
})
|
||||
|
||||
case COMMENT:
|
||||
continue
|
||||
|
||||
case JUMP:
|
||||
code = x64.Jump8(code, 0x00)
|
||||
size := 1
|
||||
|
@ -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{
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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()
|
@ -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
|
||||
}
|
@ -9,5 +9,6 @@ const (
|
||||
|
||||
var (
|
||||
Assembler = false
|
||||
Verbose = false
|
||||
Comments = false
|
||||
Dry = false
|
||||
)
|
||||
|
@ -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()
|
||||
}
|
19
src/build/core/CompileAllFunctions.go
Normal file
19
src/build/core/CompileAllFunctions.go
Normal 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()
|
||||
}
|
19
src/build/core/CompileAssign.go
Normal file
19
src/build/core/CompileAssign.go
Normal 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)
|
||||
}
|
@ -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
|
@ -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 {
|
@ -1,4 +1,4 @@
|
||||
package build
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
@ -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
123
src/build/core/Evaluate.go
Normal 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
|
||||
}
|
@ -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)
|
@ -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)
|
@ -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)
|
||||
}
|
@ -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
|
||||
}
|
@ -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]
|
@ -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
|
@ -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
58
src/build/core/state.go
Normal 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("╰────────────────────────────────────────────────╯")
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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()
|
@ -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
23
tests/examples_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
7
tests/programs/successive-calls.q
Normal file
7
tests/programs/successive-calls.q
Normal file
@ -0,0 +1,7 @@
|
||||
main() {
|
||||
syscall(60, f(1) + f(2) + f(3))
|
||||
}
|
||||
|
||||
f(x) {
|
||||
return x + 1
|
||||
}
|
@ -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()
|
Loading…
Reference in New Issue
Block a user