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.
|
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
|
||||||
|
@ -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
|
||||||
}
|
}
|
@ -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) {
|
||||||
|
@ -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 (
|
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"))
|
||||||
}
|
}
|
||||||
|
@ -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 (
|
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}
|
||||||
)
|
)
|
||||||
|
@ -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
|
||||||
|
@ -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{
|
||||||
|
@ -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"
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
@ -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 (
|
var (
|
||||||
Assembler = false
|
Assembler = false
|
||||||
Verbose = false
|
Comments = false
|
||||||
|
Dry = false
|
||||||
)
|
)
|
||||||
|
@ -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()
|
|
||||||
}
|
|
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 (
|
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
|
@ -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 {
|
@ -1,4 +1,4 @@
|
|||||||
package build
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
@ -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
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 (
|
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)
|
@ -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)
|
@ -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)
|
||||||
}
|
}
|
@ -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
|
||||||
}
|
}
|
@ -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]
|
@ -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
|
@ -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
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
|
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 (
|
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 {
|
||||||
name := tokens[0].Text()
|
return errors.New(errors.NotImplemented, file, tokens[0].Position)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
if err != nil {
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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()
|
@ -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
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 (
|
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()
|
Loading…
Reference in New Issue
Block a user