From 0dffb7936411e05198c558f7e3463cd8a74775a2 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Wed, 12 Feb 2025 19:05:40 +0100 Subject: [PATCH] Simplified compiler package --- src/compiler/Compile.go | 24 +-- src/compiler/CompileFunctions.go | 27 +++ src/compiler/PrintInstructions.go | 10 ++ src/compiler/PrintStatistics.go | 29 ++++ src/compiler/Result.go | 179 +------------------- src/compiler/{Linux.go => SyscallsLinux.go} | 0 src/compiler/{Mac.go => SyscallsMac.go} | 0 src/compiler/Write.go | 33 ++++ src/compiler/WriteFile.go | 27 +++ src/compiler/eachFunction.go | 32 ++++ src/compiler/finalize.go | 74 ++++++++ 11 files changed, 234 insertions(+), 201 deletions(-) create mode 100644 src/compiler/CompileFunctions.go create mode 100644 src/compiler/PrintInstructions.go create mode 100644 src/compiler/PrintStatistics.go rename src/compiler/{Linux.go => SyscallsLinux.go} (100%) rename src/compiler/{Mac.go => SyscallsMac.go} (100%) create mode 100644 src/compiler/Write.go create mode 100644 src/compiler/WriteFile.go create mode 100644 src/compiler/eachFunction.go create mode 100644 src/compiler/finalize.go diff --git a/src/compiler/Compile.go b/src/compiler/Compile.go index df5f2dc..9a7f4e6 100644 --- a/src/compiler/Compile.go +++ b/src/compiler/Compile.go @@ -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() -} diff --git a/src/compiler/CompileFunctions.go b/src/compiler/CompileFunctions.go new file mode 100644 index 0000000..d4ee25b --- /dev/null +++ b/src/compiler/CompileFunctions.go @@ -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() +} diff --git a/src/compiler/PrintInstructions.go b/src/compiler/PrintInstructions.go new file mode 100644 index 0000000..686aa63 --- /dev/null +++ b/src/compiler/PrintInstructions.go @@ -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() + }) +} diff --git a/src/compiler/PrintStatistics.go b/src/compiler/PrintStatistics.go new file mode 100644 index 0000000..65b9066 --- /dev/null +++ b/src/compiler/PrintStatistics.go @@ -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("╰──────────────────────────────────────────────────────────────────────────────╯") +} diff --git a/src/compiler/Result.go b/src/compiler/Result.go index 9ca52e4..a0d6091 100644 --- a/src/compiler/Result.go +++ b/src/compiler/Result.go @@ -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() -} diff --git a/src/compiler/Linux.go b/src/compiler/SyscallsLinux.go similarity index 100% rename from src/compiler/Linux.go rename to src/compiler/SyscallsLinux.go diff --git a/src/compiler/Mac.go b/src/compiler/SyscallsMac.go similarity index 100% rename from src/compiler/Mac.go rename to src/compiler/SyscallsMac.go diff --git a/src/compiler/Write.go b/src/compiler/Write.go new file mode 100644 index 0000000..70c1e96 --- /dev/null +++ b/src/compiler/Write.go @@ -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() +} diff --git a/src/compiler/WriteFile.go b/src/compiler/WriteFile.go new file mode 100644 index 0000000..009562e --- /dev/null +++ b/src/compiler/WriteFile.go @@ -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) +} diff --git a/src/compiler/eachFunction.go b/src/compiler/eachFunction.go new file mode 100644 index 0000000..03973f4 --- /dev/null +++ b/src/compiler/eachFunction.go @@ -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) + } +} diff --git a/src/compiler/finalize.go b/src/compiler/finalize.go new file mode 100644 index 0000000..343347e --- /dev/null +++ b/src/compiler/finalize.go @@ -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) +}