Separated compiler into its own package

This commit is contained in:
Eduard Urbach 2024-07-18 10:08:38 +02:00
parent c19ad24428
commit 724794b4aa
Signed by: akyoto
GPG Key ID: C874F672B1AF20C0
14 changed files with 199 additions and 167 deletions

View File

@ -4,7 +4,7 @@ import (
"path/filepath"
"strings"
"git.akyoto.dev/cli/q/src/build/core"
"git.akyoto.dev/cli/q/src/build/compiler"
"git.akyoto.dev/cli/q/src/build/scanner"
)
@ -21,9 +21,9 @@ func New(files ...string) *Build {
}
// Run parses the input files and generates an executable file.
func (build *Build) Run() (core.Result, error) {
func (build *Build) Run() (compiler.Result, error) {
functions, errors := scanner.Scan(build.Files)
return core.Compile(functions, errors)
return compiler.Compile(functions, errors)
}
// Executable returns the path to the executable.

View File

@ -0,0 +1,75 @@
package compiler
import (
"sync"
"git.akyoto.dev/cli/q/src/build/core"
"git.akyoto.dev/cli/q/src/build/errors"
)
// Compile waits for the scan to finish and compiles all functions.
func Compile(functions <-chan *core.Function, errs <-chan error) (Result, error) {
result := Result{}
all := map[string]*core.Function{}
for functions != nil || errs != nil {
select {
case err, ok := <-errs:
if !ok {
errs = nil
continue
}
return result, err
case function, ok := <-functions:
if !ok {
functions = nil
continue
}
function.Functions = all
all[function.Name] = function
}
}
// Start parallel compilation
CompileFunctions(all)
// Report errors if any occurred
for _, function := range all {
if function.Err != nil {
return result, function.Err
}
result.InstructionCount += len(function.Assembler.Instructions)
result.DataCount += len(function.Assembler.Data)
}
// Check for existence of `main`
main, exists := all["main"]
if !exists {
return result, errors.MissingMainFunction
}
result.Main = main
result.Functions = all
return result, nil
}
// CompileFunctions starts a goroutine for each function compilation and waits for completion.
func CompileFunctions(functions map[string]*core.Function) {
wg := sync.WaitGroup{}
for _, function := range functions {
wg.Add(1)
go func() {
defer wg.Done()
function.Compile()
}()
}
wg.Wait()
}

View File

@ -1,4 +1,4 @@
package core
package compiler
import (
"bufio"
@ -6,15 +6,17 @@ import (
"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/elf"
"git.akyoto.dev/cli/q/src/build/os/linux"
)
// Result contains all the compiled functions in a build.
type Result struct {
Main *Function
Functions map[string]*Function
Main *core.Function
Functions map[string]*core.Function
InstructionCount int
DataCount int
}
// finalize generates the final machine code.
@ -25,7 +27,7 @@ func (r *Result) finalize() ([]byte, []byte) {
// a return address on the stack, which allows return statements in `main`.
final := asm.Assembler{
Instructions: make([]asm.Instruction, 0, r.InstructionCount+4),
Data: map[string][]byte{},
Data: make(map[string][]byte, r.DataCount),
}
final.Call("main")
@ -35,8 +37,8 @@ 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) {
final.Merge(f.assembler)
r.eachFunction(r.Main, map[*core.Function]bool{}, func(f *core.Function) {
final.Merge(f.Assembler)
})
code, data := final.Finalize()
@ -45,11 +47,11 @@ func (r *Result) finalize() ([]byte, []byte) {
// 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 *core.Function, traversed map[*core.Function]bool, call func(*core.Function)) {
call(caller)
traversed[caller] = true
for _, x := range caller.assembler.Instructions {
for _, x := range caller.Assembler.Instructions {
if x.Mnemonic != asm.CALL {
continue
}
@ -71,7 +73,7 @@ func (r *Result) eachFunction(caller *Function, traversed map[*Function]bool, ca
// PrintInstructions prints out the generated instructions.
func (r *Result) PrintInstructions() {
r.eachFunction(r.Main, map[*Function]bool{}, func(f *Function) {
r.eachFunction(r.Main, map[*core.Function]bool{}, func(f *core.Function) {
f.PrintInstructions()
})
}

View File

@ -1,55 +1,66 @@
package core
import (
"git.akyoto.dev/cli/q/src/build/ast"
"git.akyoto.dev/cli/q/src/build/errors"
"git.akyoto.dev/cli/q/src/build/token"
)
// 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{}
for functions != nil || errs != nil {
select {
case err, ok := <-errs:
if !ok {
errs = nil
continue
}
return result, err
case function, ok := <-functions:
if !ok {
functions = nil
continue
}
function.functions = allFunctions
allFunctions[function.Name] = function
}
}
// Start parallel compilation
CompileAllFunctions(allFunctions)
// Report errors if any occurred
for _, function := range allFunctions {
if function.err != nil {
return result, function.err
}
result.InstructionCount += len(function.assembler.Instructions)
}
// Check for existence of `main`
main, exists := allFunctions["main"]
if !exists {
return result, errors.MissingMainFunction
}
result.Main = main
result.Functions = allFunctions
return result, nil
// Compile turns a function into machine code.
func (f *Function) Compile() {
defer close(f.finished)
f.AddLabel(f.Name)
f.Err = f.CompileTokens(f.Body)
f.Return()
}
// CompileTokens compiles a token list.
func (f *Function) CompileTokens(tokens token.List) error {
body, err := ast.Parse(tokens)
if err != nil {
err.(*errors.Error).File = f.File
return err
}
return f.CompileAST(body)
}
// CompileAST compiles an abstract syntax tree.
func (f *Function) CompileAST(tree ast.AST) error {
for _, node := range tree {
err := f.CompileASTNode(node)
if err != nil {
return err
}
}
return nil
}
// 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.CompileCall(node.Expression)
case *ast.Define:
return f.CompileDefinition(node)
case *ast.Return:
return f.CompileReturn(node)
case *ast.If:
return f.CompileIf(node)
case *ast.Loop:
return f.CompileLoop(node)
default:
panic("unknown AST type")
}
}

View File

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

View File

@ -15,7 +15,7 @@ func (f *Function) CompileCall(root *expression.Expression) error {
isSyscall := funcName == "syscall"
if !isSyscall {
_, exists := f.functions[funcName]
_, exists := f.Functions[funcName]
if !exists {
return errors.New(&errors.UnknownFunction{Name: funcName}, f.File, root.Children[0].Token.Position)
@ -52,6 +52,10 @@ func (f *Function) CompileCall(root *expression.Expression) error {
}
for _, register := range registers {
if register == f.cpu.Output[0] && root.Parent != nil {
continue
}
f.Scope().Free(register)
}

View File

@ -63,6 +63,10 @@ func (f *Function) useVariable(variable *Variable) {
continue
}
if config.Comments {
f.Comment("%s (%s) used in scope %d", local.Name, local.Register, i)
}
local.Alive--
if local.Alive < 0 {
@ -71,7 +75,7 @@ func (f *Function) useVariable(variable *Variable) {
if local.Alive == 0 {
if config.Comments {
f.Comment("%s died (%s) in scope %d", local.Name, local.Register, i)
f.Comment("%s (%s) died in scope %d", local.Name, local.Register, i)
}
scope.Free(local.Register)
@ -87,7 +91,7 @@ func (f *Function) identifierExists(name string) bool {
return true
}
_, exists := f.functions[name]
_, exists := f.Functions[name]
return exists
}

View File

@ -3,9 +3,7 @@ package core
import (
"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/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"
)
@ -15,7 +13,7 @@ type Function struct {
Name string
File *fs.File
Body token.List
state
State
}
// NewFunction creates a new function.
@ -24,8 +22,8 @@ func NewFunction(name string, file *fs.File, body token.List) *Function {
Name: name,
File: file,
Body: body,
state: state{
assembler: asm.Assembler{
State: State{
Assembler: asm.Assembler{
Instructions: make([]asm.Instruction, 0, 32),
},
cpu: cpu.CPU{
@ -43,65 +41,6 @@ func NewFunction(name string, file *fs.File, body token.List) *Function {
}
}
// Compile turns a function into machine code.
func (f *Function) Compile() {
defer close(f.finished)
f.AddLabel(f.Name)
f.err = f.CompileTokens(f.Body)
f.Return()
}
// CompileTokens compiles a token list.
func (f *Function) CompileTokens(tokens token.List) error {
body, err := ast.Parse(tokens)
if err != nil {
err.(*errors.Error).File = f.File
return err
}
return f.CompileAST(body)
}
// CompileAST compiles an abstract syntax tree.
func (f *Function) CompileAST(tree ast.AST) error {
for _, node := range tree {
err := f.CompileASTNode(node)
if err != nil {
return err
}
}
return nil
}
// 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.CompileCall(node.Expression)
case *ast.Define:
return f.CompileDefinition(node)
case *ast.Return:
return f.CompileReturn(node)
case *ast.If:
return f.CompileIf(node)
case *ast.Loop:
return f.CompileLoop(node)
default:
panic("unknown AST type")
}
}
// String returns the function name.
func (f *Function) String() string {
return f.Name

View File

@ -9,28 +9,28 @@ import (
)
func (f *Function) AddLabel(label string) {
f.assembler.Label(asm.LABEL, label)
f.Assembler.Label(asm.LABEL, label)
f.postInstruction()
}
func (f *Function) Call(label string) {
f.assembler.Call(label)
f.Assembler.Call(label)
f.Scope().Use(f.cpu.Output[0])
f.postInstruction()
}
func (f *Function) Comment(format string, args ...any) {
f.assembler.Comment(fmt.Sprintf(format, args...))
f.Assembler.Comment(fmt.Sprintf(format, args...))
f.postInstruction()
}
func (f *Function) Jump(mnemonic asm.Mnemonic, label string) {
f.assembler.Label(mnemonic, label)
f.Assembler.Label(mnemonic, label)
f.postInstruction()
}
func (f *Function) Register(mnemonic asm.Mnemonic, a cpu.Register) {
f.assembler.Register(mnemonic, a)
f.Assembler.Register(mnemonic, a)
if mnemonic == asm.POP {
f.Scope().Use(a)
@ -44,7 +44,7 @@ func (f *Function) RegisterNumber(mnemonic asm.Mnemonic, a cpu.Register, b int)
f.SaveRegister(a)
}
f.assembler.RegisterNumber(mnemonic, a, b)
f.Assembler.RegisterNumber(mnemonic, a, b)
if mnemonic == asm.MOVE {
f.Scope().Use(a)
@ -58,7 +58,7 @@ func (f *Function) RegisterLabel(mnemonic asm.Mnemonic, register cpu.Register, l
f.SaveRegister(register)
}
f.assembler.RegisterLabel(mnemonic, register, label)
f.Assembler.RegisterLabel(mnemonic, register, label)
if mnemonic == asm.MOVE {
f.Scope().Use(register)
@ -76,7 +76,7 @@ func (f *Function) RegisterRegister(mnemonic asm.Mnemonic, a cpu.Register, b cpu
f.SaveRegister(a)
}
f.assembler.RegisterRegister(mnemonic, a, b)
f.Assembler.RegisterRegister(mnemonic, a, b)
if mnemonic == asm.MOVE {
f.Scope().Use(a)
@ -86,12 +86,12 @@ func (f *Function) RegisterRegister(mnemonic asm.Mnemonic, a cpu.Register, b cpu
}
func (f *Function) Return() {
f.assembler.Return()
f.Assembler.Return()
f.postInstruction()
}
func (f *Function) Syscall() {
f.assembler.Syscall()
f.Assembler.Syscall()
f.Scope().Use(f.cpu.Output[0])
f.postInstruction()
}

View File

@ -15,7 +15,7 @@ type Scope struct {
}
// Scope returns the current scope.
func (s *state) Scope() *Scope {
func (s *State) Scope() *Scope {
return s.scopes[len(s.scopes)-1]
}

View File

@ -9,14 +9,14 @@ import (
"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
// State is the data structure we embed in each function to preserve compilation State.
type State struct {
Assembler asm.Assembler
Functions map[string]*Function
Err error
scopes []*Scope
functions map[string]*Function
registerHistory []uint64
finished chan struct{}
assembler asm.Assembler
cpu cpu.CPU
count counter
}
@ -30,10 +30,10 @@ type counter struct {
}
// PrintInstructions shows the assembly instructions.
func (s *state) PrintInstructions() {
func (s *State) PrintInstructions() {
ansi.Dim.Println("╭──────────────────────────────────────────────────────────────────────────────╮")
for i, x := range s.assembler.Instructions {
for i, x := range s.Assembler.Instructions {
ansi.Dim.Print("│ ")
switch x.Mnemonic {

View File

@ -41,7 +41,7 @@ func (f *Function) TokenToRegister(t token.Token, register cpu.Register) error {
f.count.data++
label := fmt.Sprintf("%s_data_%d", f.Name, f.count.data)
value := t.Bytes[1 : len(t.Bytes)-1]
f.assembler.SetData(label, value)
f.Assembler.SetData(label, value)
f.RegisterLabel(asm.MOVE, register, label)
return nil

View File

@ -13,12 +13,12 @@ type Variable struct {
}
// Variable returns the variable with the given name or `nil` if it doesn't exist.
func (s *state) Variable(name string) *Variable {
func (s *State) Variable(name string) *Variable {
return s.Scope().variables[name]
}
// VariableInRegister returns the variable that occupies the given register or `nil` if none occupy the register.
func (s *state) VariableInRegister(register cpu.Register) *Variable {
func (s *State) VariableInRegister(register cpu.Register) *Variable {
for _, v := range s.Scope().variables {
if v.Register == register {
return v

View File

@ -3,6 +3,9 @@ package tests_test
import (
"path/filepath"
"testing"
"git.akyoto.dev/cli/q/src/build"
"git.akyoto.dev/go/assert"
)
var examples = []struct {
@ -22,3 +25,16 @@ func TestExamples(t *testing.T) {
})
}
}
func BenchmarkExamples(b *testing.B) {
for _, test := range examples {
b.Run(test.Name, func(b *testing.B) {
compiler := build.New(filepath.Join("..", "examples", test.Name))
for i := 0; i < b.N; i++ {
_, err := compiler.Run()
assert.Nil(b, err)
}
})
}
}