diff --git a/asm/amd64/amd64.go b/asm/amd64/amd64.go new file mode 100644 index 0000000..009d674 --- /dev/null +++ b/asm/amd64/amd64.go @@ -0,0 +1,115 @@ +package amd64 + +import ( + "fmt" + "strings" +) + +type Program struct { + Functions []Function +} + +func (p *Program) Emit() string { + var builder strings.Builder + + for _, function := range p.Functions { + builder.WriteString(function.Emit()) + builder.WriteString("\n") + } + + return builder.String() +} + +type Function struct { + Name string + Instructions []Instruction +} + +// This calls the main function and uses it's return value to exit +const executableAsmHeader = "format ELF64 executable\n" + + "segment readable executable\n" + + "entry _start\n" + + "_start:\n" + + " call main\n" + + " mov rdi, rax\n" + + " mov rax, 60\n" + + " syscall\n" + +func (f *Function) Emit() string { + var builder strings.Builder + + builder.WriteString(executableAsmHeader) + builder.WriteString(fmt.Sprintf("%s:\n", f.Name)) + + for _, inst := range f.Instructions { + builder.WriteString(fmt.Sprintf(" %s\n", inst.InstructionString())) + } + + return builder.String() +} + +type Opcode string + +const ( + Mov Opcode = "mov" + Ret Opcode = "ret" +) + +type Instruction struct { + Opcode Opcode + Lhs Operand + Rhs Operand +} + +func (i *Instruction) InstructionString() string { + if i.Lhs == nil { + return fmt.Sprintf("%s", i.Opcode) + } + + return fmt.Sprintf("%s %s, %s", i.Opcode, i.Lhs.OperandString(Eight), i.Rhs.OperandString(Eight)) +} + +type OperandSize int + +type Operand interface { + OperandString(OperandSize) string +} + +type Register int + +const ( + AX Register = iota + R10 + + One OperandSize = iota + Four + Eight +) + +func (r Register) OperandString(size OperandSize) string { + switch r { + case AX: + switch size { + case One: + return "al" + case Four: + return "eax" + } + return "rax" + case R10: + switch size { + case One: + return "r10b" + case Four: + return "r10d" + } + return "r10" + } + return "INVALID_REGISTER" +} + +type Imm int64 + +func (i Imm) OperandString(size OperandSize) string { + return fmt.Sprintf("%d", i) +} diff --git a/asm/amd64/amd64_test.go b/asm/amd64/amd64_test.go new file mode 100644 index 0000000..0f5ce27 --- /dev/null +++ b/asm/amd64/amd64_test.go @@ -0,0 +1,136 @@ +package amd64 + +import ( + "strings" + "testing" + + "robaertschi.xyz/robaertschi/tt/ttir" +) + +func TestOperands(t *testing.T) { + var op Operand + + op = AX + if str := op.OperandString(One); str != "al" { + t.Errorf("The register AX should be \"al\" but got %q", str) + } + + op = Imm(3) + if str := op.OperandString(One); str != "3" { + t.Errorf("The immediate value 3 should be \"3\" but got %q", str) + } +} + +func TestCodegen(t *testing.T) { + program := ttir.Program{ + Functions: []ttir.Function{ + { + Name: "main", + Instructions: []ttir.Instruction{ + &ttir.Ret{Op: &ttir.Constant{Value: 0}}, + }, + }, + }, + } + + expectedProgram := Program{ + Functions: []Function{ + { + Name: "main", + Instructions: []Instruction{ + { + Opcode: Mov, + Lhs: AX, + Rhs: Imm(0), + }, + { + Opcode: Ret, + }, + }, + }, + }, + } + + actualProgram := CgProgram(program) + expectProgram(t, expectedProgram, actualProgram) + + actual := actualProgram.Emit() + + expected := executableAsmHeader + "main:\n mov rax, 0\n ret\n" + if strings.Trim(actual, " \n\t") != strings.Trim(expected, " \n\t") { + t.Errorf("Expected program to be:\n>>%s<<\nbut got:\n>>%s<<\n", expected, actual) + } +} + +func expectProgram(t *testing.T, expected Program, actual Program) { + t.Helper() + if len(expected.Functions) != len(actual.Functions) { + t.Errorf("Expected %d functions but got %d", len(expected.Functions), len(actual.Functions)) + return + } + + for i := range expected.Functions { + expectFunction(t, expected.Functions[i], actual.Functions[i]) + } +} + +func expectFunction(t *testing.T, expected Function, actual Function) { + t.Helper() + if expected.Name != actual.Name { + t.Errorf("Expected function name %q but got %q", expected.Name, actual.Name) + } + + if len(expected.Instructions) != len(actual.Instructions) { + t.Errorf("Expected %d instructions but got %d, expected: %v, actual: %v", len(expected.Instructions), len(actual.Instructions), expected.Instructions, actual.Instructions) + return + } + + for i := range expected.Instructions { + expectInstruction(t, expected.Instructions[i], actual.Instructions[i]) + } +} + +func expectInstruction(t *testing.T, expected Instruction, actual Instruction) { + t.Helper() + if expected.Opcode != actual.Opcode { + t.Errorf("Expected opcode %q but got %q", expected.Opcode, actual.Opcode) + } + + switch expected.Opcode { + case Mov: + expectOperand(t, expected.Lhs, actual.Lhs) + expectOperand(t, expected.Rhs, actual.Rhs) + case Ret: + // nothing to do + } + +} + +func expectOperand(t *testing.T, expected Operand, actual Operand) { + t.Helper() + + switch expected := expected.(type) { + case Register: + actual, ok := actual.(Register) + if !ok { + t.Errorf("Expected Register but got %T", actual) + return + } + + if expected != actual { + t.Errorf("Expected Register %q but got %q", expected, actual) + } + case Imm: + actual, ok := actual.(Imm) + if !ok { + t.Errorf("Expected Immediate but got %T", actual) + return + } + + if expected != actual { + t.Errorf("Expected Immediate %q but got %q", expected, actual) + } + default: + t.Errorf("Unknown operand type %T", expected) + } +} diff --git a/asm/amd64/codegen.go b/asm/amd64/codegen.go new file mode 100644 index 0000000..e70ffa1 --- /dev/null +++ b/asm/amd64/codegen.go @@ -0,0 +1,59 @@ +package amd64 + +import ( + "fmt" + + "robaertschi.xyz/robaertschi/tt/ttir" +) + +func toAsmOperand(op ttir.Operand) Operand { + switch op := op.(type) { + case *ttir.Constant: + return Imm(op.Value) + default: + panic(fmt.Sprintf("unkown operand %T", op)) + } +} + +func CgProgram(prog ttir.Program) Program { + funcs := make([]Function, 0) + + for _, f := range prog.Functions { + funcs = append(funcs, cgFunction(f)) + } + + return Program{ + Functions: funcs, + } +} + +func cgFunction(f ttir.Function) Function { + newInstructions := []Instruction{} + + for _, inst := range f.Instructions { + newInstructions = append(newInstructions, cgInstruction(inst)...) + } + + return Function{ + Name: f.Name, + Instructions: newInstructions, + } +} + +func cgInstruction(i ttir.Instruction) []Instruction { + switch i := i.(type) { + case *ttir.Ret: + return []Instruction{ + { + Opcode: Mov, + Lhs: AX, + Rhs: toAsmOperand(i.Op), + }, + { + Opcode: Ret, + }, + } + } + + return []Instruction{} +} diff --git a/cmd/cmd.go b/cmd/cmd.go new file mode 100644 index 0000000..f325529 --- /dev/null +++ b/cmd/cmd.go @@ -0,0 +1,26 @@ +package main + +import ( + "flag" + "fmt" + "os" +) + +func main() { + + flag.Usage = func() { + fmt.Fprintf(flag.CommandLine.Output(), "Usage of %s [flags] input\nPossible flags:\n", os.Args[0]) + flag.PrintDefaults() + } + + var output string + flag.StringVar(&output, "o", "", "Output a executable named `file`") + flag.StringVar(&output, "output", "", "Output a executable named `file`") + flag.Parse() + + input := flag.Arg(0) + if input == "" { + flag.Usage() + os.Exit(1) + } +} diff --git a/test.asm b/test.asm new file mode 100644 index 0000000..e10f7b1 --- /dev/null +++ b/test.asm @@ -0,0 +1,13 @@ +format ELF64 executable + +segment readable executable +main: + mov rax, 0 + ret + +entry _start +_start: + call main + mov rdi, rax + mov rax, 60 + syscall diff --git a/ttir/emit.go b/ttir/emit.go index a40f7e4..37ce260 100644 --- a/ttir/emit.go +++ b/ttir/emit.go @@ -18,7 +18,7 @@ func EmitProgram(program *tast.Program) *Program { func emitFunction(function *tast.FunctionDeclaration) *Function { value, instructions := emitExpression(function.Body) - instructions = append(instructions, &Ret{op: value}) + instructions = append(instructions, &Ret{Op: value}) return &Function{ Name: function.Name, Instructions: instructions, diff --git a/ttir/ttir.go b/ttir/ttir.go index 1739a60..8da3e3e 100644 --- a/ttir/ttir.go +++ b/ttir/ttir.go @@ -30,11 +30,11 @@ type Instruction interface { } type Ret struct { - op Operand + Op Operand } func (r *Ret) String() string { - return fmt.Sprintf("ret %s\n", r.op) + return fmt.Sprintf("ret %s\n", r.Op) } func (r *Ret) instruction() {} diff --git a/ttir/ttir_test.go b/ttir/ttir_test.go index 2916617..69d4ce5 100644 --- a/ttir/ttir_test.go +++ b/ttir/ttir_test.go @@ -85,7 +85,7 @@ func expectInstruction(t *testing.T, inst Instruction, actual Instruction) { return } - expectOperand(t, inst.op, ret.op) + expectOperand(t, inst.Op, ret.Op) } } @@ -116,7 +116,7 @@ func TestBasicFunction(t *testing.T) { Name: "main", Instructions: []Instruction{ &Ret{ - op: &Constant{Value: 0}, + Op: &Constant{Value: 0}, }, }, }, diff --git a/typechecker/checker.go b/typechecker/checker.go index 5b8c0b9..e685355 100644 --- a/typechecker/checker.go +++ b/typechecker/checker.go @@ -9,7 +9,9 @@ import ( "robaertschi.xyz/robaertschi/tt/token" ) -type Checker struct{} +type Checker struct { + foundMain bool +} func New() *Checker { return &Checker{} @@ -32,6 +34,11 @@ func (c *Checker) CheckProgram(program *ast.Program) (*tast.Program, error) { } } + if !c.foundMain { + // TODO: Add support for libraries + errs = append(errs, errors.New("no function called 'main' found")) + } + return &tast.Program{Declarations: decls}, errors.Join(errs...) } @@ -44,6 +51,10 @@ func (c *Checker) checkDeclaration(decl ast.Declaration) (tast.Declaration, erro return nil, err } + if decl.Name == "main" { + c.foundMain = true + } + return &tast.FunctionDeclaration{Token: decl.Token, Body: body, ReturnType: body.Type(), Name: decl.Name}, nil } return nil, errors.New("unhandled declaration in type checker")