Simplified compiler package

This commit is contained in:
Eduard Urbach 2025-02-12 19:05:40 +01:00
parent b7b4dad1a5
commit 0dffb79364
Signed by: akyoto
GPG Key ID: C874F672B1AF20C0
11 changed files with 234 additions and 201 deletions

View File

@ -1,8 +1,6 @@
package compiler
import (
"sync"
"git.akyoto.dev/cli/q/src/core"
"git.akyoto.dev/cli/q/src/errors"
"git.akyoto.dev/cli/q/src/fs"
@ -16,7 +14,7 @@ func Compile(files <-chan *fs.File, functions <-chan *core.Function, structs <-c
allFunctions := map[string]*core.Function{}
allStructs := map[string]*types.Struct{}
for functions != nil || files != nil || errs != nil {
for functions != nil || structs != nil || files != nil || errs != nil {
select {
case function, ok := <-functions:
if !ok {
@ -102,23 +100,3 @@ func Compile(files <-chan *fs.File, functions <-chan *core.Function, structs <-c
result.finalize()
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 {
if function.IsExtern() {
continue
}
wg.Add(1)
go func() {
defer wg.Done()
function.Compile()
}()
}
wg.Wait()
}

View File

@ -0,0 +1,27 @@
package compiler
import (
"sync"
"git.akyoto.dev/cli/q/src/core"
)
// 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 {
if function.IsExtern() {
continue
}
wg.Add(1)
go func() {
defer wg.Done()
function.Compile()
}()
}
wg.Wait()
}

View File

@ -0,0 +1,10 @@
package compiler
import "git.akyoto.dev/cli/q/src/core"
// PrintInstructions prints out the generated instructions.
func (r *Result) PrintInstructions() {
r.eachFunction(r.Main, map[*core.Function]bool{}, func(f *core.Function) {
f.PrintInstructions()
})
}

View File

@ -0,0 +1,29 @@
package compiler
import (
"fmt"
"git.akyoto.dev/go/color/ansi"
)
// PrintStatistics shows the statistics.
func (r *Result) PrintStatistics() {
ansi.Dim.Println("╭──────────────────────────────────────────────────────────────────────────────╮")
ansi.Dim.Print("│ ")
ansi.Dim.Printf("%-44s", "Code:")
fmt.Printf("%-32s", fmt.Sprintf("%d bytes", len(r.Code)))
ansi.Dim.Print(" │\n")
ansi.Dim.Print("│ ")
ansi.Dim.Printf("%-44s", "Data:")
fmt.Printf("%-32s", fmt.Sprintf("%d bytes", len(r.Data)))
ansi.Dim.Print(" │\n")
ansi.Dim.Print("│ ")
ansi.Dim.Printf("%-44s", "Functions:")
fmt.Printf("%-32s", fmt.Sprintf("%d / %d", len(r.Traversed), len(r.Functions)))
ansi.Dim.Print(" │\n")
ansi.Dim.Println("╰──────────────────────────────────────────────────────────────────────────────╯")
}

View File

@ -1,24 +1,11 @@
package compiler
import (
"bufio"
"fmt"
"io"
"os"
"git.akyoto.dev/cli/q/src/asm"
"git.akyoto.dev/cli/q/src/asmc"
"git.akyoto.dev/cli/q/src/config"
"git.akyoto.dev/cli/q/src/core"
"git.akyoto.dev/cli/q/src/dll"
"git.akyoto.dev/cli/q/src/elf"
"git.akyoto.dev/cli/q/src/macho"
"git.akyoto.dev/cli/q/src/pe"
"git.akyoto.dev/cli/q/src/x86"
"git.akyoto.dev/go/color/ansi"
)
// Result contains all the compiled functions in a build.
// Result contains everything we need to write an executable file to disk.
type Result struct {
Main *core.Function
Functions map[string]*core.Function
@ -29,167 +16,3 @@ type Result struct {
Data []byte
DLLs dll.List
}
// finalize generates the final machine code.
func (r *Result) finalize() {
// 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
// a return address on the stack, which allows return statements in `main`.
final := asm.Assembler{
Instructions: make([]asm.Instruction, 0, r.InstructionCount+8),
Data: make(map[string][]byte, r.DataCount),
}
final.Call("main.main")
switch config.TargetOS {
case config.Linux:
final.RegisterNumber(asm.MOVE, x86.SyscallInputRegisters[0], LinuxExit)
final.RegisterNumber(asm.MOVE, x86.SyscallInputRegisters[1], 0)
final.Syscall()
case config.Mac:
final.RegisterNumber(asm.MOVE, x86.SyscallInputRegisters[0], MacExit)
final.RegisterNumber(asm.MOVE, x86.SyscallInputRegisters[1], 0)
final.Syscall()
case config.Windows:
final.RegisterNumber(asm.MOVE, x86.WindowsInputRegisters[0], 0)
final.DLLCall("kernel32.ExitProcess")
}
r.DLLs = dll.List{
{Name: "kernel32", Functions: []string{"ExitProcess"}},
}
r.Traversed = make(map[*core.Function]bool, len(r.Functions))
// This will place the main function immediately after the entry point
// and also add everything the main function calls recursively.
r.eachFunction(r.Main, r.Traversed, func(f *core.Function) {
final.Merge(f.Assembler)
for _, library := range f.DLLs {
for _, fn := range library.Functions {
r.DLLs = r.DLLs.Append(library.Name, fn)
}
}
})
final.Label(asm.LABEL, "_crash")
switch config.TargetOS {
case config.Linux:
final.RegisterNumber(asm.MOVE, x86.SyscallInputRegisters[0], LinuxExit)
final.RegisterNumber(asm.MOVE, x86.SyscallInputRegisters[1], 1)
final.Syscall()
case config.Mac:
final.RegisterNumber(asm.MOVE, x86.SyscallInputRegisters[0], MacExit)
final.RegisterNumber(asm.MOVE, x86.SyscallInputRegisters[1], 1)
final.Syscall()
case config.Windows:
final.RegisterNumber(asm.MOVE, x86.WindowsInputRegisters[0], 1)
final.DLLCall("kernel32.ExitProcess")
}
r.Code, r.Data = asmc.Finalize(final, r.DLLs)
}
// 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 *core.Function, traversed map[*core.Function]bool, call func(*core.Function)) {
call(caller)
traversed[caller] = true
for _, x := range caller.Assembler.Instructions {
if x.Mnemonic != asm.CALL {
continue
}
name := x.Data.(*asm.Label).Name
callee, exists := r.Functions[name]
if !exists {
continue
}
if traversed[callee] {
continue
}
r.eachFunction(callee, traversed, call)
}
}
// PrintInstructions prints out the generated instructions.
func (r *Result) PrintInstructions() {
r.eachFunction(r.Main, map[*core.Function]bool{}, func(f *core.Function) {
f.PrintInstructions()
})
}
// PrintStatistics shows the statistics.
func (r *Result) PrintStatistics() {
ansi.Dim.Println("╭──────────────────────────────────────────────────────────────────────────────╮")
ansi.Dim.Print("│ ")
ansi.Dim.Printf("%-44s", "Code:")
fmt.Printf("%-32s", fmt.Sprintf("%d bytes", len(r.Code)))
ansi.Dim.Print(" │\n")
ansi.Dim.Print("│ ")
ansi.Dim.Printf("%-44s", "Data:")
fmt.Printf("%-32s", fmt.Sprintf("%d bytes", len(r.Data)))
ansi.Dim.Print(" │\n")
ansi.Dim.Print("│ ")
ansi.Dim.Printf("%-44s", "Functions:")
fmt.Printf("%-32s", fmt.Sprintf("%d / %d", len(r.Traversed), len(r.Functions)))
ansi.Dim.Print(" │\n")
ansi.Dim.Println("╰──────────────────────────────────────────────────────────────────────────────╯")
}
// Write writes the executable to the given writer.
func (r *Result) Write(writer io.Writer) error {
return write(writer, r.Code, r.Data, r.DLLs)
}
// Write writes an executable file to disk.
func (r *Result) WriteFile(path string) error {
file, err := os.Create(path)
if err != nil {
return err
}
err = r.Write(file)
if err != nil {
file.Close()
return err
}
err = file.Close()
if err != nil {
return err
}
return os.Chmod(path, 0755)
}
// write writes an executable file to the given writer.
func write(writer io.Writer, code []byte, data []byte, dlls dll.List) error {
buffer := bufio.NewWriter(writer)
switch config.TargetOS {
case config.Linux:
elf.Write(buffer, code, data)
case config.Mac:
macho.Write(buffer, code, data)
case config.Windows:
pe.Write(buffer, code, data, dlls)
}
return buffer.Flush()
}

33
src/compiler/Write.go Normal file
View File

@ -0,0 +1,33 @@
package compiler
import (
"bufio"
"io"
"git.akyoto.dev/cli/q/src/config"
"git.akyoto.dev/cli/q/src/dll"
"git.akyoto.dev/cli/q/src/elf"
"git.akyoto.dev/cli/q/src/macho"
"git.akyoto.dev/cli/q/src/pe"
)
// Write writes the executable to the given writer.
func (r *Result) Write(writer io.Writer) error {
return write(writer, r.Code, r.Data, r.DLLs)
}
// write writes an executable file to the given writer.
func write(writer io.Writer, code []byte, data []byte, dlls dll.List) error {
buffer := bufio.NewWriter(writer)
switch config.TargetOS {
case config.Linux:
elf.Write(buffer, code, data)
case config.Mac:
macho.Write(buffer, code, data)
case config.Windows:
pe.Write(buffer, code, data, dlls)
}
return buffer.Flush()
}

27
src/compiler/WriteFile.go Normal file
View File

@ -0,0 +1,27 @@
package compiler
import "os"
// Write writes an executable file to disk.
func (r *Result) WriteFile(path string) error {
file, err := os.Create(path)
if err != nil {
return err
}
err = r.Write(file)
if err != nil {
file.Close()
return err
}
err = file.Close()
if err != nil {
return err
}
return os.Chmod(path, 0755)
}

View File

@ -0,0 +1,32 @@
package compiler
import (
"git.akyoto.dev/cli/q/src/asm"
"git.akyoto.dev/cli/q/src/core"
)
// 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 *core.Function, traversed map[*core.Function]bool, call func(*core.Function)) {
call(caller)
traversed[caller] = true
for _, x := range caller.Assembler.Instructions {
if x.Mnemonic != asm.CALL {
continue
}
name := x.Data.(*asm.Label).Name
callee, exists := r.Functions[name]
if !exists {
continue
}
if traversed[callee] {
continue
}
r.eachFunction(callee, traversed, call)
}
}

74
src/compiler/finalize.go Normal file
View File

@ -0,0 +1,74 @@
package compiler
import (
"git.akyoto.dev/cli/q/src/asm"
"git.akyoto.dev/cli/q/src/asmc"
"git.akyoto.dev/cli/q/src/config"
"git.akyoto.dev/cli/q/src/core"
"git.akyoto.dev/cli/q/src/dll"
"git.akyoto.dev/cli/q/src/x86"
)
// finalize generates the final machine code.
func (r *Result) finalize() {
// 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
// a return address on the stack, which allows return statements in `main`.
final := asm.Assembler{
Instructions: make([]asm.Instruction, 0, r.InstructionCount+8),
Data: make(map[string][]byte, r.DataCount),
}
final.Call("main.main")
switch config.TargetOS {
case config.Linux:
final.RegisterNumber(asm.MOVE, x86.SyscallInputRegisters[0], LinuxExit)
final.RegisterNumber(asm.MOVE, x86.SyscallInputRegisters[1], 0)
final.Syscall()
case config.Mac:
final.RegisterNumber(asm.MOVE, x86.SyscallInputRegisters[0], MacExit)
final.RegisterNumber(asm.MOVE, x86.SyscallInputRegisters[1], 0)
final.Syscall()
case config.Windows:
final.RegisterNumber(asm.MOVE, x86.WindowsInputRegisters[0], 0)
final.DLLCall("kernel32.ExitProcess")
}
r.DLLs = dll.List{
{Name: "kernel32", Functions: []string{"ExitProcess"}},
}
r.Traversed = make(map[*core.Function]bool, len(r.Functions))
// This will place the main function immediately after the entry point
// and also add everything the main function calls recursively.
r.eachFunction(r.Main, r.Traversed, func(f *core.Function) {
final.Merge(f.Assembler)
for _, library := range f.DLLs {
for _, fn := range library.Functions {
r.DLLs = r.DLLs.Append(library.Name, fn)
}
}
})
final.Label(asm.LABEL, "_crash")
switch config.TargetOS {
case config.Linux:
final.RegisterNumber(asm.MOVE, x86.SyscallInputRegisters[0], LinuxExit)
final.RegisterNumber(asm.MOVE, x86.SyscallInputRegisters[1], 1)
final.Syscall()
case config.Mac:
final.RegisterNumber(asm.MOVE, x86.SyscallInputRegisters[0], MacExit)
final.RegisterNumber(asm.MOVE, x86.SyscallInputRegisters[1], 1)
final.Syscall()
case config.Windows:
final.RegisterNumber(asm.MOVE, x86.WindowsInputRegisters[0], 1)
final.DLLCall("kernel32.ExitProcess")
}
r.Code, r.Data = asmc.Finalize(final, r.DLLs)
}