├── .gitignore ├── run.bat ├── debug.bat ├── .vscode ├── tasks.json └── launch.json ├── README.md ├── src ├── test.lox ├── main.odin ├── chunk.odin ├── memory.odin ├── value.odin ├── table.odin ├── debug.odin ├── scanner.odin ├── object.odin ├── vm.odin └── compiler.odin └── test.lox /.gitignore: -------------------------------------------------------------------------------- 1 | .vs/ 2 | 3 | *.exe 4 | *.pdb 5 | ols.json -------------------------------------------------------------------------------- /run.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | if not exist \bin mkdir \bin 4 | odin build src -out:bin/lox.exe -o:speed 5 | .\bin\lox.exe %* -------------------------------------------------------------------------------- /debug.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | if not exist \bin mkdir \bin 4 | odin build src -debug -out:bin/lox.exe 5 | echo Debug build complete. 6 | 7 | echo Opening Visual Studio 8 | devenv bin/lox.exe %* -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "build", 8 | "type": "shell", 9 | "command": "odin build src -debug -out:bin/lox.exe" 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # odinLox 2 | An implementation of a Lox bytecode virtual machine and compiler based on Robert Nystrom's Crafting Interpreters written in the [Odin Programming Language](https://odin-lang.org/). 3 | 4 | The language is complete, including modulo and NaN Boxing optimizations. 5 | 6 | ## Usage - Windows 7 | To build and run the VM, simply run: 8 | ``` 9 | .\run 10 | ``` 11 | 12 | ## Usage - Other 13 | I've not included build scripts for other platforms but provided you have an Odin compiler you can run: 14 | ``` 15 | odin build src -out:lox.exe -o:speed 16 | ``` 17 | This will produce an executable for your platform. 18 | -------------------------------------------------------------------------------- /src/test.lox: -------------------------------------------------------------------------------- 1 | 2 | class Zoo { 3 | init(a, b) { 4 | this.aarvark = 1; 5 | this.baboon = 1; 6 | this.cat = 1; 7 | this.donkey = 1; 8 | this.elephant = 1; 9 | this.fox = 1; 10 | } 11 | ant() { return this.aarvark; } 12 | banana() { return this.baboon; } 13 | tuna() { return this.cat; } 14 | hay() { return this.donkey; } 15 | grass() { return this.elephant; } 16 | mouse() { return this.fox; } 17 | } 18 | 19 | var zoo = Zoo("hello", "zoo"); 20 | var sum = 0; 21 | var start = clock(); 22 | var batch = 0; 23 | while (clock() - start < 10000000000) { 24 | for (var i = 0; i < 10000; i = i + 1) { 25 | } 26 | } 27 | 28 | //print sum; 29 | //print batch; 30 | //print clock() - start; 31 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "(Windows) Launch", 9 | "type": "cppvsdbg", 10 | "request": "launch", 11 | "program": "${workspaceRoot}/bin/lox.exe", 12 | "args": ["test.lox"], 13 | "stopAtEntry": false, 14 | "cwd": "${fileDirname}", 15 | "environment": [], 16 | "console": "integratedTerminal", 17 | "preLaunchTask": "build" 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /test.lox: -------------------------------------------------------------------------------- 1 | class Zoo { 2 | init() { 3 | this.aarvark = 1; 4 | this.baboon = 1; 5 | this.cat = 1; 6 | this.donkey = 1; 7 | this.elephant = 1; 8 | this.fox = 1; 9 | } 10 | ant() { return this.aarvark; } 11 | banana() { return this.baboon; } 12 | tuna() { return this.cat; } 13 | hay() { return this.donkey; } 14 | grass() { return this.elephant; } 15 | mouse() { return this.fox; } 16 | } 17 | 18 | var zoo = Zoo(); 19 | var sum = 0; 20 | var start = clock(); 21 | var batch = 0; 22 | while (clock() - start < 10000000000) { 23 | for (var i = 0; i < 10000; i = i + 1) { 24 | sum = sum + zoo.ant() 25 | + zoo.banana() 26 | + zoo.tuna() 27 | + zoo.hay() 28 | + zoo.grass() 29 | + zoo.mouse(); 30 | } 31 | batch = batch + 1; 32 | } 33 | 34 | print sum; 35 | print batch; 36 | print clock() - start; 37 | -------------------------------------------------------------------------------- /src/main.odin: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "core:bufio" 4 | import "core:fmt" 5 | import "core:io" 6 | import "core:log" 7 | import "core:mem" 8 | import "core:os" 9 | import "core:time" 10 | 11 | main :: proc() { 12 | context.logger = log.create_console_logger() 13 | 14 | // tracking_allocator: mem.Tracking_Allocator 15 | // mem.tracking_allocator_init(&tracking_allocator, context.allocator) 16 | // context.allocator = mem.tracking_allocator(&tracking_allocator) 17 | 18 | initVM() 19 | // defer(freeVM()) 20 | 21 | if (len(os.args) == 1) { 22 | repl() 23 | } else if (len(os.args) == 2) { 24 | runFile(os.args[1]) 25 | } else { 26 | fmt.println("Usage: olox [path]") 27 | os.exit(64) 28 | } 29 | 30 | // for _, leak in tracking_allocator.allocation_map { 31 | // log.warnf("%v leaked %v bytes", leak.location, leak.size) 32 | // } 33 | // for bad_free in tracking_allocator.bad_free_array { 34 | // log.warnf("%v allocation %p was freed badly", bad_free.location, bad_free.memory) 35 | // } 36 | } 37 | 38 | repl :: proc() { 39 | line: [1024]u8 40 | reader: bufio.Reader 41 | bufio.reader_init_with_buf(&reader, io.to_reader(os.stream_from_handle(os.stdin)), line[:]) 42 | for { 43 | fmt.printf("> ") 44 | 45 | line, err := bufio.reader_read_slice(&reader, '\n') 46 | if err != nil { 47 | fmt.println(err) 48 | break; 49 | } 50 | interpret(string(line[:])) 51 | } 52 | } 53 | 54 | runFile :: proc(path: string) { 55 | // fmt.println(os.get_current_directory()) 56 | source, success := os.read_entire_file(path) 57 | if (!success) { 58 | fmt.printf("Could not open file \"%v\".\n", path) 59 | os.exit(74) 60 | } 61 | defer delete(source) 62 | result := interpret(string(source[:])) 63 | 64 | if result == InterpretResult.COMPILE_ERROR { os.exit(65) } 65 | if result == InterpretResult.RUNTIME_ERROR { os.exit(70) } 66 | 67 | } -------------------------------------------------------------------------------- /src/chunk.odin: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "core:fmt" 4 | 5 | OpCode :: enum u8 { 6 | CONSTANT, 7 | NIL, 8 | TRUE, 9 | FALSE, 10 | POP, 11 | GET_LOCAL, 12 | SET_LOCAL, 13 | GET_GLOBAL, 14 | DEFINE_GLOBAL, 15 | SET_GLOBAL, 16 | GET_UPVALUE, 17 | SET_UPVALUE, 18 | GET_PROPERTY, 19 | SET_PROPERTY, 20 | GET_SUPER, 21 | EQUAL, 22 | GREATER, 23 | LESS, 24 | ADD, 25 | SUBTRACT, 26 | MULTIPLY, 27 | DIVIDE, 28 | NOT, 29 | NEGATE, 30 | PRINT, 31 | JUMP, 32 | JUMP_IF_FALSE, 33 | LOOP, 34 | CALL, 35 | INVOKE, 36 | SUPER_INVOKE, 37 | CLOSURE, 38 | CLOSE_UPVALUE, 39 | RETURN, 40 | METHOD, 41 | CLASS, 42 | INHERIT, 43 | } 44 | 45 | Chunk :: struct { 46 | code: [dynamic]u8, 47 | lines: [dynamic]int, 48 | constants: [dynamic]Value, 49 | } 50 | 51 | freeChunk :: proc(c: ^Chunk) { 52 | delete(c.code) 53 | delete(c.constants) 54 | } 55 | 56 | @(private = "file") 57 | writeChunk_proc :: proc(c: ^Chunk, byte: u8, line: int) { 58 | if DEBUG_STRESS_GC { 59 | collectGarbage() 60 | } 61 | append(&c.code, byte) 62 | append(&c.lines, line) 63 | } 64 | 65 | @(private = "file") 66 | writeChunk_OpCode :: proc(c: ^Chunk, op: OpCode, line: int) { 67 | writeChunk_proc(c, cast(u8)op, line) 68 | } 69 | 70 | @(private = "file") 71 | writeChunk_Int :: proc(c: ^Chunk, i: int, line: int) { 72 | writeChunk_proc(c, cast(u8)i, line) 73 | } 74 | 75 | writeChunk_byte :: proc(c: ^Chunk, byte: u8, line: int) { 76 | writeChunk_proc(c, byte, line) 77 | } 78 | 79 | writeChunk :: proc { 80 | writeChunk_OpCode, 81 | writeChunk_Int, 82 | writeChunk_byte, 83 | } 84 | 85 | addConstant :: proc(c: ^Chunk, value: Value) -> int { 86 | if DEBUG_STRESS_GC { 87 | collectGarbage() 88 | } 89 | push(value) 90 | append(&c.constants, value) 91 | pop() 92 | return len(c.constants) - 1 93 | } 94 | -------------------------------------------------------------------------------- /src/memory.odin: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "core:log" 4 | import "core:fmt" 5 | 6 | DEBUG_STRESS_GC :: false 7 | DEBUG_LOG_GC :: false 8 | 9 | GC_HEAP_GROW_FACTOR :: 2 10 | 11 | collectGarbage :: proc() { 12 | when DEBUG_LOG_GC { 13 | log.debug("-- gc begin") 14 | before := vm.bytesAllocated 15 | } 16 | 17 | markRoots() 18 | traceReferences() 19 | tableRemoveWhite(&vm.strings) 20 | sweep() 21 | 22 | vm.nextGC = vm.bytesAllocated * GC_HEAP_GROW_FACTOR 23 | 24 | when DEBUG_LOG_GC { 25 | log.debug("-- gc end\n") 26 | log.debugf(" collected %v bytes (from %v to %v) next at %v", 27 | before - vm.bytesAllocated, before, vm.bytesAllocated, vm.nextGC) 28 | } 29 | } 30 | 31 | markObject :: proc(object: ^Obj) { 32 | if object == nil { return } 33 | if object.isMarked { return } 34 | 35 | when DEBUG_LOG_GC { 36 | fmt.printf("[DEBUG] --- %p mark ", object) 37 | printValue(OBJ_VAL(object)) 38 | fmt.println() 39 | } 40 | object.isMarked = true 41 | 42 | append(&vm.grayStack, object) 43 | vm.grayCount += 1 44 | } 45 | 46 | markValue :: proc(value: Value) { 47 | if IS_OBJ(value) { 48 | markObject(AS_OBJ(value)) 49 | } 50 | } 51 | 52 | markArray :: proc(array: ^[dynamic]Value) { 53 | for value in array { 54 | markValue(value) 55 | } 56 | } 57 | 58 | blackenObject :: proc(object: ^Obj) { 59 | when DEBUG_LOG_GC { 60 | fmt.printf("[DEBUG] --- %p blacken ", object) 61 | printValue(OBJ_VAL(object)) 62 | fmt.println() 63 | } 64 | 65 | switch object.type { 66 | case .BOUND_METHOD: { 67 | bound := cast(^ObjBoundMethod) object 68 | markValue(bound.receiver) 69 | markObject(bound.method) 70 | } 71 | case .CLASS: { 72 | klass := cast(^ObjClass) object 73 | markObject(klass.name) 74 | markTable(&klass.methods) 75 | } 76 | case .CLOSURE: { 77 | closure := cast(^ObjClosure) object 78 | markObject(closure.function) 79 | for upvalue in closure.upvalues { 80 | markObject(upvalue) 81 | } 82 | } 83 | case .FUNCTION: { 84 | function := cast(^ObjFunction) object 85 | markObject(function.name) 86 | markArray(&function.chunk.constants) 87 | } 88 | case .INSTANCE: { 89 | instance := cast(^ObjInstance) object 90 | markObject(instance.klass) 91 | markTable(&instance.fields) 92 | } 93 | case .UPVALUE: 94 | markValue((cast(^ObjUpvalue) object).closed) 95 | case .NATIVE: 96 | fallthrough 97 | case .STRING: 98 | } 99 | } 100 | 101 | markRoots :: proc() { 102 | for i in 0.. 0 { 120 | vm.grayCount -= 1 121 | object := vm.grayStack[vm.grayCount] 122 | blackenObject(object) 123 | } 124 | } 125 | 126 | sweep :: proc() { 127 | previous: ^Obj 128 | object := vm.objects 129 | for object != nil { 130 | if object.isMarked { 131 | object.isMarked = false 132 | previous = object 133 | object = object.next 134 | } else { 135 | unreached := object 136 | object = object.next 137 | if previous != nil { 138 | previous.next = object 139 | } else { 140 | vm.objects = object 141 | } 142 | 143 | freeObject(unreached) 144 | } 145 | } 146 | } -------------------------------------------------------------------------------- /src/value.odin: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "core:fmt" 4 | import "core:log" 5 | import "core:mem" 6 | 7 | 8 | NAN_BOXING :: true 9 | 10 | when NAN_BOXING { 11 | Value :: distinct u64 12 | 13 | SIGN_BIT : u64 : 0x8000000000000000 14 | QNAN : u64 : 0x7ffc000000000000 15 | 16 | TAG_NIL :: 1 17 | TAG_FALSE :: 2 18 | TAG_TRUE :: 3 19 | 20 | IS_BOOL :: proc(value: Value) -> bool { return (u64(value) | 1) == TRUE_VAL } 21 | IS_NIL :: proc(value: Value) -> bool { return (value) == NIL_VAL() } 22 | IS_NUMBER :: proc(value: Value) -> bool { return (u64(value) & QNAN) != QNAN } 23 | IS_OBJ :: proc(value: Value) -> bool { return (u64(value) & (QNAN | SIGN_BIT)) == (QNAN | SIGN_BIT)} 24 | 25 | AS_BOOL :: proc(value: Value) -> bool { return u64(value) == TRUE_VAL } 26 | AS_NUMBER :: proc(value: Value) -> f64 { return transmute(f64) value } 27 | AS_OBJ :: proc(value: Value) -> ^Obj { return cast(^Obj) uintptr(u64(value) & ~(SIGN_BIT | QNAN)) } 28 | 29 | FALSE_VAL : u64 : QNAN | TAG_FALSE 30 | TRUE_VAL : u64 : QNAN | TAG_TRUE 31 | BOOL_VAL :: proc(b: bool) -> Value { return Value(TRUE_VAL) if b else Value(FALSE_VAL) } 32 | NIL_VAL :: proc() -> Value { return Value(QNAN | TAG_NIL) } 33 | NUMBER_VAL :: proc(num: f64) -> Value { 34 | // mem.copy(&value, &num, size_of(f64)) 35 | return transmute(Value) num 36 | } 37 | OBJ_VAL :: proc(obj: ^Obj) -> Value { return Value(SIGN_BIT | QNAN | cast(u64) uintptr(obj))} 38 | 39 | } else { 40 | ValueType :: enum { 41 | BOOL, 42 | NIL, 43 | NUMBER, 44 | OBJ, 45 | } 46 | 47 | Value :: struct { 48 | type: ValueType, 49 | variant: union { 50 | bool, 51 | f64, 52 | ^Obj, 53 | }, 54 | } 55 | 56 | IS_BOOL :: proc(value: Value) -> bool { return value.type == .BOOL } 57 | IS_NIL :: proc(value: Value) -> bool { return value.type == .NIL } 58 | IS_NUMBER :: proc(value: Value) -> bool { return value.type == .NUMBER } 59 | IS_OBJ :: proc(value: Value) -> bool { return value.type == .OBJ } 60 | 61 | AS_OBJ :: proc(value: Value) -> ^Obj { return value.variant.(^Obj) } 62 | AS_BOOL :: proc(value: Value) -> bool { return value.variant.(bool) } 63 | AS_NUMBER :: proc(value: Value) -> f64 { return value.variant.(f64) } 64 | 65 | BOOL_VAL :: proc(value: bool) -> Value { return Value{.BOOL, value}} 66 | NIL_VAL :: proc() -> Value { return Value{.NIL, nil}} 67 | NUMBER_VAL :: proc(value: f64) -> Value { return Value{.NUMBER, value}} 68 | OBJ_VAL :: proc(value: ^Obj) -> Value { return Value{.OBJ, value}} 69 | } 70 | 71 | printValue :: proc(value: Value) { 72 | when NAN_BOXING { 73 | if (IS_BOOL(value)) { 74 | fmt.printf("true" if AS_BOOL(value) else "false") 75 | } else if (IS_NIL(value)) { 76 | fmt.printf("nil") 77 | } else if (IS_NUMBER(value)) { 78 | fmt.printf("%v", AS_NUMBER(value)) 79 | } else if (IS_OBJ(value)) { 80 | printObject(AS_OBJ(value)) 81 | } 82 | } else { 83 | #partial switch value.type { 84 | case .OBJ: printObject(AS_OBJ(value)) 85 | case: fmt.print(value.variant) 86 | } 87 | } 88 | } 89 | 90 | valuesEqual :: proc(a, b: Value) -> bool { 91 | when NAN_BOXING { 92 | if (IS_NUMBER(a) && IS_NUMBER(b)) { 93 | return AS_NUMBER(a) == AS_NUMBER(b) 94 | } 95 | return a == b 96 | } else { 97 | if a.type != b.type { return false } 98 | switch a.type { 99 | case .BOOL: return AS_BOOL(a) == AS_BOOL(b) 100 | case .NIL: return true 101 | case .NUMBER: return AS_NUMBER(a) == AS_NUMBER(b) 102 | // case .OBJ: return a.variant == b.variant // Valid because of string interning 103 | case .OBJ: return AS_OBJ(a) == AS_OBJ(b) 104 | case: return false // unreachable 105 | } 106 | } 107 | } -------------------------------------------------------------------------------- /src/table.odin: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "core:mem" 4 | import "core:strings" 5 | 6 | TABLE_MAX_LOAD :: 0.75 7 | 8 | Entry :: struct { 9 | key: ^ObjString, 10 | value: Value, 11 | } 12 | 13 | Table :: struct { 14 | count: int, 15 | capacity: int, 16 | entries: []Entry, 17 | } 18 | 19 | freeTable :: proc(table: ^Table) { 20 | delete(table.entries) 21 | } 22 | 23 | tableSet :: proc(table: ^Table, key: ^ObjString, value: Value) -> bool { 24 | if f32(table.count + 1) > f32(table.capacity) * TABLE_MAX_LOAD { 25 | capacity := growCapacity(table.capacity) 26 | adjustCapacity(table, capacity) 27 | } 28 | 29 | entry := findEntry(table.entries, table.capacity, key) 30 | isNewKey := entry.key == nil 31 | if isNewKey && IS_NIL(entry.value) { table.count += 1 } 32 | 33 | entry.key = key 34 | entry.value = value 35 | return isNewKey 36 | } 37 | 38 | tableDelete :: proc(table: ^Table, key: ^ObjString) -> bool { 39 | if table.count == 0 { return false } 40 | 41 | entry := findEntry(table.entries, table.capacity, key) 42 | if entry.key == nil { return false } 43 | 44 | entry.key = nil 45 | entry.value = BOOL_VAL(true) 46 | return true 47 | } 48 | 49 | tableAddAll :: proc(from, to: ^Table) { 50 | for i in 0.. ^ObjString { 59 | if table.count == 0 { return nil } 60 | 61 | index := hash & u32(table.capacity - 1) 62 | for { 63 | entry := &table.entries[index] 64 | if entry.key == nil { 65 | // Stop if we find an empty non-tombstone entry. 66 | if IS_NIL(entry.value) { return nil } 67 | } else if len(entry.key.str) == len(str) && entry.key.hash == hash && strings.compare(entry.key.str, str) == 0 { 68 | // We found it. 69 | return entry.key 70 | } 71 | 72 | index = (index + 1) & u32(table.capacity - 1) 73 | } 74 | } 75 | 76 | tableRemoveWhite :: proc(table: ^Table) { 77 | for entry in table.entries { 78 | if entry.key != nil && !entry.key.obj.isMarked { 79 | tableDelete(table, entry.key) 80 | } 81 | } 82 | } 83 | 84 | markTable :: proc(table: ^Table) { 85 | for i in 0.. ^Entry { 93 | index := key.hash & u32(capacity - 1) 94 | tombstone: ^Entry = nil 95 | for { 96 | entry := &entries[index] 97 | if entry.key == nil { 98 | if IS_NIL(entry.value) { 99 | // Empty entry. 100 | return tombstone if tombstone != nil else entry 101 | } else { 102 | // We found a tombstone 103 | if tombstone == nil { tombstone = entry } 104 | } 105 | } if entry.key == key { 106 | // We found the key. 107 | return entry 108 | } 109 | 110 | index = (index + 1) & u32(capacity - 1) 111 | } 112 | } 113 | 114 | tableGet :: proc(table: ^Table, key: ^ObjString) -> (Value, bool) { 115 | if table.count == 0 { return {}, false } 116 | 117 | entry := findEntry(table.entries, table.capacity, key) 118 | if entry.key == nil { return {}, false } 119 | 120 | return entry.value, true 121 | } 122 | 123 | adjustCapacity :: proc(table: ^Table, capacity: int) { 124 | entries := make([]Entry, capacity) 125 | for i in 0.. int { 147 | return 8 if capacity < 8 else capacity * 2 148 | } 149 | -------------------------------------------------------------------------------- /src/debug.odin: -------------------------------------------------------------------------------- 1 | //+private 2 | package main 3 | 4 | import "core:fmt" 5 | 6 | disassembleChunk :: proc(chunk: Chunk, name: string) { 7 | fmt.printf("== %s ==\n", name) 8 | 9 | offset := 0; 10 | for offset < len(chunk.code) { 11 | offset = disassembleInstruction(chunk, offset) 12 | } 13 | } 14 | 15 | disassembleInstruction :: proc(chunk: Chunk, offset: int) -> int { 16 | fmt.printf("%04d ", offset) 17 | if offset > 0 && chunk.lines[offset] == chunk.lines[offset - 1] { 18 | fmt.printf(" | ") 19 | } else { 20 | fmt.printf("%4d ", chunk.lines[offset]) 21 | } 22 | 23 | instruction := cast(OpCode)chunk.code[offset] 24 | switch instruction { 25 | case .CONSTANT: 26 | return constantInstruction(.CONSTANT, chunk, offset) 27 | case .NIL: 28 | return simpleInstruction(.NIL, offset) 29 | case .TRUE: 30 | return simpleInstruction(.TRUE, offset) 31 | case .FALSE: 32 | return simpleInstruction(.FALSE, offset) 33 | case .POP: 34 | return simpleInstruction(.POP, offset) 35 | case .GET_LOCAL: 36 | return byteInstruction(.GET_LOCAL, chunk, offset) 37 | case .SET_LOCAL: 38 | return byteInstruction(.SET_LOCAL, chunk, offset) 39 | case .GET_GLOBAL: 40 | return constantInstruction(.GET_GLOBAL, chunk, offset) 41 | case .DEFINE_GLOBAL: 42 | return constantInstruction(.DEFINE_GLOBAL, chunk, offset) 43 | case .SET_GLOBAL: 44 | return constantInstruction(.SET_GLOBAL, chunk, offset) 45 | case .GET_UPVALUE: 46 | return byteInstruction(.GET_UPVALUE, chunk, offset) 47 | case .SET_UPVALUE: 48 | return byteInstruction(.SET_UPVALUE, chunk, offset) 49 | case .GET_PROPERTY: 50 | return constantInstruction(.GET_PROPERTY, chunk, offset) 51 | case .SET_PROPERTY: 52 | return constantInstruction(.SET_PROPERTY, chunk, offset) 53 | case .GET_SUPER: 54 | return constantInstruction(.GET_SUPER, chunk, offset) 55 | case .EQUAL: 56 | return simpleInstruction(.EQUAL, offset) 57 | case .GREATER: 58 | return simpleInstruction(.GREATER, offset) 59 | case .LESS: 60 | return simpleInstruction(.LESS, offset) 61 | case .ADD: 62 | return simpleInstruction(.ADD, offset) 63 | case .SUBTRACT: 64 | return simpleInstruction(.SUBTRACT, offset) 65 | case .METHOD: 66 | return constantInstruction(.METHOD, chunk, offset) 67 | case .MULTIPLY: 68 | return simpleInstruction(.MULTIPLY, offset) 69 | case .DIVIDE: 70 | return simpleInstruction(.DIVIDE, offset) 71 | case .NOT: 72 | return simpleInstruction(.NOT, offset) 73 | case .NEGATE: 74 | return simpleInstruction(.NEGATE, offset) 75 | case .PRINT: 76 | return simpleInstruction(.PRINT, offset) 77 | case .JUMP: 78 | return jumpInstruction(.JUMP, 1, chunk, offset) 79 | case .JUMP_IF_FALSE: 80 | return jumpInstruction(.JUMP_IF_FALSE, 1, chunk, offset) 81 | case .LOOP: 82 | return jumpInstruction(.LOOP, -1, chunk, offset) 83 | case .CALL: 84 | return byteInstruction(.CALL, chunk, offset) 85 | case .INVOKE: 86 | return invokeInstruction(.INVOKE, chunk, offset) 87 | case .SUPER_INVOKE: 88 | return invokeInstruction(.SUPER_INVOKE, chunk, offset) 89 | case .CLOSURE: 90 | offset := offset 91 | offset += 1 92 | constant := chunk.code[offset] 93 | offset += 1 94 | fmt.printf("%-16s %4d ", "CLOSURE", constant) 95 | printValue(chunk.constants[constant]) 96 | fmt.println() 97 | 98 | function := cast(^ObjFunction) AS_OBJ(chunk.constants[constant]) 99 | for j in 0.. int { 123 | fmt.println(name) 124 | return offset + 1 125 | } 126 | 127 | @(private = "file") 128 | byteInstruction :: proc(name: OpCode, chunk: Chunk, offset: int) -> int { 129 | slot := chunk.code[offset + 1] 130 | buf: [32]u8 131 | name_str := fmt.bprintf(buf[:], "%v", name) 132 | fmt.printf("%-16v %v '\n", name_str, slot) 133 | return offset + 2 134 | } 135 | 136 | @(private = "file") 137 | jumpInstruction :: proc(name: OpCode, sign: int, chunk: Chunk, offset: int) -> int { 138 | jump := u16(chunk.code[offset + 1] << 8) 139 | jump |= u16(chunk.code[offset + 2]) 140 | fmt.printf("%-16v %4d -> %d\n", name, offset, offset + 3 + sign * int(jump)) 141 | return offset + 3 142 | } 143 | 144 | @(private = "file") 145 | constantInstruction :: proc(name: OpCode, chunk: Chunk, offset: int) -> int { 146 | constant := chunk.code[offset + 1] 147 | buf: [32]u8 148 | name_str := fmt.bprintf(buf[:], "%v", name) 149 | fmt.printf("%-16v %v '", name_str, constant) 150 | printValue(chunk.constants[constant]) 151 | fmt.printf("'\n") 152 | return offset + 2 153 | } 154 | 155 | invokeInstruction :: proc(name: OpCode, chunk: Chunk, offset: int) -> int { 156 | constant := chunk.code[offset + 1] 157 | argCount := chunk.code[offset + 2] 158 | fmt.printf("%-16v (%v args) %4v '", name, argCount, constant) 159 | printValue(chunk.constants[constant]) 160 | fmt.println("'") 161 | return offset + 3 162 | } 163 | 164 | -------------------------------------------------------------------------------- /src/scanner.odin: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "core:fmt" 4 | import "core:io" 5 | import "core:mem" 6 | import "core:strings" 7 | import "core:unicode" 8 | import "core:unicode/utf8" 9 | import "core:unicode/utf8/utf8string" 10 | 11 | String :: utf8string.String 12 | 13 | TokenType :: enum { 14 | // Single-character tokens. 15 | LEFT_PAREN, RIGHT_PAREN, 16 | LEFT_BRACE, RIGHT_BRACE, 17 | COMMA, DOT, MINUS, PLUS, 18 | SEMICOLON, SLASH, STAR, 19 | 20 | // One or two character tokens. 21 | BANG, BANG_EQUAL, 22 | EQUAL, EQUAL_EQUAL, 23 | GREATER, GREATER_EQUAL, 24 | LESS, LESS_EQUAL, 25 | 26 | // Literals 27 | IDENTIFIER, STRING, NUMBER, 28 | 29 | // Keywords 30 | AND, CLASS, ELSE, FALSE, 31 | FOR, FUN, IF, NIL, OR, 32 | PRINT, RETURN, SUPER, THIS, 33 | TRUE, VAR, WHILE, 34 | 35 | ERROR, EOF, 36 | } 37 | 38 | Scanner :: struct { 39 | buf: String, 40 | start: int, 41 | current: int, 42 | line: int, 43 | } 44 | 45 | Token :: struct { 46 | type: TokenType, 47 | value: string, 48 | line: int, 49 | } 50 | 51 | scanner: Scanner 52 | 53 | initScanner :: proc(source: string) { 54 | utf8string.init(&scanner.buf, source) 55 | scanner.start = 0 56 | scanner.current = 0 57 | scanner.line = 1 58 | } 59 | 60 | scanToken :: proc() -> Token { 61 | skipWhitespace() 62 | scanner.start = scanner.current 63 | if isAtEnd() { 64 | return makeToken(.EOF) 65 | } 66 | 67 | c := advance() 68 | if unicode.is_letter(c) { return identifier() } 69 | if unicode.is_digit(c) { return numberLiteral() } 70 | switch c { 71 | case '(': return makeToken(.LEFT_PAREN) 72 | case ')': return makeToken(.RIGHT_PAREN) 73 | case '{': return makeToken(.LEFT_BRACE) 74 | case '}': return makeToken(.RIGHT_BRACE) 75 | case ';': return makeToken(.SEMICOLON) 76 | case ',': return makeToken(.COMMA) 77 | case '.': return makeToken(.DOT) 78 | case '-': return makeToken(.MINUS) 79 | case '+': return makeToken(.PLUS) 80 | case '/': return makeToken(.SLASH) 81 | case '*': return makeToken(.STAR) 82 | case '!': return makeToken(.BANG_EQUAL if match('=') else .BANG) 83 | case '=': return makeToken(.EQUAL_EQUAL if match('=') else .EQUAL) 84 | case '<': return makeToken(.LESS_EQUAL if match('=') else .LESS) 85 | case '>': return makeToken(.GREATER_EQUAL if match('=') else .GREATER) 86 | case '"': return stringLiteral() 87 | } 88 | 89 | return errorToken("Unexpected character.") 90 | } 91 | 92 | isAtEnd :: proc() -> bool { 93 | return scanner.current == utf8string.len(&scanner.buf)-1 94 | } 95 | 96 | makeToken :: proc(type: TokenType) -> (token: Token) { 97 | token.type = type 98 | token.value = utf8string.slice(&scanner.buf, scanner.start, scanner.current) 99 | token.line = scanner.line 100 | return 101 | } 102 | 103 | errorToken :: proc(error_message: string) -> (token: Token) { 104 | token.type = .ERROR 105 | token.value = error_message 106 | token.line = scanner.line 107 | return 108 | } 109 | 110 | skipWhitespace :: proc() { 111 | for { 112 | if isAtEnd() { return } 113 | c := peek() 114 | switch c { 115 | case ' ', '\r', '\t': advance() 116 | case '\n': { 117 | scanner.line += 1 118 | advance() 119 | } 120 | case '/': { 121 | nextChar, err := peekNext() 122 | if err == nil { 123 | if nextChar == '/' { 124 | // A comment goes until the end of the line. 125 | for peek() != '\n' && !isAtEnd() { advance() } 126 | } else { 127 | return 128 | } 129 | } else { return } 130 | } 131 | case: return 132 | } 133 | } 134 | } 135 | 136 | checkKeyword :: proc(keyword: string, type: TokenType) -> TokenType { 137 | slice := utf8string.slice(&scanner.buf, scanner.start, scanner.start + len(keyword)) 138 | if strings.compare(string(slice), keyword) == 0 { 139 | return type 140 | } 141 | 142 | return .IDENTIFIER 143 | } 144 | 145 | identifierType :: proc() -> TokenType { 146 | switch utf8string.at(&scanner.buf, scanner.start) { 147 | case 'a': return checkKeyword("and", .AND) 148 | case 'c': return checkKeyword("class", .CLASS) 149 | case 'e': return checkKeyword("else", .ELSE) 150 | case 'f': { 151 | if scanner.current - scanner.start > 1 { 152 | switch utf8string.at(&scanner.buf, scanner.start + 1) { 153 | case 'a': return checkKeyword("false", .FALSE) 154 | case 'o': return checkKeyword("for", .FOR) 155 | case 'u': return checkKeyword("fun", .FUN) 156 | } 157 | } 158 | } 159 | case 'i': return checkKeyword("if", .IF) 160 | case 'n': return checkKeyword("nil", .NIL) 161 | case 'o': return checkKeyword("or", .OR) 162 | case 'p': return checkKeyword("print", .PRINT) 163 | case 'r': return checkKeyword("return", .RETURN) 164 | case 's': return checkKeyword("super", .SUPER) 165 | case 't': { 166 | if scanner.current - scanner.start > 1 { 167 | switch utf8string.at(&scanner.buf, scanner.start + 1) { 168 | case 'h': return checkKeyword("this", .THIS) 169 | case 'r': return checkKeyword("true", .TRUE) 170 | } 171 | } 172 | } 173 | case 'v': return checkKeyword("var", .VAR) 174 | case 'w': return checkKeyword("while", .WHILE) 175 | } 176 | return .IDENTIFIER 177 | 178 | } 179 | 180 | identifier :: proc() -> Token { 181 | for unicode.is_letter(peek()) || unicode.is_digit(peek()) { 182 | advance() 183 | } 184 | return makeToken(identifierType()) 185 | } 186 | 187 | numberLiteral :: proc() -> Token { 188 | for unicode.is_digit(rune(peek())) { advance() } 189 | 190 | // Look for a fractional part. 191 | nextChar, _ := peekNext() 192 | if peek() == '.' && unicode.is_digit(rune(nextChar)) { 193 | // Consume the ".". 194 | advance() 195 | 196 | for unicode.is_digit(rune(peek())) { advance() } 197 | } 198 | 199 | return makeToken(.NUMBER) 200 | 201 | } 202 | 203 | stringLiteral :: proc() -> Token { 204 | for peek() != '"' && !isAtEnd() { 205 | if peek() == '\n' { scanner.line += 1 } 206 | advance() 207 | } 208 | 209 | if isAtEnd() { return errorToken("Unterminated string.") } 210 | 211 | // The closing quote. 212 | advance() 213 | return makeToken(.STRING) 214 | } 215 | 216 | @(private = "file") 217 | advance :: proc() -> rune { 218 | scanner.current += 1 219 | return utf8string.at(&scanner.buf, scanner.current-1) 220 | } 221 | 222 | @(private = "file") 223 | peek :: proc() -> rune { 224 | return utf8string.at(&scanner.buf, scanner.current) 225 | } 226 | 227 | peekNext :: proc() -> (rune, io.Error) { 228 | if isAtEnd() { return ' ', io.Error.EOF } 229 | return utf8string.at(&scanner.buf, scanner.current+1), nil 230 | } 231 | 232 | @(private = "file") 233 | match :: proc(expected: rune) -> bool { 234 | if isAtEnd() { return false } 235 | if utf8string.at(&scanner.buf, scanner.current) != expected { return false } 236 | scanner.current += 1 237 | return true 238 | } 239 | -------------------------------------------------------------------------------- /src/object.odin: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "core:log" 4 | import "core:fmt" 5 | import "core:strings" 6 | 7 | ObjType :: enum { 8 | BOUND_METHOD, 9 | CLASS, 10 | CLOSURE, 11 | FUNCTION, 12 | INSTANCE, 13 | NATIVE, 14 | STRING, 15 | UPVALUE, 16 | } 17 | 18 | Obj :: struct { 19 | type: ObjType, 20 | isMarked: bool, 21 | next: ^Obj, 22 | } 23 | 24 | ObjFunction :: struct { 25 | using obj: Obj, 26 | arity: u32, 27 | upvalueCount: int, 28 | chunk: Chunk, 29 | name: ^ObjString, 30 | } 31 | 32 | NativeFn :: proc (argCount: u8, args: []Value) -> Value 33 | 34 | ObjNative :: struct { 35 | using obj: Obj, 36 | function: NativeFn, 37 | } 38 | 39 | ObjString :: struct { 40 | using obj: Obj, 41 | str: string, 42 | hash: u32, 43 | } 44 | 45 | ObjUpvalue :: struct { 46 | using obj: Obj, 47 | location: ^Value, 48 | closed: Value, 49 | nextUpvalue: ^ObjUpvalue, 50 | } 51 | 52 | ObjClass :: struct { 53 | using obj: Obj, 54 | name: ^ObjString, 55 | methods: Table, 56 | } 57 | 58 | ObjInstance :: struct { 59 | using obj: Obj, 60 | klass: ^ObjClass, 61 | fields: Table, 62 | } 63 | 64 | ObjClosure :: struct { 65 | using obj: Obj, 66 | function: ^ObjFunction, 67 | upvalues: [dynamic]^ObjUpvalue, 68 | upvalueCount: int, 69 | } 70 | 71 | ObjBoundMethod :: struct { 72 | using obj: Obj, 73 | receiver: Value, 74 | method: ^ObjClosure, 75 | } 76 | 77 | newBoundMethod :: proc(receiver: Value, method: ^ObjClosure) -> ^ObjBoundMethod { 78 | bound := allocateObject(ObjBoundMethod, .BOUND_METHOD) 79 | bound.receiver = receiver 80 | bound.method = method 81 | return bound 82 | } 83 | 84 | newFunction :: proc() -> ^ObjFunction { 85 | function := allocateObject(ObjFunction, .FUNCTION) 86 | 87 | return function 88 | } 89 | 90 | newInstance :: proc(klass: ^ObjClass) -> ^ObjInstance { 91 | instance := allocateObject(ObjInstance, .INSTANCE) 92 | instance.klass = klass 93 | return instance 94 | } 95 | 96 | newNative :: proc(function: NativeFn) -> ^ObjNative { 97 | native := allocateObject(ObjNative, .NATIVE) 98 | native.function = function 99 | return native 100 | } 101 | 102 | isObjType :: proc(value: Value, type: ObjType) -> bool { 103 | return IS_OBJ(value) && AS_OBJ(value).type == type 104 | } 105 | 106 | printObject :: proc(object: ^Obj) { 107 | switch object.type { 108 | case .BOUND_METHOD: printFunction((cast(^ObjBoundMethod) object).method.function) 109 | case .CLASS: fmt.printf("%v", (cast(^ObjClass) object).name.str) 110 | case .CLOSURE: printFunction((cast(^ObjClosure) object).function) 111 | case .FUNCTION: printFunction(cast(^ObjFunction) object) 112 | case .INSTANCE: fmt.printf("%v instance", (cast(^ObjInstance) object).klass.name.str) 113 | case .NATIVE: fmt.print("") 114 | case .STRING: fmt.printf("%v", (cast(^ObjString) object).str) 115 | case .UPVALUE: fmt.print("upvalue") 116 | case: fmt.print(object) 117 | } 118 | } 119 | 120 | copyString :: proc(str: string) -> ^ObjString { 121 | s := strings.clone(str) 122 | hash := hashString(s) 123 | 124 | interned := tableFindString(&vm.strings, s, hash) 125 | if interned != nil { return interned } 126 | 127 | return allocateString(s, hash) 128 | } 129 | 130 | newUpvalue :: proc(slot: ^Value) -> ^ObjUpvalue { 131 | upvalue := allocateObject(ObjUpvalue, .UPVALUE) 132 | upvalue.closed = NIL_VAL() 133 | upvalue.location = slot 134 | return upvalue 135 | } 136 | 137 | printFunction :: proc(function: ^ObjFunction) { 138 | if (function.name == nil) { 139 | fmt.printf("