package build 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/os/linux" ) // Result contains all the compiled functions in a build. type Result struct { Main *Function Functions map[string]*Function InstructionCount int } // Finalize generates the final machine code. func (r *Result) Finalize() ([]byte, []byte) { // This will be the entry point of the executable. // The only job of the entry function is to call `main` and exit cleanly. // The reason we call `main` instead of using `main` itself is to place // a return address on the stack, which allows return statements in `main`. final := asm.Assembler{ Instructions: make([]asm.Instruction, 0, r.InstructionCount+4), } final.Call("main") final.RegisterNumber(asm.MOVE, x64.SyscallRegisters[0], linux.Exit) final.RegisterNumber(asm.MOVE, x64.SyscallRegisters[1], 0) final.Syscall() // 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) }) code, data := final.Finalize() return code, data } // 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)) { 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) } } // Write write the final executable to disk. func (r *Result) Write(path string) error { code, data := r.Finalize() return Write(path, code, data) }