├── .gitattributes ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.zig ├── build.zig.zon ├── docs └── grammar ├── examples ├── bog_from_c.c ├── zig_from_bog.bog └── zig_from_bog.zig ├── include └── bog.h ├── src ├── Bytecode.zig ├── Compiler.zig ├── Gc.zig ├── List.zig ├── Map.zig ├── String.zig ├── Tree.zig ├── Vm.zig ├── bog.zig ├── lib.zig ├── main.zig ├── multi_array_list.zig ├── parser.zig ├── render.zig ├── repl.zig ├── std.zig ├── std │ ├── debug.zig │ ├── fs.zig │ ├── io.zig │ ├── json.zig │ ├── map.zig │ ├── math.zig │ └── os.zig ├── tokenizer.zig └── value.zig └── tests ├── behavior.zig ├── error.zig └── fmt.zig /.gitattributes: -------------------------------------------------------------------------------- 1 | *.zig text eol=lf 2 | lib/ linguist-vendored 3 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | jobs: 4 | test: 5 | strategy: 6 | matrix: 7 | os: [ubuntu-latest, macos-latest, windows-latest] 8 | runs-on: ${{matrix.os}} 9 | steps: 10 | - uses: actions/checkout@v3 11 | - uses: mlugg/setup-zig@v1 12 | with: 13 | version: master 14 | - run: zig build test 15 | fmt: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v3 19 | - uses: mlugg/setup-zig@v1 20 | with: 21 | version: master 22 | - run: zig fmt --check build.zig src -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .zig-cache/ 2 | zig-out/ 3 | examples/bin 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Vexu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Small, strongly typed, embeddable language. 2 | ## [Examples](examples) 3 | 4 | ### Hello world 5 | ```julia 6 | let {print} = import "std.io" 7 | let world = "world" 8 | print(f"hello {world}!") 9 | ``` 10 | 11 | ### Async/await 12 | ```julia 13 | let {print} = import "std.io" 14 | 15 | let foo = fn() 16 | print("foo started") 17 | let bar_frame = async bar() 18 | print("in foo") 19 | let bar_res = await bar_frame 20 | print("foo finished") 21 | return bar_res 22 | 23 | let bar = fn() 24 | print("bar started") 25 | suspend 26 | print("bar resumed") 27 | suspend 28 | print("bar finished") 29 | return 1 30 | 31 | 32 | print("main started") 33 | let foo_frame = async foo() 34 | print("in main") 35 | let res = await foo_frame 36 | print("main finished:", res) 37 | ``` 38 | ```sh-session 39 | $ bog async.bog 40 | main started 41 | foo started 42 | bar started 43 | in foo 44 | bar resumed 45 | in main 46 | bar finished 47 | foo finished 48 | main finished: 1 49 | ``` 50 | 51 | ### Calculator 52 | ```julia 53 | let {input, print} = import "std.io" 54 | 55 | try 56 | let val1 = input("first argument: ") as num 57 | let op = input("operation: ") 58 | let val2 = input("second argument: ") as num 59 | 60 | match op 61 | "*" => print(val1 * val2) 62 | "+" => print(val1 + val2) 63 | "-" => print(val1 - val2) 64 | "/" => print(val1 / val2) 65 | "**" => print(val1 ** val2) 66 | _ => print(f"unknown op: {op}") 67 | catch 68 | print("that's not a number") 69 | ``` 70 | 71 | ### Use command line arguments 72 | ```julia 73 | # run with `path/to/bog path/here.bog arg1 arg2 "foo"` 74 | let {print} = import "std.io" 75 | print(import "args") 76 | ``` 77 | 78 | ### Loops 79 | ```julia 80 | let mut sum = 0 81 | for let c in "hellö wörld" 82 | match c 83 | "h" => sum += 1 84 | "e" => sum += 2 85 | "l" => sum += 3 86 | "ö" => sum += 4 87 | "w" => sum += 5 88 | "d" => sum += 6 89 | 90 | return sum # 31 91 | ``` 92 | ```julia 93 | let getSome = fn(val) if (val != 0) val - 1 94 | 95 | let mut val = 10 96 | while let newVal = getSome(val) 97 | val = newVal 98 | return val # 0 99 | ``` 100 | 101 | ### Error handling 102 | ```julia 103 | let {input, print} = import "std.io" 104 | 105 | let fails_on_1 = fn(arg) if arg == 1 error(69) 106 | let fails_on_2 = fn(arg) if arg == 2 error(42) 107 | let fails_on_3 = fn(arg) if arg == 3 error(17) 108 | 109 | let foo = fn(arg) 110 | try 111 | fails_on_1(arg) 112 | fails_on_2(arg) 113 | fails_on_3(arg) 114 | catch let err 115 | return err 116 | 117 | return 99 118 | 119 | print(for let i in 0:4 foo(i)) # [99, 69, 42, 17] 120 | print(try fails_on_1(input("give number: ") as int) catch "gave 1") 121 | ``` 122 | 123 | ### Destructuring assignment 124 | ```julia 125 | let add = fn ((a,b)) a + b 126 | let tuplify = fn (a,b) (a,b) 127 | return add(tuplify(1,2)) # 3 128 | ``` 129 | 130 | ## Embed 131 | ```zig 132 | const bog = @import("bog"); 133 | 134 | var vm = bog.Vm.init(allocator, .{ .import_files = true }); 135 | defer vm.deinit(); 136 | try vm.addStd(); 137 | 138 | const res = vm.run(source) catch |e| switch (e) { 139 | else => |err| return err, 140 | error.TokenizeError, error.ParseError, error.CompileError => { 141 | try vm.errors.render(source, out_stream); 142 | return error.RunningBogFailed; 143 | }, 144 | }; 145 | 146 | const bog_bool = try res.bogToZig(bool, &vm); 147 | ``` 148 | 149 | ### Calling Bog functions from Zig 150 | 151 | ```zig 152 | var vm = Vm.init(allocator, .{}); 153 | defer vm.deinit(); 154 | 155 | const res = vm.run(source) catch |e| switch (e) { 156 | else => |err| return err, 157 | error.TokenizeError, error.ParseError, error.CompileError => { 158 | try vm.errors.render(source, out_stream); 159 | return error.RunningBogFailed; 160 | }, 161 | }; 162 | 163 | const call_res = vm.call(res, "bogFunction", .{1, true}) catch |e| switch (e) { 164 | else => |err| return err, 165 | error.TokenizeError, error.ParseError, error.CompileError => { 166 | try vm.errors.render(source, out_stream); 167 | return error.CallingBogFunctionFailed; 168 | }, 169 | }; 170 | 171 | const bog_integer = try call_res.bogToZig(i64, &vm); 172 | ``` 173 | 174 | ### Calling Zig functions from Bog 175 | 176 | ```zig 177 | const my_lib = struct { 178 | pub fn pow(val: i64) i64 { 179 | return val * val; 180 | } 181 | }; 182 | 183 | var vm = Vm.init(allocator, .{}); 184 | defer vm.deinit(); 185 | try vm.addPackage("my_lib", my_lib); 186 | 187 | const res = vm.run(source) catch |e| switch (e) { 188 | else => |err| return err, 189 | error.TokenizeError, error.ParseError, error.CompileError => { 190 | try vm.errors.render(source, out_stream); 191 | return error.RunningBogFailed; 192 | }, 193 | }; 194 | 195 | const bog_integer = try res.bogToZig(i64, &vm); 196 | std.debug.assert(bog_integer == 8); 197 | ``` 198 | 199 | ```julia 200 | let {pow} = import "my_lib" 201 | 202 | return 2 * pow(2) 203 | ``` 204 | 205 | ## Setup 206 | * Download master version of Zig from https://ziglang.org/download/ 207 | * Clone this repo 208 | * Build with `zig build` 209 | * Run with `./zig-cache/bin/bog` 210 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Build = std.Build; 3 | 4 | pub fn build(b: *Build) void { 5 | const target = b.standardTargetOptions(.{}); 6 | const optimize = b.standardOptimizeOption(.{}); 7 | 8 | const linenoise = b.dependency("linenoise", .{ 9 | .target = target, 10 | .optimize = optimize, 11 | }); 12 | 13 | const bog_module = b.createModule(.{ 14 | .root_source_file = b.path("src/bog.zig"), 15 | .imports = &.{ 16 | .{ 17 | .name = "linenoise", 18 | .module = linenoise.module("linenoise"), 19 | }, 20 | }, 21 | }); 22 | 23 | const lib_options = b.addOptions(); 24 | lib_options.addOption( 25 | bool, 26 | "no_std", 27 | b.option(bool, "NO_ADD_STD", "Do not export bog_Vm_addStd to reduce binary size") orelse false, 28 | ); 29 | lib_options.addOption( 30 | bool, 31 | "no_std_no_io", 32 | b.option(bool, "NO_ADD_STD_NO_IO", "Do not export bog_Vm_addStd to reduce binary size") orelse false, 33 | ); 34 | 35 | const lib = b.addStaticLibrary(.{ 36 | .name = "bog", 37 | .optimize = optimize, 38 | .target = target, 39 | .root_source_file = b.path("src/lib.zig"), 40 | .link_libc = true, 41 | }); 42 | lib.root_module.addOptions("build_options", lib_options); 43 | 44 | const lib_step = b.step("lib", "Build C library"); 45 | lib_step.dependOn(&b.addInstallArtifact(lib, .{}).step); 46 | 47 | // c library usage example 48 | const c_example = b.addExecutable(.{ 49 | .name = "bog_from_c", 50 | .optimize = optimize, 51 | .target = target, 52 | .link_libc = true, 53 | }); 54 | c_example.addCSourceFile(.{ .file = b.path("examples/bog_from_c.c") }); 55 | 56 | c_example.addIncludePath(b.path("include")); 57 | c_example.linkLibrary(lib); 58 | c_example.step.dependOn(lib_step); 59 | 60 | // calling zig from bog example 61 | const zig_from_bog = b.addExecutable(.{ 62 | .name = "zig_from_bog", 63 | .optimize = optimize, 64 | .target = target, 65 | .root_source_file = b.path("examples/zig_from_bog.zig"), 66 | }); 67 | zig_from_bog.root_module.addImport("bog", bog_module); 68 | 69 | const examples_step = b.step("examples", "Build all examples"); 70 | examples_step.dependOn(&b.addInstallArtifact(c_example, .{ .dest_dir = .{ .override = .{ .custom = "examples/bin" } } }).step); 71 | examples_step.dependOn(&b.addInstallArtifact(zig_from_bog, .{ .dest_dir = .{ .override = .{ .custom = "examples/bin" } } }).step); 72 | 73 | addTests(b, examples_step, bog_module, .{ 74 | "src/main.zig", 75 | "tests/fmt.zig", 76 | "tests/behavior.zig", 77 | "tests/error.zig", 78 | }); 79 | 80 | const exe = b.addExecutable(.{ 81 | .name = "bog", 82 | .optimize = optimize, 83 | .target = target, 84 | .root_source_file = b.path("src/main.zig"), 85 | }); 86 | exe.root_module.addImport("bog", bog_module); 87 | b.installArtifact(exe); 88 | 89 | const fmt_step = b.step("fmt", "Format all source files"); 90 | fmt_step.dependOn(&b.addFmt(.{ 91 | .paths = &.{ 92 | "build.zig", 93 | "src", 94 | "examples", 95 | "tests", 96 | }, 97 | }).step); 98 | 99 | const clean_step = b.step("clean", "Delete all artifacts created by zig build"); 100 | const rm_zig_cache = b.addRemoveDirTree(b.path(".zig-cache")); 101 | const rm_examples_bing = b.addRemoveDirTree(b.path("examples/bin")); 102 | clean_step.dependOn(&rm_zig_cache.step); 103 | clean_step.dependOn(&rm_examples_bing.step); 104 | } 105 | 106 | fn addTests(b: *Build, examples_step: *std.Build.Step, bog_module: *std.Build.Module, tests: anytype) void { 107 | const tests_step = b.step("test", "Run all tests"); 108 | tests_step.dependOn(examples_step); 109 | inline for (tests) |t| { 110 | var test_step = b.addTest(.{ .root_source_file = b.path(t) }); 111 | test_step.root_module.addImport("bog", bog_module); 112 | tests_step.dependOn(&test_step.step); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = .toy_lang, 3 | 4 | .version = "0.0.0", 5 | 6 | .fingerprint = 0xb6e52362c59e5b4a, // Changing this has security and trust implications. 7 | 8 | .minimum_zig_version = "0.14.0", 9 | 10 | .dependencies = .{ 11 | .linenoise = .{ 12 | .url = "git+https://github.com/joachimschmidt557/linenoize#8bf767663382624bd676b05829fa8d3975a05d88", 13 | .hash = "linenoize-0.1.0-J7HK8IfXAABxH-V4Bp2Q0G7P-nrOmq9g8LuQAoX3SjDy", 14 | }, 15 | }, 16 | .paths = .{ 17 | "LICENSE", 18 | "README.md", 19 | "build.zig", 20 | "build.zig.zon", 21 | "examples/", 22 | "include/", 23 | "src/", 24 | "lib/", 25 | }, 26 | } 27 | -------------------------------------------------------------------------------- /docs/grammar: -------------------------------------------------------------------------------- 1 | root : (stmt NL)* EOF 2 | 3 | stmt 4 | : decl 5 | | assign_expr 6 | 7 | decl : "let" primary_expr "=" block_or_expr 8 | 9 | assign_expr : expr (("=" | "+=" | "-=" | "*=" | "**=" | "/=" | "//=" | "%=" | "<<=" | ">>=" | "&=" | "|=" | "^=") block_or_expr)? 10 | 11 | block_or_expr : block | assign_expr 12 | 13 | block : NL (stmt NL)+ 14 | 15 | expr 16 | : jump_expr 17 | | fn 18 | | bool_expr 19 | 20 | jump_expr 21 | : "return" block_or_expr? 22 | | "break" 23 | | "continue" 24 | | "throw" block_or_expr 25 | | "suspend" 26 | | "resume" block_or_expr 27 | 28 | fn : "fn" "(" (primary_expr ",")* (primary_expr "..."?)? ")" block_or_expr 29 | 30 | bool_expr 31 | : "not" comparison_expr 32 | | comparison_expr ("or" comparison_expr)* 33 | | comparison_expr ("and" comparison_expr)* 34 | 35 | comparison_expr 36 | : range_expr (("<" | "<=" | ">" | ">="| "==" | "!=" | "in") range_expr)? 37 | | range_expr ("is" type_name)? 38 | 39 | type_name : "null" | "int" | "num" | "bool" | "str" | "tuple" | "map" | "list" | "error" | "range" | "fn" 40 | 41 | range_expr : bit_expr (":" bit_expr? (":" bit_expr)?)? 42 | 43 | bit_expr : shift_expr (("&" shift_expr)* | ("|" shift_expr)* | ("^" shift_expr)* 44 | 45 | shift_expr : add_expr (("<<" | ">>") add_expr)? 46 | 47 | add_expr : mul_expr (("-" | "+") mul_expr)* 48 | 49 | mul_expr : cast_expr (("*" | "/" | "//" | "%") cast_expr)* 50 | 51 | cast_expr : prefix_expr ("as" type_name)? 52 | 53 | prefix_expr : ("-" | "~" | "await" )? power_expr 54 | 55 | power_expr : suffix_expr ("**" power_expr)? 56 | 57 | suffix_expr 58 | : primary_expr suffix_op* 59 | | "async" primary_expr suffix_op* call_args 60 | 61 | suffix_op 62 | : "[" expr "]" 63 | | call_args 64 | | "." IDENTIFIER 65 | 66 | call_args : "(" (spread_expr ",")* spread_expr? ")" 67 | 68 | spread_expr : "..."? expr 69 | 70 | primary_expr 71 | : "mut"? IDENTIFIER 72 | | "_" 73 | | STRING 74 | | format_string 75 | | NUMBER 76 | | "true" 77 | | "false" 78 | | "this" 79 | | "null" 80 | | initializer 81 | | "error" initializer? 82 | | "@" IDENTIFIER initializer? 83 | | "import" block_or_expr 84 | | if 85 | | while 86 | | for 87 | | match 88 | | try 89 | 90 | format_string : FORMAT_START expr "="? (FORMAT expr "="?)* FORMAT_END 91 | 92 | initializer 93 | : "(" block_or_expr ")" 94 | | "(" (spread_expr ",")+ spread_expr? ")" 95 | | "{" (expr "=" expr ",")* (expr "=" expr)? "}" 96 | | "[" (spread_expr ",")* spread_expr? "]" 97 | 98 | if : "if" ("let" primary_expr "=")? expr block_or_expr ("else" block_or_expr)? 99 | 100 | while : "while" ("let" primary_expr "=")? expr block_or_expr 101 | 102 | for : "for" ("let" primary_expr "in")? expr block_or_expr 103 | 104 | match : "match" expr (NL match_case)+ NL 105 | 106 | match_case 107 | : "let" primary_expr "=>" block_or_expr 108 | | expr ("," expr)* ","? "=>" block_or_expr 109 | 110 | try : "try" block_or_expr ("catch" ("let" primary_expr | expr)? block_or_expr)* 111 | -------------------------------------------------------------------------------- /examples/bog_from_c.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "bog.h" 5 | 6 | char *read_file(char *name) { 7 | FILE *file = fopen(name, "r"); 8 | if (file == NULL) 9 | return NULL; 10 | 11 | fseek(file, 0, SEEK_END); 12 | long bytes = ftell(file); 13 | fseek(file, 0, SEEK_SET); 14 | 15 | char *buffer = (char *)malloc(bytes + 1); 16 | if (!buffer) { 17 | fclose(file); 18 | return NULL; 19 | } 20 | 21 | fread(buffer, sizeof(char), bytes, file); 22 | buffer[bytes] = '\0'; 23 | fclose(file); 24 | return buffer; 25 | } 26 | 27 | int main(int argc, char **argv) { 28 | if (argc != 2) { 29 | printf("expected file name\n"); 30 | return 1; 31 | } 32 | char *source = read_file(argv[1]); 33 | if (!source) { 34 | printf("cannot read file %s\n", argv[1]); 35 | return 1; 36 | } 37 | 38 | bog_Vm *vm; 39 | if (bog_Vm_init(&vm, true) != BOG_ERROR_NONE) 40 | return 1; 41 | 42 | if (bog_Vm_addStd(vm) != BOG_ERROR_NONE) 43 | goto error; 44 | 45 | bog_Value *res; 46 | if (bog_Vm_run(vm, &res, source) != BOG_ERROR_NONE) { 47 | bog_Vm_renderErrors(vm, source, stderr); 48 | goto error; 49 | } 50 | 51 | free(source); 52 | bog_Vm_deinit(vm); 53 | return 0; 54 | 55 | error: 56 | free(source); 57 | bog_Vm_deinit(vm); 58 | return 1; 59 | } -------------------------------------------------------------------------------- /examples/zig_from_bog.bog: -------------------------------------------------------------------------------- 1 | const pow = import("pow") 2 | 3 | return 2 * pow(2) 4 | -------------------------------------------------------------------------------- /examples/zig_from_bog.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const bog = @import("bog"); 3 | 4 | pub fn pow(val: i64) i64 { 5 | return val * val; 6 | } 7 | 8 | pub fn main() !void { 9 | var state = std.heap.GeneralPurposeAllocator(.{}){}; 10 | const allocator = state.allocator(); 11 | 12 | var vm = bog.Vm.init(allocator, .{}); 13 | defer vm.deinit(); 14 | try vm.addPackage("pow", pow); 15 | 16 | const path = "examples/zig_from_bog.bog"; 17 | var mod = mod: { 18 | const source = try std.fs.cwd().readFileAlloc(vm.gc.gpa, path, vm.options.max_import_size); 19 | errdefer vm.gc.gpa.free(source); 20 | 21 | break :mod bog.compile(vm.gc.gpa, source, path, &vm.errors) catch |e| switch (e) { 22 | else => |err| return err, 23 | error.TokenizeError, error.ParseError, error.CompileError => { 24 | try vm.errors.render(std.io.getStdErr().writer()); 25 | return error.RunningBogFailed; 26 | }, 27 | }; 28 | }; 29 | errdefer mod.deinit(vm.gc.gpa); 30 | 31 | var frame = bog.Vm.Frame{ 32 | .this = bog.Value.Null, 33 | .mod = &mod, 34 | .body = mod.main, 35 | .caller_frame = null, 36 | .module_frame = undefined, 37 | .captures = &.{}, 38 | .params = 0, 39 | }; 40 | defer frame.deinit(&vm); 41 | frame.module_frame = &frame; 42 | 43 | vm.gc.stack_protect_start = @frameAddress(); 44 | 45 | const frame_val = try vm.gc.alloc(.frame); 46 | frame_val.* = .{ .frame = &frame }; 47 | defer frame_val.* = .{ .int = 0 }; // clear frame 48 | 49 | const res = try vm.run(&frame); 50 | 51 | const bog_integer = try res.bogToZig(i64, frame.ctx(&vm)); 52 | std.debug.assert(bog_integer == 8); 53 | } 54 | -------------------------------------------------------------------------------- /include/bog.h: -------------------------------------------------------------------------------- 1 | #ifndef BOG_H 2 | #define BOG_H 3 | 4 | #include 5 | #include 6 | 7 | typedef struct bog_Vm bog_Vm; 8 | typedef struct bog_Value bog_Value; 9 | typedef struct bog_Errors bog_Errors; 10 | typedef struct bog_Tree bog_Tree; 11 | 12 | typedef enum bog_Error { 13 | BOG_ERROR_NONE, 14 | BOG_ERROR_OUT_OF_MEMORY, 15 | BOG_ERROR_TOKENIZE, 16 | BOG_ERROR_PARSE, 17 | BOG_ERROR_COMPILE, 18 | BOG_ERROR_RUNTIME, 19 | BOG_ERROR_NOT_A_MAP, 20 | BOG_ERROR_NO_SUCH_MEMBER, 21 | BOG_ERROR_NOT_A_FUNCTION, 22 | BOG_ERROR_INVALID_ARG_COUNT, 23 | BOG_ERROR_NATIVE_FUNCTIONS_UNSUPPORTED, 24 | BOG_ERROR_IO, 25 | } bog_Error; 26 | 27 | // possible errors: BOG_ERROR_OUT_OF_MEMORY 28 | bog_Error bog_Vm_init(bog_Vm **vm, bool import_files); 29 | void bog_Vm_deinit(bog_Vm *vm); 30 | 31 | // possible errors: BOG_ERROR_OUT_OF_MEMORY 32 | // build with NO_ADD_STD to exclude this and reduce binary size 33 | bog_Error bog_Vm_addStd(bog_Vm *vm); 34 | // build with NO_ADD_STD_NO_IO to exclude this and reduce binary size 35 | bog_Error bog_Vm_addStdNoIo(bog_Vm *vm); 36 | 37 | // possible errors: BOG_ERROR_OUT_OF_MEMORY, BOG_ERROR_TOKENIZE, BOG_ERROR_PARSE, BOG_ERROR_COMPILE, 38 | // BOG_ERROR_RUNTIME 39 | bog_Error bog_Vm_run(bog_Vm *vm, bog_Value **result, const char *source); 40 | 41 | // possible errors: BOG_ERROR_OUT_OF_MEMORY, BOG_ERROR_RUNTIME, 42 | // BOG_ERROR_NOT_A_MAP, BOG_ERROR_NO_SUCH_MEMBER, BOG_ERROR_NOT_A_FUNCTION, 43 | // BOG_ERROR_INVALID_ARG_COUNT, BOG_ERROR_NATIVE_FUNCTIONS_UNSUPPORTED 44 | bog_Error bog_Vm_call(bog_Vm *vm, bog_Value **result, bog_Value *container, const char *func_name); 45 | 46 | // possible errors: BOG_ERROR_IO 47 | bog_Error bog_Vm_renderErrors(bog_Vm *vm, const char *source, FILE *out); 48 | 49 | // possible errors: BOG_ERROR_OUT_OF_MEMORY 50 | bog_Error bog_Errors_init(bog_Errors **errors); 51 | void bog_Errors_deinit(bog_Errors *errors); 52 | 53 | // possible errors: BOG_ERROR_IO 54 | bog_Error bog_Errors_render(bog_Errors *errors, const char *source, FILE *out); 55 | 56 | // possible errors: BOG_ERROR_OUT_OF_MEMORY, BOG_ERROR_TOKENIZE, BOG_ERROR_PARSE 57 | bog_Error bog_parse(bog_Tree **tree, const char *source, bog_Errors *errors); 58 | void bog_Tree_deinit(bog_Tree *tree); 59 | 60 | // possible errors: BOG_ERROR_IO 61 | bog_Error bog_Tree_render(bog_Tree *tree, FILE *out, bool* changed); 62 | 63 | #endif // BOG_H 64 | -------------------------------------------------------------------------------- /src/Bytecode.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const mem = std.mem; 3 | const Allocator = mem.Allocator; 4 | const bog = @import("bog.zig"); 5 | const Node = bog.Node; 6 | const Type = bog.Type; 7 | 8 | const Bytecode = @This(); 9 | 10 | code: Inst.List.Slice, 11 | extra: []const Ref, 12 | 13 | main: []const u32, 14 | 15 | strings: []const u8, 16 | debug_info: DebugInfo, 17 | 18 | pub fn deinit(b: *Bytecode, gpa: Allocator) void { 19 | gpa.free(b.extra); 20 | gpa.free(b.main); 21 | gpa.free(b.strings); 22 | b.debug_info.lines.deinit(gpa); 23 | gpa.free(b.debug_info.path); 24 | gpa.free(b.debug_info.source); 25 | b.code.deinit(gpa); 26 | b.* = undefined; 27 | } 28 | 29 | pub const Ref = enum(u32) { 30 | _, 31 | pub fn format(ref: Ref, _: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void { 32 | var buf: [8]u8 = undefined; 33 | buf[0] = '%'; 34 | const end = std.fmt.formatIntBuf(buf[1..], @intFromEnum(ref), 10, .lower, .{}); 35 | try std.fmt.formatBuf(buf[0 .. end + 1], options, writer); 36 | } 37 | }; 38 | 39 | pub inline fn indexToRef(i: u64, params: u32) Ref { 40 | return @enumFromInt(i + params); 41 | } 42 | pub inline fn refToIndex(r: Ref, params: u32) u32 { 43 | return @intFromEnum(r) - params; 44 | } 45 | 46 | /// All integers are little endian 47 | pub const Inst = struct { 48 | op: Op, 49 | data: Data, 50 | 51 | pub const List = @import("multi_array_list.zig").MultiArrayList(Inst); 52 | 53 | pub const Op = enum(u8) { 54 | /// No operation. 55 | nop, 56 | 57 | // literal construction 58 | 59 | /// null, true, false 60 | primitive, 61 | /// integer literal 62 | int, 63 | /// number literal 64 | num, 65 | /// string literal 66 | str, 67 | 68 | // aggregate construction 69 | 70 | // use Data.extra 71 | build_tuple, 72 | build_list, 73 | build_map, 74 | // use Data.un 75 | build_error, 76 | build_error_null, 77 | /// uses Data.extra with 78 | /// extra[0] == operand 79 | /// extra[1] == str.offset 80 | build_tagged, 81 | /// uses Data.str 82 | build_tagged_null, 83 | /// uses Data.extra with 84 | /// uses Data.extra with 85 | /// extra[0] == args_len 86 | /// extra[1] == captures_len 87 | /// extra[2..][0..captures_len] == captures 88 | /// extra[2 + captures_len..] == body 89 | build_func, 90 | /// uses Data.bin 91 | build_range, 92 | /// uses Data.range 93 | build_range_step, 94 | 95 | /// import, uses Data.str 96 | import, 97 | 98 | /// discards Data.un, complains if it's an error 99 | discard, 100 | 101 | /// res = copy(operand) 102 | copy_un, 103 | /// lhs = copy(rhs) 104 | copy, 105 | /// lhs = rhs 106 | move, 107 | /// res = GLOBAL(operand) 108 | load_global, 109 | /// GLOBAL(lhs) = rhs 110 | store_global, 111 | /// res = CAPTURE(operand) 112 | load_capture, 113 | /// res = THIS 114 | load_this, 115 | 116 | // binary operators 117 | 118 | // numeric 119 | div_floor, 120 | div, 121 | mul, 122 | pow, 123 | rem, 124 | add, 125 | sub, 126 | 127 | // bitwise 128 | l_shift, 129 | r_shift, 130 | bit_and, 131 | bit_or, 132 | bit_xor, 133 | 134 | // comparisons 135 | equal, 136 | not_equal, 137 | less_than, 138 | less_than_equal, 139 | greater_than, 140 | greater_than_equal, 141 | 142 | /// lhs in rhs 143 | in, 144 | 145 | /// container(lhs).append(rhs) 146 | append, 147 | 148 | /// simple cast 149 | as, 150 | /// simple type check 151 | is, 152 | 153 | // unary operations 154 | negate, 155 | bool_not, 156 | bit_not, 157 | spread, 158 | @"await", 159 | 160 | /// uses Data.un, error(A) => A 161 | unwrap_error, 162 | /// uses Data.str, @tag(A) => A 163 | unwrap_tagged, 164 | /// same as `unwrap_tagged` but returns null if unable 165 | unwrap_tagged_or_null, 166 | 167 | /// uses Data.bin 168 | /// returns null if lhs is not tuple/list or if its len is not equal to @intFromEnum(rhs) 169 | check_len, 170 | /// same as `check_len` but return error on false 171 | assert_len, 172 | /// uses Data.bin with the rhs being how many elements the tuple/list 173 | /// must have at least. 174 | spread_dest, 175 | 176 | /// use Data.bin 177 | get, 178 | /// returns null if no 179 | get_or_null, 180 | /// Optimized version of `get` which turns the: 181 | /// %1 = int 0 182 | /// %2 = get %0 %1 183 | /// pattern into: 184 | /// %1 = get_int %0 0 185 | get_int, 186 | /// uses Data.range with 187 | /// start == container 188 | /// extra[0] == index 189 | /// extra[1] == value 190 | set, 191 | 192 | /// uses Data.jump_condition 193 | /// Operand is where the error value should be stored 194 | /// and offset is where the VM should jump to handle the error. 195 | push_err_handler, 196 | pop_err_handler, 197 | 198 | /// uses Data.jump 199 | jump, 200 | // use Data.jump_condition 201 | jump_if_true, 202 | jump_if_false, 203 | jump_if_null, 204 | /// if operand is not an error jumps, 205 | /// otherwise unwraps the error 206 | unwrap_error_or_jump, 207 | 208 | // use Data.un 209 | iter_init, 210 | // use Data.jump_condition 211 | iter_next, 212 | 213 | /// uses Data.extra with 214 | /// extra[0] == callee 215 | call, 216 | /// uses Data.bin, lhs(rhs) 217 | call_one, 218 | /// uses Data.un, operand() 219 | call_zero, 220 | /// Same as `call` but this is passed as the first argument. 221 | this_call, 222 | /// uses Data.bin, lhs(this) 223 | this_call_zero, 224 | 225 | // Async variants of various function call instructions. 226 | async_call_zero, 227 | async_call_one, 228 | async_call, 229 | async_this_call_zero, 230 | async_this_call, 231 | 232 | // use Data.un 233 | ret, 234 | ret_null, 235 | throw, 236 | @"suspend", 237 | @"resume", 238 | 239 | pub fn needsDebugInfo(op: Op) bool { 240 | return switch (op) { 241 | // zig fmt: off 242 | .call, .call_one, .call_zero, .this_call, .this_call_zero, .set, 243 | .get, .assert_len, .unwrap_tagged, .unwrap_error, .bit_not, 244 | .bool_not, .negate, .as, .in, .less_than, .less_than_equal, 245 | .greater_than, .greater_than_equal, .mul, .pow, .add, .sub, 246 | .l_shift, .r_shift, .bit_and, .bit_or, .bit_xor, .rem, .div, 247 | .div_floor, .import, .build_range_step, .build_range, 248 | .iter_init, .iter_next, .spread, .spread_dest, .get_int, 249 | .@"resume", .@"await", .async_call_zero, .async_call_one, 250 | .async_call, .async_this_call_zero, .async_this_call, => true, 251 | // zig fmt: on 252 | else => false, 253 | }; 254 | } 255 | 256 | pub fn hasResult(op: Op) bool { 257 | return switch (op) { 258 | // zig fmt: off 259 | .discard, .copy, .move, .append, .check_len, 260 | .assert_len, .set, .push_err_handler, .pop_err_handler, 261 | .jump, .jump_if_true, .jump_if_false, .jump_if_null, .ret, 262 | .ret_null, .throw, .spread, .spread_dest, .get_int, 263 | .@"suspend", .@"resume", .store_global => false, 264 | // zig fmt: on 265 | else => true, 266 | }; 267 | } 268 | }; 269 | 270 | pub const Data = union { 271 | none: void, 272 | primitive: enum { 273 | null, 274 | true, 275 | false, 276 | }, 277 | int: i64, 278 | num: f64, 279 | str: struct { 280 | offset: u32, 281 | len: u32, 282 | }, 283 | extra: struct { 284 | extra: u32, 285 | len: u32, 286 | }, 287 | range: struct { 288 | start: Ref, 289 | /// end = extra[extra] 290 | /// step = extra[extra + 1] 291 | extra: u32, 292 | }, 293 | bin: struct { 294 | lhs: Ref, 295 | rhs: Ref, 296 | }, 297 | bin_ty: struct { 298 | operand: Ref, 299 | ty: Type, 300 | }, 301 | un: Ref, 302 | jump: u32, 303 | jump_condition: struct { 304 | operand: Ref, 305 | offset: u32, 306 | }, 307 | }; 308 | 309 | comptime { 310 | if (!std.debug.runtime_safety) std.debug.assert(@sizeOf(Data) == @sizeOf(u64)); 311 | } 312 | }; 313 | 314 | pub const DebugInfo = struct { 315 | path: []const u8 = "", 316 | source: []const u8 = "", 317 | lines: Lines, 318 | 319 | pub const Lines = std.AutoHashMapUnmanaged(u32, u32); 320 | }; 321 | 322 | fn dumpLineCol(b: *Bytecode, byte_offset: u32) void { 323 | var start: u32 = 0; 324 | // find the start of the line which is either a newline or a splice 325 | var line_num: u32 = 1; 326 | var i: u32 = 0; 327 | while (i < byte_offset) : (i += 1) { 328 | if (b.debug_info.source[i] == '\n') { 329 | start = i + 1; 330 | line_num += 1; 331 | } 332 | } 333 | const col_num = byte_offset - start + 1; 334 | std.debug.print("{s}:{d}:{d}\n", .{ b.debug_info.path, line_num, col_num }); 335 | } 336 | 337 | pub fn dump(b: *Bytecode, body: []const u32, params: u32) void { 338 | const ops = b.code.items(.op); 339 | const data = b.code.items(.data); 340 | for (body, 0..) |i, inst| { 341 | if (ops[i] == .nop) continue; 342 | const ref = indexToRef(inst, params); 343 | if (ops[i].needsDebugInfo()) { 344 | dumpLineCol(b, b.debug_info.lines.get(@intCast(i)).?); 345 | } 346 | std.debug.print("{d:4} ", .{inst}); 347 | if (ops[i].hasResult()) { 348 | std.debug.print("{:4} = ", .{ref}); 349 | } else { 350 | std.debug.print(" ", .{}); 351 | } 352 | std.debug.print("{s} ", .{@tagName(ops[i])}); 353 | switch (ops[i]) { 354 | .nop => unreachable, 355 | .primitive => std.debug.print("{s}\n", .{@tagName(data[i].primitive)}), 356 | .int => std.debug.print("{d}\n", .{data[i].int}), 357 | .num => std.debug.print("{d}\n", .{data[i].num}), 358 | .str, .unwrap_tagged, .unwrap_tagged_or_null => { 359 | const str = b.strings[data[i].str.offset..][0..data[i].str.len]; 360 | std.debug.print("{s}\n", .{str}); 361 | }, 362 | .build_tuple => { 363 | const extra = b.extra[data[i].extra.extra..][0..data[i].extra.len]; 364 | std.debug.print("(", .{}); 365 | dumpList(extra); 366 | std.debug.print(")\n", .{}); 367 | }, 368 | .build_list => { 369 | const extra = b.extra[data[i].extra.extra..][0..data[i].extra.len]; 370 | std.debug.print("[", .{}); 371 | dumpList(extra); 372 | std.debug.print("]\n", .{}); 373 | }, 374 | .build_map => { 375 | const extra = b.extra[data[i].extra.extra..][0..data[i].extra.len]; 376 | std.debug.print("{{", .{}); 377 | var extra_i: u32 = 0; 378 | while (extra_i < extra.len) : (extra_i += 2) { 379 | if (extra_i != 0) std.debug.print(", ", .{}); 380 | std.debug.print("{} = {}", .{ extra[extra_i], extra[extra_i + 1] }); 381 | } 382 | std.debug.print("}}\n", .{}); 383 | }, 384 | .build_func => { 385 | const extra = b.extra[data[i].extra.extra..][0..data[i].extra.len]; 386 | const args = @intFromEnum(extra[0]); 387 | const captures_len = @intFromEnum(extra[1]); 388 | const fn_captures = extra[2..][0..captures_len]; 389 | const fn_body: []const u32 = @ptrCast(extra[2 + captures_len ..]); 390 | std.debug.print("\n\nfn(args: {d}, captures: [", .{args}); 391 | dumpList(fn_captures); 392 | std.debug.print("]) {{\n", .{}); 393 | b.dump(fn_body, args); 394 | std.debug.print("}}\n\n", .{}); 395 | }, 396 | .build_tagged_null => { 397 | const str = b.strings[data[i].str.offset..][0..data[i].str.len]; 398 | std.debug.print("@{s} = null\n", .{str}); 399 | }, 400 | .build_tagged => { 401 | const operand = b.extra[data[i].extra.extra]; 402 | const str_offset = @intFromEnum(b.extra[data[i].extra.extra + 1]); 403 | const str = b.strings[str_offset..][0..data[i].extra.len]; 404 | std.debug.print("@{s} = {}\n", .{ str, operand }); 405 | }, 406 | .build_range => std.debug.print("{}:{}\n", .{ data[i].bin.lhs, data[i].bin.rhs }), 407 | .build_range_step => { 408 | const start = data[i].range.start; 409 | const end = b.extra[data[i].range.extra]; 410 | const step = b.extra[data[i].range.extra + 1]; 411 | std.debug.print("{}:{}:{}\n", .{ start, end, step }); 412 | }, 413 | .set => { 414 | const container = data[i].range.start; 415 | const index = b.extra[data[i].range.extra]; 416 | const val = b.extra[data[i].range.extra + 1]; 417 | std.debug.print("{}[{}] = {}\n", .{ container, index, val }); 418 | }, 419 | .check_len, 420 | .assert_len, 421 | .spread_dest, 422 | => { 423 | const operand = data[i].bin.lhs; 424 | const len = @intFromEnum(data[i].bin.rhs); 425 | std.debug.print("{} {d}\n", .{ operand, len }); 426 | }, 427 | .load_global => std.debug.print("GLOBAL({})\n", .{data[i].un}), 428 | .store_global => { 429 | const load_inst = body[refToIndex(data[i].bin.lhs, params)]; 430 | std.debug.print("GLOBAL({}) = {}\n", .{ data[load_inst].un, data[i].bin.rhs }); 431 | }, 432 | .load_capture => std.debug.print("CAPTURE({d})\n", .{@intFromEnum(data[i].un)}), 433 | .copy, 434 | .move, 435 | .get, 436 | .get_or_null, 437 | .div_floor, 438 | .div, 439 | .mul, 440 | .pow, 441 | .rem, 442 | .add, 443 | .sub, 444 | .l_shift, 445 | .r_shift, 446 | .bit_and, 447 | .bit_or, 448 | .bit_xor, 449 | .equal, 450 | .not_equal, 451 | .less_than, 452 | .less_than_equal, 453 | .greater_than, 454 | .greater_than_equal, 455 | .in, 456 | => std.debug.print("{} {}\n", .{ data[i].bin.lhs, data[i].bin.rhs }), 457 | .get_int => std.debug.print("{} {}\n", .{ data[i].bin.lhs, @intFromEnum(data[i].bin.rhs) }), 458 | .append => std.debug.print("{}.append({})\n", .{ data[i].bin.lhs, data[i].bin.rhs }), 459 | .as, .is => std.debug.print("{} {s}\n", .{ data[i].bin_ty.operand, @tagName(data[i].bin_ty.ty) }), 460 | .ret, 461 | .throw, 462 | .@"resume", 463 | .@"await", 464 | .negate, 465 | .bool_not, 466 | .bit_not, 467 | .spread, 468 | .unwrap_error, 469 | .iter_init, 470 | .discard, 471 | .build_error, 472 | .copy_un, 473 | .import, 474 | => std.debug.print("{}\n", .{data[i].un}), 475 | .pop_err_handler, 476 | .jump, 477 | => std.debug.print("{d}\n", .{data[i].jump}), 478 | .jump_if_true, 479 | .jump_if_false, 480 | .unwrap_error_or_jump, 481 | .jump_if_null, 482 | .iter_next, 483 | .push_err_handler, 484 | => std.debug.print( 485 | "{d} cond {}\n", 486 | .{ data[i].jump_condition.offset, data[i].jump_condition.operand }, 487 | ), 488 | .call, .async_call => { 489 | const extra = b.extra[data[i].extra.extra..][0..data[i].extra.len]; 490 | std.debug.print("{}(", .{extra[0]}); 491 | dumpList(extra[1..]); 492 | std.debug.print(")\n", .{}); 493 | }, 494 | .this_call, .async_this_call => { 495 | const extra = b.extra[data[i].extra.extra..][0..data[i].extra.len]; 496 | std.debug.print("{}.{}(", .{ extra[1], extra[0] }); 497 | dumpList(extra[2..]); 498 | std.debug.print(")\n", .{}); 499 | }, 500 | .call_one, .async_call_one => std.debug.print("{}({})\n", .{ data[i].bin.lhs, data[i].bin.rhs }), 501 | .this_call_zero, .async_this_call_zero => std.debug.print("{}.{}()\n", .{ data[i].bin.rhs, data[i].bin.lhs }), 502 | .call_zero, .async_call_zero => std.debug.print("{}()\n", .{data[i].un}), 503 | .ret_null, 504 | .build_error_null, 505 | .load_this, 506 | .@"suspend", 507 | => std.debug.print("\n", .{}), 508 | } 509 | } 510 | } 511 | 512 | fn dumpList(list: []const Ref) void { 513 | for (list, 0..) |item, i| { 514 | if (i != 0) std.debug.print(", ", .{}); 515 | std.debug.print("{}", .{item}); 516 | } 517 | } 518 | -------------------------------------------------------------------------------- /src/Gc.zig: -------------------------------------------------------------------------------- 1 | //! A non-moving garbage collector. 2 | //! Inspired by https://www.pllab.riec.tohoku.ac.jp/papers/icfp2011UenoOhoriOtomoAuthorVersion.pdf 3 | const std = @import("std"); 4 | const mem = std.mem; 5 | const Allocator = mem.Allocator; 6 | const assert = std.debug.assert; 7 | const log = std.log.scoped(.gc); 8 | const bog = @import("bog.zig"); 9 | const Value = bog.Value; 10 | const Type = bog.Type; 11 | const expect = std.testing.expect; 12 | 13 | /// A pool of values prefixed with a header containing two bitmaps for 14 | /// the old and young generation. 15 | const Page = struct { 16 | const max_size = 1_048_576; 17 | comptime { 18 | // 2^20, 1 MiB 19 | assert(@sizeOf(Page) == max_size); 20 | } 21 | const val_count = @divFloor(max_size - @sizeOf(u32) * 2, (@sizeOf(Value) + @sizeOf(State))); 22 | const pad_size = max_size - @sizeOf(u32) * 2 - (@sizeOf(Value) + @sizeOf(State)) * val_count; 23 | 24 | const State = enum { 25 | empty, 26 | white, 27 | gray, 28 | black, 29 | }; 30 | 31 | const List = std.ArrayListUnmanaged(*Page); 32 | 33 | /// States of all values. 34 | meta: [val_count]State, 35 | /// Padding to ensure size is 1 MiB. 36 | __padding: [pad_size]u8, 37 | 38 | /// Index to the first free slot. 39 | free: u32, 40 | 41 | /// used during the collection phase to detect whether the Gc should keep 42 | /// checking values in this page. 43 | marked: u32, 44 | 45 | /// Actual values, all pointers will stay valid as long as they are 46 | /// referenced from a root. 47 | values: [val_count]Value, 48 | 49 | fn create() !*Page { 50 | const page = try std.heap.page_allocator.create(Page); 51 | @memset(mem.bytesAsSlice(usize, mem.asBytes(page)), 0); 52 | return page; 53 | } 54 | 55 | fn destroy(page: *Page, gc: *Gc) void { 56 | for (page.meta, 0..) |s, i| { 57 | if (s == .empty) continue; 58 | page.values[i].deinit(gc.gpa); 59 | } 60 | std.heap.page_allocator.destroy(page); 61 | } 62 | 63 | fn alloc(page: *Page) ?*Value { 64 | while (page.free < page.values.len) { 65 | defer page.free += 1; 66 | 67 | if (page.meta[page.free] == .empty) { 68 | page.meta[page.free] = .white; 69 | return &page.values[page.free]; 70 | } 71 | } 72 | return null; 73 | } 74 | 75 | fn clear(page: *Page, gc: *Gc) u32 { 76 | var freed: u32 = 0; 77 | var i: u32 = val_count; 78 | while (i > 0) { 79 | i -= 1; 80 | switch (page.meta[i]) { 81 | .black, .gray => { 82 | // value lives to see another day 83 | page.meta[i] = .white; 84 | }, 85 | .white => { 86 | freed += 1; 87 | page.meta[i] = .empty; 88 | page.values[i].deinit(gc.gpa); 89 | page.free = i; 90 | }, 91 | .empty => {}, 92 | } 93 | } 94 | return freed; 95 | } 96 | 97 | fn indexOf(page: *Page, value: *const Value) ?u32 { 98 | // is the value before this page 99 | if (@intFromPtr(value) < @intFromPtr(&page.values[0])) return null; 100 | // is the value after this page 101 | if (@intFromPtr(value) > @intFromPtr(&page.values[page.values.len - 1])) return null; 102 | 103 | // value is in this page 104 | return @intCast((@intFromPtr(value) - @intFromPtr(&page.values[0])) / @sizeOf(Value)); 105 | } 106 | }; 107 | 108 | const Gc = @This(); 109 | 110 | simple_pages: Page.List = .{}, 111 | aggregate_pages: Page.List = .{}, 112 | gpa: Allocator, 113 | page_limit: u32, 114 | stack_protect_start: usize = 0, 115 | allocated: u32 = 0, 116 | 117 | const PageAndIndex = struct { 118 | page: *Page, 119 | index: usize, 120 | }; 121 | 122 | pub fn markVal(gc: *Gc, maybe_value: ?*const Value) void { 123 | const value = maybe_value orelse return; 124 | // These will never be allocated 125 | if (value == Value.Null or 126 | value == Value.True or 127 | value == Value.False) return; 128 | 129 | for (gc.simple_pages.items) |page| { 130 | const index = page.indexOf(value) orelse continue; 131 | if (page.meta[index] == .white) { 132 | page.meta[index] = .black; 133 | } 134 | return; 135 | } 136 | 137 | for (gc.aggregate_pages.items) |page| { 138 | const index = page.indexOf(value) orelse continue; 139 | if (page.meta[index] == .white) { 140 | page.meta[index] = .gray; 141 | page.marked += 1; 142 | } 143 | return; 144 | } 145 | 146 | // Value was not allocated by gc. 147 | } 148 | 149 | fn markGray(gc: *Gc) void { 150 | // mark all pages as dirty 151 | for (gc.aggregate_pages.items) |page| { 152 | page.marked = Page.val_count; 153 | } 154 | 155 | // mark all white values reachable from gray values as gray 156 | var marked_any = true; 157 | while (marked_any) { 158 | marked_any = false; 159 | for (gc.aggregate_pages.items) |page| { 160 | if (page.marked == 0) continue; 161 | page.marked = 0; 162 | for (&page.meta, 0..) |*s, i| { 163 | if (s.* != .gray) continue; 164 | 165 | s.* = .black; 166 | switch (page.values[i]) { 167 | .list => |list| { 168 | for (list.inner.items) |val| { 169 | gc.markVal(val); 170 | } 171 | }, 172 | .tuple => |tuple| { 173 | for (tuple) |val| { 174 | gc.markVal(val); 175 | } 176 | }, 177 | .map => |map| { 178 | var iter = map.iterator(); 179 | while (iter.next()) |entry| { 180 | gc.markVal(entry.key_ptr.*); 181 | gc.markVal(entry.value_ptr.*); 182 | } 183 | }, 184 | .err => |err| { 185 | gc.markVal(err); 186 | }, 187 | .func => |func| { 188 | for (func.captures()) |val| { 189 | gc.markVal(val); 190 | } 191 | }, 192 | .frame => |frame| { 193 | for (frame.stack.items) |val| { 194 | gc.markVal(val); 195 | } 196 | for (frame.captures) |val| { 197 | gc.markVal(val); 198 | } 199 | gc.markVal(frame.this); 200 | }, 201 | .iterator => |iter| { 202 | gc.markVal(iter.value); 203 | }, 204 | .spread => |spread| { 205 | gc.markVal(spread.iterable); 206 | }, 207 | .tagged => |tag| { 208 | gc.markVal(tag.value); 209 | }, 210 | .native_val => |n| { 211 | if (n.vtable.markVal) |some| { 212 | some(n.ptr, gc); 213 | } 214 | }, 215 | // These values cannot be allocated in aggregate_pages 216 | .native, .str, .int, .num, .range, .null, .bool => {}, 217 | } 218 | } 219 | if (page.marked != 0) marked_any = true; 220 | } 221 | } 222 | } 223 | 224 | /// Collect all unreachable values. 225 | pub fn collect(gc: *Gc) usize { 226 | // mark roots as reachable 227 | if (gc.stack_protect_start != 0) { 228 | var i: [*]*Value = @ptrFromInt(gc.stack_protect_start); 229 | while (@intFromPtr(i) > @frameAddress()) : (i -= 1) { 230 | gc.markVal(i[0]); 231 | } 232 | } 233 | 234 | // mark values referenced from root values as reachable 235 | gc.markGray(); 236 | 237 | // free all unreachable values 238 | var freed: u32 = 0; 239 | for (gc.simple_pages.items) |page| { 240 | freed += page.clear(gc); 241 | } 242 | for (gc.aggregate_pages.items) |page| { 243 | freed += page.clear(gc); 244 | } 245 | log.info("collected {d} out of {d} objects ({d:.2}%)", .{ 246 | freed, 247 | gc.allocated, 248 | (@as(f32, @floatFromInt(freed)) / @as(f32, @floatFromInt(gc.allocated))) * 100, 249 | }); 250 | gc.allocated -= freed; 251 | return freed; 252 | } 253 | 254 | pub fn init(allocator: Allocator, page_limit: u32) Gc { 255 | std.debug.assert(page_limit >= 1); 256 | return .{ 257 | .gpa = allocator, 258 | .page_limit = page_limit, 259 | }; 260 | } 261 | 262 | /// Frees all values and their allocations. 263 | pub fn deinit(gc: *Gc) void { 264 | for (gc.aggregate_pages.items) |page| page.destroy(gc); 265 | gc.aggregate_pages.deinit(gc.gpa); 266 | for (gc.simple_pages.items) |page| page.destroy(gc); 267 | gc.simple_pages.deinit(gc.gpa); 268 | } 269 | 270 | /// Allocate a new Value on the heap. 271 | pub fn alloc(gc: *Gc, ty: Type) !*Value { 272 | switch (ty) { 273 | .null => unreachable, 274 | .bool => unreachable, 275 | .native, 276 | .str, 277 | .int, 278 | .num, 279 | .range, 280 | => return gc.allocExtra(&gc.simple_pages), 281 | .list, 282 | .tuple, 283 | .map, 284 | .err, 285 | .func, 286 | .frame, 287 | .iterator, 288 | .tagged, 289 | .spread, 290 | .native_val, 291 | => return gc.allocExtra(&gc.aggregate_pages), 292 | } 293 | } 294 | 295 | fn allocExtra(gc: *Gc, pages: *Page.List) !*Value { 296 | if (pages.items.len == 0) { 297 | const page = try Page.create(); 298 | errdefer page.destroy(gc); 299 | try pages.append(gc.gpa, page); 300 | 301 | // we just created this page so it is empty. 302 | gc.allocated += 1; 303 | return page.alloc() orelse unreachable; 304 | } 305 | 306 | for (pages.items) |page| { 307 | if (page.alloc()) |some| { 308 | gc.allocated += 1; 309 | return some; 310 | } 311 | } 312 | 313 | const freed = gc.collect(); 314 | 315 | const threshold = 0.75; 316 | const new_capacity = @as(f32, @floatFromInt(freed)) / @as(f32, @floatFromInt(gc.allocated)); 317 | 318 | if (new_capacity < threshold and pages.items.len != gc.page_limit) { 319 | log.info("collected {d}, allocating a new page", .{freed}); 320 | 321 | const page = try Page.create(); 322 | errdefer page.destroy(gc); 323 | try pages.append(gc.gpa, page); 324 | 325 | // we just created this page so it is empty. 326 | gc.allocated += 1; 327 | return page.alloc() orelse unreachable; 328 | } else if (freed != 0) { 329 | // we just freed a whole bunch of values, allocation cannot fail 330 | return gc.allocExtra(pages) catch unreachable; 331 | } 332 | 333 | // no values could be collected and page_limit has been reached 334 | return error.OutOfMemory; 335 | } 336 | 337 | /// Allocates a shallow copy of `val`. 338 | pub fn dupe(gc: *Gc, val: *const Value) !*Value { 339 | // no need to copy always memoized values 340 | if (val == Value.Null) return Value.Null; 341 | if (val == Value.True) return Value.True; 342 | if (val == Value.False) return Value.False; 343 | 344 | const new = try gc.alloc(val.*); 345 | switch (val.*) { 346 | .list => |*l| { 347 | new.* = .{ .list = .{} }; 348 | try new.list.inner.appendSlice(gc.gpa, l.inner.items); 349 | }, 350 | .tuple => |t| { 351 | new.* = .{ .tuple = try gc.gpa.dupe(*Value, t) }; 352 | }, 353 | .map => |*m| { 354 | new.* = .{ .map = try m.clone(gc.gpa) }; 355 | }, 356 | .str => |*s| { 357 | if (s.capacity != 0) { 358 | new.* = Value.string(try gc.gpa.dupe(u8, s.data)); 359 | } else { 360 | new.* = val.*; 361 | } 362 | }, 363 | else => new.* = val.*, 364 | } 365 | return new; 366 | } 367 | 368 | test "stack protect" { 369 | if (@import("builtin").os.tag == .windows) { 370 | // TODO @frameAddress returns an address after &val1 on windows? 371 | return error.SkipZigTest; 372 | } 373 | var gc = Gc.init(std.testing.allocator, 2); 374 | defer gc.deinit(); 375 | 376 | gc.stack_protect_start = @frameAddress(); 377 | 378 | _ = try gc.alloc(.int); 379 | _ = try gc.alloc(.int); 380 | 381 | try expect(gc.collect() == 0); 382 | 383 | gc.stack_protect_start = 0; 384 | try expect(gc.collect() == 2); 385 | } 386 | -------------------------------------------------------------------------------- /src/List.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const mem = std.mem; 3 | const Allocator = mem.Allocator; 4 | const bog = @import("bog.zig"); 5 | const Vm = bog.Vm; 6 | const Value = bog.Value; 7 | const Type = bog.Type; 8 | 9 | const List = @This(); 10 | 11 | inner: std.ArrayListUnmanaged(*Value) = .{}, 12 | 13 | pub fn deinit(l: *List, allocator: Allocator) void { 14 | l.inner.deinit(allocator); 15 | l.* = undefined; 16 | } 17 | 18 | pub fn eql(a: List, b: List) bool { 19 | if (a.inner.items.len != b.inner.items.len) return false; 20 | for (a.inner.items, 0..) |v, i| { 21 | if (!v.eql(b.inner.items[i])) return false; 22 | } 23 | return true; 24 | } 25 | 26 | pub fn get(list: *const List, ctx: Vm.Context, index: *const Value, res: *?*Value) Value.NativeError!void { 27 | switch (index.*) { 28 | .int => { 29 | var i = index.int; 30 | if (i < 0) 31 | i += @intCast(list.inner.items.len); 32 | if (i < 0 or i >= list.inner.items.len) 33 | return ctx.throw("index out of bounds"); 34 | 35 | res.* = list.inner.items[@intCast(i)]; 36 | }, 37 | .range => |r| { 38 | if (r.start < 0 or r.end > list.inner.items.len) 39 | return ctx.throw("index out of bounds"); 40 | 41 | res.* = try ctx.vm.gc.alloc(.list); 42 | res.*.?.* = .{ .list = .{} }; 43 | const res_list = &res.*.?.*.list; 44 | try res_list.inner.ensureUnusedCapacity(ctx.vm.gc.gpa, @intCast(r.count())); 45 | 46 | var it = r.iterator(); 47 | while (it.next()) |some| { 48 | res_list.inner.appendAssumeCapacity(list.inner.items[@intCast(some)]); 49 | } 50 | }, 51 | .str => |s| { 52 | if (res.* == null) { 53 | res.* = try ctx.vm.gc.alloc(.int); 54 | } 55 | 56 | if (mem.eql(u8, s.data, "len")) { 57 | res.*.?.* = .{ .int = @intCast(list.inner.items.len) }; 58 | } else inline for (@typeInfo(methods).@"struct".decls) |method| { 59 | if (mem.eql(u8, s.data, method.name)) { 60 | res.* = try Value.zigFnToBog(ctx.vm, @field(methods, method.name)); 61 | return; 62 | } 63 | } else { 64 | return ctx.throw("no such property"); 65 | } 66 | }, 67 | else => return ctx.throw("invalid index type"), 68 | } 69 | } 70 | 71 | pub const methods = struct { 72 | fn append(list: Value.This(*List), ctx: Vm.Context, vals: Value.Variadic(*Value)) !void { 73 | try list.t.inner.appendSlice(ctx.vm.gc.gpa, vals.t); 74 | } 75 | }; 76 | 77 | pub fn set(list: *List, ctx: Vm.Context, index: *const Value, new_val: *Value) Value.NativeError!void { 78 | switch (index.*) { 79 | .int => { 80 | var i = index.int; 81 | if (i < 0) 82 | i += @intCast(list.inner.items.len); 83 | if (i < 0 or i >= list.inner.items.len) 84 | return ctx.throw("index out of bounds"); 85 | 86 | list.inner.items[@intCast(i)] = new_val; 87 | }, 88 | .range => |r| { 89 | if (r.start < 0 or r.end > list.inner.items.len) 90 | return ctx.throw("index out of bounds"); 91 | 92 | var it = r.iterator(); 93 | while (it.next()) |some| { 94 | list.inner.items[@intCast(some)] = new_val; 95 | } 96 | }, 97 | else => return ctx.throw("invalid index type"), 98 | } 99 | } 100 | 101 | pub fn as(list: *List, ctx: Vm.Context, type_id: Type) Vm.Error!*Value { 102 | _ = list; 103 | _ = type_id; 104 | return ctx.frame.fatal(ctx.vm, "TODO cast to list"); 105 | } 106 | 107 | pub fn from(val: *Value, ctx: Vm.Context) Vm.Error!*Value { 108 | _ = val; 109 | return ctx.frame.fatal(ctx.vm, "TODO cast from list"); 110 | } 111 | 112 | pub fn in(list: *const List, val: *const Value) bool { 113 | for (list.inner.items) |v| { 114 | if (v.eql(val)) return true; 115 | } 116 | return false; 117 | } 118 | -------------------------------------------------------------------------------- /src/Map.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const debug = std.debug; 3 | const assert = debug.assert; 4 | const testing = std.testing; 5 | const math = std.math; 6 | const mem = std.mem; 7 | const meta = std.meta; 8 | const trait = meta.trait; 9 | const autoHash = std.hash.autoHash; 10 | const Wyhash = std.hash.Wyhash; 11 | const Allocator = mem.Allocator; 12 | const Value = @import("bog.zig").Value; 13 | 14 | /// It is permitted to access this field directly. 15 | entries: DataList = .{}, 16 | 17 | /// When entries length is less than `linear_scan_max`, this remains `null`. 18 | /// Once entries length grows big enough, this field is allocated. There is 19 | /// an IndexHeader followed by an array of Index(I) structs, where I is defined 20 | /// by how many total indexes there are. 21 | index_header: ?*IndexHeader = null, 22 | 23 | /// Modifying the key is allowed only if it does not change the hash. 24 | /// Modifying the value is allowed. 25 | /// Entry pointers become invalid whenever this ArrayHashMap is modified, 26 | /// unless `ensureTotalCapacity`/`ensureUnusedCapacity` was previously used. 27 | pub const Entry = struct { 28 | key_ptr: **const Value, 29 | value_ptr: **Value, 30 | }; 31 | 32 | /// A KV pair which has been copied out of the backing store 33 | pub const KV = struct { 34 | key: *const Value, 35 | value: *Value, 36 | }; 37 | 38 | /// The Data type used for the MultiArrayList backing this map 39 | pub const Data = struct { 40 | hash: Hash, 41 | key: *const Value, 42 | value: *Value, 43 | }; 44 | 45 | /// The MultiArrayList type backing this map 46 | pub const DataList = @import("multi_array_list.zig").MultiArrayList(Data); 47 | 48 | /// The stored hash type, either u32 or void. 49 | pub const Hash = u32; 50 | 51 | /// getOrPut variants return this structure, with pointers 52 | /// to the backing store and a flag to indicate whether an 53 | /// existing entry was found. 54 | /// Modifying the key is allowed only if it does not change the hash. 55 | /// Modifying the value is allowed. 56 | /// Entry pointers become invalid whenever this ArrayHashMap is modified, 57 | /// unless `ensureTotalCapacity`/`ensureUnusedCapacity` was previously used. 58 | pub const GetOrPutResult = struct { 59 | key_ptr: **const Value, 60 | value_ptr: **Value, 61 | found_existing: bool, 62 | index: u32, 63 | }; 64 | 65 | const Map = @This(); 66 | 67 | const linear_scan_max = 8; 68 | 69 | const RemovalType = enum { 70 | swap, 71 | ordered, 72 | }; 73 | 74 | /// Frees the backing allocation and leaves the map in an undefined state. 75 | /// Note that this does not free keys or values. You must take care of that 76 | /// before calling this function, if it is needed. 77 | pub fn deinit(self: *Map, allocator: Allocator) void { 78 | self.entries.deinit(allocator); 79 | if (self.index_header) |header| { 80 | header.free(allocator); 81 | } 82 | self.* = undefined; 83 | } 84 | 85 | /// Returns the number of KV pairs stored in this map. 86 | pub fn count(self: Map) u32 { 87 | return self.entries.len; 88 | } 89 | 90 | /// Returns the backing array of keys in this map. 91 | /// Modifying the map may invalidate this array. 92 | pub fn keys(self: Map) []*const Value { 93 | return self.entries.items(.key); 94 | } 95 | /// Returns the backing array of values in this map. 96 | /// Modifying the map may invalidate this array. 97 | pub fn values(self: Map) []*Value { 98 | return self.entries.items(.value); 99 | } 100 | 101 | /// Returns an iterator over the pairs in this map. 102 | /// Modifying the map may invalidate this iterator. 103 | pub fn iterator(self: Map) Iterator { 104 | const slice = self.entries.slice(); 105 | return .{ 106 | .keys = slice.items(.key).ptr, 107 | .values = slice.items(.value).ptr, 108 | .len = @intCast(slice.len), 109 | }; 110 | } 111 | pub const Iterator = struct { 112 | keys: [*]*const Value, 113 | values: [*]*Value, 114 | len: u32, 115 | index: u32 = 0, 116 | 117 | pub fn next(it: *Iterator) ?Entry { 118 | if (it.index >= it.len) return null; 119 | const result = Entry{ 120 | .key_ptr = &it.keys[it.index], 121 | .value_ptr = &it.values[it.index], 122 | }; 123 | it.index += 1; 124 | return result; 125 | } 126 | 127 | /// Reset the iterator to the initial index 128 | pub fn reset(it: *Iterator) void { 129 | it.index = 0; 130 | } 131 | }; 132 | 133 | /// If key exists this function cannot fail. 134 | /// If there is an existing item with `key`, then the result 135 | /// `Entry` pointer points to it, and found_existing is true. 136 | /// Otherwise, puts a new item with undefined value, and 137 | /// the `Entry` pointer points to it. Caller should then initialize 138 | /// the value (but not the key). 139 | pub fn getOrPut(self: *Map, allocator: Allocator, key: *const Value) !GetOrPutResult { 140 | const gop = try self.getOrPutContextAdapted(allocator, key); 141 | if (!gop.found_existing) { 142 | gop.key_ptr.* = key; 143 | } 144 | return gop; 145 | } 146 | fn getOrPutContextAdapted(self: *Map, allocator: Allocator, key: *const Value) !GetOrPutResult { 147 | self.ensureTotalCapacity(allocator, self.entries.len + 1) catch |err| { 148 | // "If key exists this function cannot fail." 149 | const index = self.getIndex(key) orelse return err; 150 | const slice = self.entries.slice(); 151 | return GetOrPutResult{ 152 | .key_ptr = &slice.items(.key)[index], 153 | .value_ptr = &slice.items(.value)[index], 154 | .found_existing = true, 155 | .index = index, 156 | }; 157 | }; 158 | return self.getOrPutAssumeCapacityAdapted(key); 159 | } 160 | 161 | /// If there is an existing item with `key`, then the result 162 | /// `Entry` pointer points to it, and found_existing is true. 163 | /// Otherwise, puts a new item with undefined value, and 164 | /// the `Entry` pointer points to it. Caller should then initialize 165 | /// the value (but not the key). 166 | /// If a new entry needs to be stored, this function asserts there 167 | /// is enough capacity to store it. 168 | pub fn getOrPutAssumeCapacity(self: *Map, key: *const Value) GetOrPutResult { 169 | const gop = self.getOrPutAssumeCapacityAdapted(key); 170 | if (!gop.found_existing) { 171 | gop.key_ptr.* = key; 172 | } 173 | return gop; 174 | } 175 | /// If there is an existing item with `key`, then the result 176 | /// `Entry` pointers point to it, and found_existing is true. 177 | /// Otherwise, puts a new item with undefined key and value, and 178 | /// the `Entry` pointers point to it. Caller must then initialize 179 | /// both the key and the value. 180 | /// If a new entry needs to be stored, this function asserts there 181 | /// is enough capacity to store it. 182 | fn getOrPutAssumeCapacityAdapted(self: *Map, key: *const Value) GetOrPutResult { 183 | const header = self.index_header orelse { 184 | // Linear scan. 185 | const h = key.hash(); 186 | const slice = self.entries.slice(); 187 | const hashes_array = slice.items(.hash); 188 | const keys_array = slice.items(.key); 189 | for (keys_array, 0..) |*item_key, i| { 190 | if (hashes_array[i] == h and key.eql(item_key.*)) { 191 | return GetOrPutResult{ 192 | .key_ptr = item_key, 193 | .value_ptr = &slice.items(.value)[i], 194 | .found_existing = true, 195 | .index = @intCast(i), 196 | }; 197 | } 198 | } 199 | 200 | const index = self.entries.addOneAssumeCapacity(); 201 | // unsafe indexing because the length changed 202 | hashes_array.ptr[index] = h; 203 | 204 | return GetOrPutResult{ 205 | .key_ptr = &keys_array.ptr[index], 206 | .value_ptr = &slice.items(.value).ptr[index], 207 | .found_existing = false, 208 | .index = index, 209 | }; 210 | }; 211 | 212 | switch (header.capacityIndexType()) { 213 | .u8 => return self.getOrPutInternal(key, header, u8), 214 | .u16 => return self.getOrPutInternal(key, header, u16), 215 | .u32 => return self.getOrPutInternal(key, header, u32), 216 | } 217 | } 218 | 219 | /// Increases capacity, guaranteeing that insertions up until the 220 | /// `expected_count` will not cause an allocation, and therefore cannot fail. 221 | pub fn ensureTotalCapacity(self: *Map, allocator: Allocator, new_capacity: u32) !void { 222 | if (new_capacity <= linear_scan_max) { 223 | try self.entries.ensureTotalCapacity(allocator, new_capacity); 224 | return; 225 | } 226 | 227 | if (self.index_header) |header| { 228 | if (new_capacity <= header.capacity()) { 229 | try self.entries.ensureTotalCapacity(allocator, new_capacity); 230 | return; 231 | } 232 | } 233 | 234 | const new_bit_index = try IndexHeader.findBitIndex(new_capacity); 235 | const new_header = try IndexHeader.alloc(allocator, new_bit_index); 236 | try self.entries.ensureTotalCapacity(allocator, new_capacity); 237 | 238 | if (self.index_header) |old_header| old_header.free(allocator); 239 | self.insertAllEntriesIntoNewHeader(new_header); 240 | self.index_header = new_header; 241 | } 242 | 243 | /// Increases capacity, guaranteeing that insertions up until 244 | /// `additional_count` **more** items will not cause an allocation, and 245 | /// therefore cannot fail. 246 | pub fn ensureUnusedCapacity( 247 | self: *Map, 248 | allocator: Allocator, 249 | additional_capacity: u32, 250 | ) !void { 251 | return self.ensureTotalCapacity(allocator, self.count() + additional_capacity); 252 | } 253 | 254 | /// Clobbers any existing data. To detect if a put would clobber 255 | /// existing data, see `getOrPut`. 256 | pub fn put(self: *Map, allocator: Allocator, key: *const Value, value: *Value) !void { 257 | const result = try self.getOrPut(allocator, key); 258 | result.value_ptr.* = value; 259 | } 260 | 261 | /// Inserts a key-value pair into the hash map, asserting that no previous 262 | /// entry with the same key is already present 263 | pub fn putNoClobber(self: *Map, allocator: Allocator, key: *const Value, value: *Value) !void { 264 | const result = try self.getOrPut(allocator, key); 265 | assert(!result.found_existing); 266 | result.value_ptr.* = value; 267 | } 268 | 269 | /// Asserts there is enough capacity to store the new key-value pair. 270 | /// Clobbers any existing data. To detect if a put would clobber 271 | /// existing data, see `getOrPutAssumeCapacity`. 272 | pub fn putAssumeCapacity(self: *Map, key: *const Value, value: *Value) void { 273 | const result = self.getOrPutAssumeCapacity(key); 274 | result.value_ptr.* = value; 275 | } 276 | 277 | /// Asserts there is enough capacity to store the new key-value pair. 278 | /// Asserts that it does not clobber any existing data. 279 | /// To detect if a put would clobber existing data, see `getOrPutAssumeCapacity`. 280 | pub fn putAssumeCapacityNoClobber(self: *Map, key: *const Value, value: *Value) void { 281 | const result = self.getOrPutAssumeCapacity(key); 282 | assert(!result.found_existing); 283 | result.value_ptr.* = value; 284 | } 285 | 286 | /// Finds the index in the `entries` array where a key is stored 287 | pub fn getIndex(self: Map, key: *const Value) ?u32 { 288 | const header = self.index_header orelse { 289 | // Linear scan. 290 | const h = key.hash(); 291 | const slice = self.entries.slice(); 292 | const hashes_array = slice.items(.hash); 293 | const keys_array = slice.items(.key); 294 | for (keys_array, 0..) |*item_key, i| { 295 | if (hashes_array[i] == h and key.eql(item_key.*)) { 296 | return @intCast(i); 297 | } 298 | } 299 | return null; 300 | }; 301 | switch (header.capacityIndexType()) { 302 | .u8 => return self.getIndexWithHeaderGeneric(key, header, u8), 303 | .u16 => return self.getIndexWithHeaderGeneric(key, header, u16), 304 | .u32 => return self.getIndexWithHeaderGeneric(key, header, u32), 305 | } 306 | } 307 | fn getIndexWithHeaderGeneric(self: Map, key: anytype, header: *IndexHeader, comptime I: type) ?u32 { 308 | const indexes = header.indexes(I); 309 | const slot = self.getSlotByKey(key, header, I, indexes) orelse return null; 310 | return indexes[slot].entry_index; 311 | } 312 | 313 | /// Find the value associated with a key 314 | pub fn get(self: Map, key: *const Value) ?*Value { 315 | const index = self.getIndex(key) orelse return null; 316 | return self.values()[index]; 317 | } 318 | 319 | /// Check whether a key is stored in the map 320 | pub fn contains(self: Map, key: *const Value) bool { 321 | return self.getIndex(key) != null; 322 | } 323 | 324 | /// Create a copy of the hash map which can be modified separately. 325 | /// The copy uses the same context and allocator as this instance. 326 | pub fn clone(self: Map, allocator: Allocator) !Map { 327 | var other: Map = .{}; 328 | other.entries = try self.entries.clone(allocator); 329 | errdefer other.entries.deinit(allocator); 330 | 331 | if (self.index_header) |header| { 332 | const new_header = try IndexHeader.alloc(allocator, header.bit_index); 333 | other.insertAllEntriesIntoNewHeader(new_header); 334 | other.index_header = new_header; 335 | } 336 | return other; 337 | } 338 | 339 | // // ------------------ No pub fns below this point ------------------ 340 | 341 | /// Must `ensureTotalCapacity`/`ensureUnusedCapacity` before calling this. 342 | fn getOrPutInternal(self: *Map, key: *const Value, header: *IndexHeader, comptime I: type) GetOrPutResult { 343 | const slice = self.entries.slice(); 344 | const hashes_array = slice.items(.hash); 345 | const keys_array = slice.items(.key); 346 | const values_array = slice.items(.value); 347 | const indexes = header.indexes(I); 348 | 349 | const h = key.hash(); 350 | const start_index = h; 351 | const end_index = start_index +% indexes.len; 352 | 353 | var index = start_index; 354 | var distance_from_start_index: I = 0; 355 | while (index != end_index) : ({ 356 | index +%= 1; 357 | distance_from_start_index += 1; 358 | }) { 359 | var slot = header.constrainIndex(index); 360 | var slot_data = indexes[slot]; 361 | 362 | // If the slot is empty, there can be no more items in this run. 363 | // We didn't find a matching item, so this must be new. 364 | // Put it in the empty slot. 365 | if (slot_data.isEmpty()) { 366 | const new_index = self.entries.addOneAssumeCapacity(); 367 | indexes[slot] = .{ 368 | .distance_from_start_index = distance_from_start_index, 369 | .entry_index = @intCast(new_index), 370 | }; 371 | 372 | // update the hash if applicable 373 | hashes_array.ptr[new_index] = h; 374 | 375 | return .{ 376 | .found_existing = false, 377 | .key_ptr = &keys_array.ptr[new_index], 378 | .value_ptr = &values_array.ptr[new_index], 379 | .index = new_index, 380 | }; 381 | } 382 | 383 | // This pointer survives the following append because we call 384 | // entries.ensureTotalCapacity before getOrPutInternal. 385 | const i = slot_data.entry_index; 386 | const hash_match = h == hashes_array[i]; 387 | if (hash_match and key.eql(keys_array[i])) { 388 | return .{ 389 | .found_existing = true, 390 | .key_ptr = &keys_array[slot_data.entry_index], 391 | .value_ptr = &values_array[slot_data.entry_index], 392 | .index = slot_data.entry_index, 393 | }; 394 | } 395 | 396 | // If the entry is closer to its target than our current distance, 397 | // the entry we are looking for does not exist. It would be in 398 | // this slot instead if it was here. So stop looking, and switch 399 | // to insert mode. 400 | if (slot_data.distance_from_start_index < distance_from_start_index) { 401 | // In this case, we did not find the item. We will put a new entry. 402 | // However, we will use this index for the new entry, and move 403 | // the previous index down the line, to keep the max distance_from_start_index 404 | // as small as possible. 405 | const new_index = self.entries.addOneAssumeCapacity(); 406 | hashes_array.ptr[new_index] = h; 407 | indexes[slot] = .{ 408 | .entry_index = @intCast(new_index), 409 | .distance_from_start_index = distance_from_start_index, 410 | }; 411 | distance_from_start_index = slot_data.distance_from_start_index; 412 | var displaced_index = slot_data.entry_index; 413 | 414 | // Find somewhere to put the index we replaced by shifting 415 | // following indexes backwards. 416 | index +%= 1; 417 | distance_from_start_index += 1; 418 | while (index != end_index) : ({ 419 | index +%= 1; 420 | distance_from_start_index += 1; 421 | }) { 422 | slot = header.constrainIndex(index); 423 | slot_data = indexes[slot]; 424 | if (slot_data.isEmpty()) { 425 | indexes[slot] = .{ 426 | .entry_index = displaced_index, 427 | .distance_from_start_index = distance_from_start_index, 428 | }; 429 | return .{ 430 | .found_existing = false, 431 | .key_ptr = &keys_array.ptr[new_index], 432 | .value_ptr = &values_array.ptr[new_index], 433 | .index = new_index, 434 | }; 435 | } 436 | 437 | if (slot_data.distance_from_start_index < distance_from_start_index) { 438 | indexes[slot] = .{ 439 | .entry_index = displaced_index, 440 | .distance_from_start_index = distance_from_start_index, 441 | }; 442 | displaced_index = slot_data.entry_index; 443 | distance_from_start_index = slot_data.distance_from_start_index; 444 | } 445 | } 446 | unreachable; 447 | } 448 | } 449 | unreachable; 450 | } 451 | 452 | fn getSlotByKey(self: Map, key: anytype, header: *IndexHeader, comptime I: type, indexes: []Index(I)) ?u32 { 453 | const slice = self.entries.slice(); 454 | const hashes_array = slice.items(.hash); 455 | const keys_array = slice.items(.key); 456 | const h = key.hash(); 457 | 458 | const start_index = h; 459 | const end_index = start_index +% indexes.len; 460 | 461 | var index = start_index; 462 | var distance_from_start_index: I = 0; 463 | while (index != end_index) : ({ 464 | index +%= 1; 465 | distance_from_start_index += 1; 466 | }) { 467 | const slot = header.constrainIndex(index); 468 | const slot_data = indexes[slot]; 469 | if (slot_data.isEmpty() or slot_data.distance_from_start_index < distance_from_start_index) 470 | return null; 471 | 472 | const i = slot_data.entry_index; 473 | const hash_match = h == hashes_array[i]; 474 | if (hash_match and key.eql(keys_array[i])) 475 | return slot; 476 | } 477 | unreachable; 478 | } 479 | 480 | fn insertAllEntriesIntoNewHeader(self: *Map, header: *IndexHeader) void { 481 | switch (header.capacityIndexType()) { 482 | .u8 => return self.insertAllEntriesIntoNewHeaderGeneric(header, u8), 483 | .u16 => return self.insertAllEntriesIntoNewHeaderGeneric(header, u16), 484 | .u32 => return self.insertAllEntriesIntoNewHeaderGeneric(header, u32), 485 | } 486 | } 487 | fn insertAllEntriesIntoNewHeaderGeneric(self: *Map, header: *IndexHeader, comptime I: type) void { 488 | const slice = self.entries.slice(); 489 | const items = slice.items(.hash); 490 | const indexes = header.indexes(I); 491 | 492 | entry_loop: for (items, 0..) |key, i| { 493 | const start_index = key; 494 | const end_index = start_index +% indexes.len; 495 | var index = start_index; 496 | var entry_index: I = @intCast(i); 497 | var distance_from_start_index: I = 0; 498 | while (index != end_index) : ({ 499 | index +%= 1; 500 | distance_from_start_index += 1; 501 | }) { 502 | const slot = header.constrainIndex(index); 503 | const next_index = indexes[slot]; 504 | if (next_index.isEmpty()) { 505 | indexes[slot] = .{ 506 | .distance_from_start_index = distance_from_start_index, 507 | .entry_index = entry_index, 508 | }; 509 | continue :entry_loop; 510 | } 511 | if (next_index.distance_from_start_index < distance_from_start_index) { 512 | indexes[slot] = .{ 513 | .distance_from_start_index = distance_from_start_index, 514 | .entry_index = entry_index, 515 | }; 516 | distance_from_start_index = next_index.distance_from_start_index; 517 | entry_index = next_index.entry_index; 518 | } 519 | } 520 | unreachable; 521 | } 522 | } 523 | 524 | const CapacityIndexType = enum { u8, u16, u32 }; 525 | 526 | fn capacityIndexType(bit_index: u8) CapacityIndexType { 527 | if (bit_index <= 8) 528 | return .u8; 529 | if (bit_index <= 16) 530 | return .u16; 531 | assert(bit_index <= 32); 532 | return .u32; 533 | } 534 | 535 | fn capacityIndexSize(bit_index: u8) u32 { 536 | switch (capacityIndexType(bit_index)) { 537 | .u8 => return @sizeOf(Index(u8)), 538 | .u16 => return @sizeOf(Index(u16)), 539 | .u32 => return @sizeOf(Index(u32)), 540 | } 541 | } 542 | 543 | /// A single entry in the lookup acceleration structure. These structs 544 | /// are found in an array after the IndexHeader. Hashes index into this 545 | /// array, and linear probing is used for collisions. 546 | fn Index(comptime I: type) type { 547 | return extern struct { 548 | const Self = @This(); 549 | 550 | /// The index of this entry in the backing store. If the index is 551 | /// empty, this is empty_sentinel. 552 | entry_index: I, 553 | 554 | /// The distance between this slot and its ideal placement. This is 555 | /// used to keep maximum scan length small. This value is undefined 556 | /// if the index is empty. 557 | distance_from_start_index: I, 558 | 559 | /// The special entry_index value marking an empty slot. 560 | const empty_sentinel = ~@as(I, 0); 561 | 562 | /// A constant empty index 563 | const empty = Self{ 564 | .entry_index = empty_sentinel, 565 | .distance_from_start_index = undefined, 566 | }; 567 | 568 | /// Checks if a slot is empty 569 | fn isEmpty(idx: Self) bool { 570 | return idx.entry_index == empty_sentinel; 571 | } 572 | 573 | /// Sets a slot to empty 574 | fn setEmpty(idx: *Self) void { 575 | idx.entry_index = empty_sentinel; 576 | idx.distance_from_start_index = undefined; 577 | } 578 | }; 579 | } 580 | 581 | // /// the byte size of the index must fit in a u32. This is a power of two 582 | // /// length * the size of an Index(u32). The index is 8 bytes (3 bits repr) 583 | // /// and max_usize + 1 is not representable, so we need to subtract out 4 bits. 584 | const max_representable_index_len = @bitSizeOf(u32) - 4; 585 | const max_bit_index = @min(32, max_representable_index_len); 586 | const min_bit_index = 5; 587 | const max_capacity = (1 << max_bit_index) - 1; 588 | const index_capacities = blk: { 589 | var caps: [max_bit_index + 1]u32 = undefined; 590 | for (caps[0..max_bit_index], 0..) |*item, i| { 591 | item.* = (1 << i) * 3 / 5; 592 | } 593 | caps[max_bit_index] = max_capacity; 594 | break :blk caps; 595 | }; 596 | 597 | /// This struct is trailed by two arrays of length indexes_len 598 | /// of integers, whose integer size is determined by indexes_len. 599 | /// These arrays are indexed by constrainIndex(hash). The 600 | /// entryIndexes array contains the index in the dense backing store 601 | /// where the entry's data can be found. Entries which are not in 602 | /// use have their index value set to emptySentinel(I). 603 | /// The entryDistances array stores the distance between an entry 604 | /// and its ideal hash bucket. This is used when adding elements 605 | /// to balance the maximum scan length. 606 | const IndexHeader = struct { 607 | /// This field tracks the total number of items in the arrays following 608 | /// this header. It is the bit index of the power of two number of indices. 609 | /// This value is between min_bit_index and max_bit_index, inclusive. 610 | bit_index: u8 align(@alignOf(u32)), 611 | 612 | /// Map from an incrementing index to an index slot in the attached arrays. 613 | fn constrainIndex(header: IndexHeader, i: u32) u32 { 614 | // This is an optimization for modulo of power of two integers; 615 | // it requires `indexes_len` to always be a power of two. 616 | return @intCast(i & header.mask()); 617 | } 618 | 619 | /// Returns the attached array of indexes. I must match the type 620 | /// returned by capacityIndexType. 621 | fn indexes(header: *IndexHeader, comptime I: type) []Index(I) { 622 | const start_ptr: [*]Index(I) = @ptrCast(@alignCast(@as([*]u8, @ptrCast(header)) + @sizeOf(IndexHeader))); 623 | return start_ptr[0..header.length()]; 624 | } 625 | 626 | /// Returns the type used for the index arrays. 627 | fn capacityIndexType(header: IndexHeader) CapacityIndexType { 628 | return Map.capacityIndexType(header.bit_index); 629 | } 630 | 631 | fn capacity(self: IndexHeader) u32 { 632 | return index_capacities[self.bit_index]; 633 | } 634 | fn length(self: IndexHeader) u32 { 635 | return @as(u32, 1) << @intCast(self.bit_index); 636 | } 637 | fn mask(self: IndexHeader) u32 { 638 | return @intCast(self.length() - 1); 639 | } 640 | 641 | fn findBitIndex(desired_capacity: u32) !u8 { 642 | if (desired_capacity > max_capacity) return error.OutOfMemory; 643 | var new_bit_index: u8 = @intCast(std.math.log2_int_ceil(u32, desired_capacity)); 644 | if (desired_capacity > index_capacities[new_bit_index]) new_bit_index += 1; 645 | if (new_bit_index < min_bit_index) new_bit_index = min_bit_index; 646 | assert(desired_capacity <= index_capacities[new_bit_index]); 647 | return new_bit_index; 648 | } 649 | 650 | /// Allocates an index header, and fills the entryIndexes array with empty. 651 | /// The distance array contents are undefined. 652 | fn alloc(allocator: Allocator, new_bit_index: u8) !*IndexHeader { 653 | const len = @as(usize, 1) << @intCast(new_bit_index); 654 | const index_size = Map.capacityIndexSize(new_bit_index); 655 | const nbytes = @sizeOf(IndexHeader) + index_size * len; 656 | const bytes = try allocator.alignedAlloc(u8, @alignOf(IndexHeader), nbytes); 657 | @memset(bytes[@sizeOf(IndexHeader)..], 0xff); 658 | const result: *IndexHeader = @ptrCast(bytes.ptr); 659 | result.* = .{ 660 | .bit_index = new_bit_index, 661 | }; 662 | return result; 663 | } 664 | 665 | /// Releases the memory for a header and its associated arrays. 666 | fn free(header: *IndexHeader, allocator: Allocator) void { 667 | const index_size = Map.capacityIndexSize(header.bit_index); 668 | const ptr: [*]align(@alignOf(IndexHeader)) u8 = @ptrCast(header); 669 | const slice = ptr[0 .. @sizeOf(IndexHeader) + header.length() * index_size]; 670 | allocator.free(slice); 671 | } 672 | }; 673 | -------------------------------------------------------------------------------- /src/String.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const mem = std.mem; 3 | const Allocator = mem.Allocator; 4 | const bog = @import("bog.zig"); 5 | const Vm = bog.Vm; 6 | const Value = bog.Value; 7 | const Type = bog.Type; 8 | 9 | const String = @This(); 10 | 11 | const default_dump_depth = 4; 12 | 13 | /// Memory used by the string contents. 14 | data: []const u8, 15 | /// If 0 memory is a constant reference 16 | capacity: usize = 0, 17 | 18 | pub const Builder = struct { 19 | inner: std.ArrayList(u8), 20 | 21 | pub fn finish(b: Builder) String { 22 | return .{ 23 | .data = b.inner.items, 24 | .capacity = b.inner.capacity, 25 | }; 26 | } 27 | 28 | pub fn cancel(b: *Builder) void { 29 | b.inner.deinit(); 30 | b.* = undefined; 31 | } 32 | 33 | pub fn append(b: *Builder, data: []const u8) !void { 34 | try b.inner.appendSlice(data); 35 | } 36 | 37 | pub const Writer = std.io.Writer(*Builder, error{OutOfMemory}, appendWrite); 38 | 39 | /// Initializes a Writer which will append to the builder. 40 | pub fn writer(self: *Builder) Writer { 41 | return .{ .context = self }; 42 | } 43 | 44 | /// Same as `append` except it returns the number of bytes written, which is always the same 45 | /// as `m.len`. The purpose of this function existing is to match `std.io.Writer` API. 46 | fn appendWrite(self: *Builder, data: []const u8) !usize { 47 | try self.append(data); 48 | return data.len; 49 | } 50 | }; 51 | 52 | pub fn builder(allocator: Allocator) Builder { 53 | return Builder{ 54 | .inner = std.ArrayList(u8).init(allocator), 55 | }; 56 | } 57 | 58 | pub fn init(allocator: Allocator, comptime fmt: []const u8, args: anytype) !String { 59 | var b = builder(allocator); 60 | errdefer b.cancel(); 61 | 62 | try b.writer().print(fmt, args); 63 | return b.finish(); 64 | } 65 | 66 | pub fn deinit(str: *String, allocator: Allocator) void { 67 | if (str.capacity != 0) { 68 | allocator.free(str.data); 69 | } 70 | str.* = undefined; 71 | } 72 | 73 | pub fn eql(a: String, b: String) bool { 74 | return mem.eql(u8, a.data, b.data); 75 | } 76 | 77 | pub fn dump(str: String, writer: anytype) !void { 78 | try writer.print("\"{}\"", .{std.zig.fmtEscapes(str.data)}); 79 | } 80 | 81 | pub fn get(str: *const String, ctx: Vm.Context, index: *const Value, res: *?*Value) Value.NativeError!void { 82 | switch (index.*) { 83 | .int => return ctx.frame.fatal(ctx.vm, "TODO str get int"), 84 | .range => return ctx.frame.fatal(ctx.vm, "TODO str get with ranges"), 85 | .str => |*s| { 86 | if (res.* == null) { 87 | res.* = try ctx.vm.gc.alloc(.int); 88 | } 89 | 90 | if (mem.eql(u8, s.data, "len")) { 91 | res.*.?.* = .{ .int = @intCast(str.data.len) }; 92 | } else inline for (@typeInfo(methods).@"struct".decls) |method| { 93 | if (mem.eql(u8, s.data, method.name)) { 94 | res.* = try Value.zigFnToBog(ctx.vm, @field(methods, method.name)); 95 | return; 96 | } 97 | } else { 98 | return ctx.throw("no such property"); 99 | } 100 | }, 101 | else => return ctx.throw("invalid index type"), 102 | } 103 | } 104 | 105 | pub const methods = struct { 106 | pub fn append(str: Value.This(*String), ctx: Vm.Context, strs: Value.Variadic([]const u8)) !void { 107 | var b = builder(ctx.vm.gc.gpa); 108 | errdefer b.cancel(); 109 | 110 | var len = str.t.data.len; 111 | for (strs.t) |new| len += new.len; 112 | try b.inner.ensureUnusedCapacity(len); 113 | 114 | b.inner.appendSliceAssumeCapacity(str.t.data); 115 | for (strs.t) |new| b.inner.appendSliceAssumeCapacity(new); 116 | 117 | str.t.deinit(ctx.vm.gc.gpa); 118 | str.t.* = b.finish(); 119 | } 120 | 121 | pub fn format(str: Value.This([]const u8), ctx: Vm.Context, args: Value.Variadic(*Value)) !*Value { 122 | var b = builder(ctx.vm.gc.gpa); 123 | errdefer b.cancel(); 124 | 125 | try b.inner.ensureTotalCapacity(str.t.len); 126 | 127 | const w = b.writer(); 128 | var state: enum { 129 | start, 130 | brace, 131 | format, 132 | } = .start; 133 | var arg_i: usize = 0; 134 | var format_start: usize = 0; 135 | var options = std.fmt.FormatOptions{}; 136 | 137 | var i: usize = 0; 138 | while (i < str.t.len) : (i += 1) { 139 | const c = str.t[i]; 140 | switch (state) { 141 | .start => if (c == '{') { 142 | state = .brace; 143 | } else { 144 | try w.writeByte(c); 145 | }, 146 | .brace => if (c == '{') { 147 | try w.writeByte(c); 148 | } else { 149 | if (arg_i >= args.t.len) { 150 | return ctx.throw("not enough arguments to format string"); 151 | } 152 | format_start = i; 153 | state = .format; 154 | options = .{}; 155 | i -= 1; 156 | }, 157 | .format => if (c == '}') { 158 | const fmt = str.t[format_start..i]; 159 | var fmt_type: u8 = 0; 160 | 161 | if (fmt.len == 1) { 162 | fmt_type = fmt[0]; 163 | } else { 164 | // TODO properly parse format options 165 | } 166 | 167 | switch (fmt_type) { 168 | 'x', 'X' => { 169 | if (args.t[arg_i].* != .int) { 170 | return ctx.throwFmt("'x' takes an integer as an argument, got '{s}'", .{args.t[arg_i].typeName()}); 171 | } 172 | try std.fmt.formatInt(args.t[arg_i].int, 16, @enumFromInt(@intFromBool(fmt[0] == 'X')), options, w); 173 | }, 174 | 0 => if (args.t[arg_i].* == .str) { 175 | try b.append(args.t[arg_i].str.data); 176 | } else { 177 | try args.t[arg_i].dump(w, default_dump_depth); 178 | }, 179 | else => { 180 | return ctx.throw("unknown format specifier"); 181 | }, 182 | } 183 | 184 | state = .start; 185 | arg_i += 1; 186 | }, 187 | } 188 | } 189 | if (arg_i != args.t.len) { 190 | return ctx.throw("unused arguments"); 191 | } 192 | 193 | const ret = try ctx.vm.gc.alloc(.str); 194 | ret.* = Value{ .str = b.finish() }; 195 | return ret; 196 | } 197 | 198 | pub fn join(str: Value.This([]const u8), ctx: Vm.Context, strs: Value.Variadic([]const u8)) !*Value { 199 | if (strs.t.len == 0) { 200 | const ret = try ctx.vm.gc.alloc(.str); 201 | ret.* = Value.string(""); 202 | return ret; 203 | } 204 | var b = builder(ctx.vm.gc.gpa); 205 | errdefer b.cancel(); 206 | 207 | var len = str.t.len * (strs.t.len - 1); 208 | for (strs.t) |new| len += new.len; 209 | try b.inner.ensureUnusedCapacity(len); 210 | 211 | for (strs.t, 0..) |arg, i| { 212 | if (i != 0) { 213 | b.inner.appendSliceAssumeCapacity(str.t); 214 | } 215 | b.inner.appendSliceAssumeCapacity(arg); 216 | } 217 | 218 | const ret = try ctx.vm.gc.alloc(.str); 219 | ret.* = Value{ .str = b.finish() }; 220 | return ret; 221 | } 222 | }; 223 | 224 | pub fn set(str: *String, ctx: Vm.Context, index: *const Value, new_val: *const Value) Value.NativeError!void { 225 | _ = str; 226 | _ = index; 227 | _ = new_val; 228 | return ctx.frame.fatal(ctx.vm, "TODO set string"); 229 | } 230 | 231 | pub fn as(str: *String, ctx: Vm.Context, type_id: Type) Value.NativeError!*Value { 232 | if (type_id == .null) { 233 | return Value.Null; 234 | } else if (type_id == .bool) { 235 | if (mem.eql(u8, str.data, "true")) 236 | return Value.True 237 | else if (mem.eql(u8, str.data, "false")) 238 | return Value.False 239 | else 240 | return ctx.throw("cannot cast string to bool"); 241 | } 242 | 243 | const new_val = try ctx.vm.gc.alloc(type_id); 244 | new_val.* = switch (type_id) { 245 | .int => .{ 246 | .int = std.fmt.parseInt(i64, str.data, 0) catch |err| 247 | return ctx.throwFmt("cannot cast string to int: {s}", .{@errorName(err)}), 248 | }, 249 | .num => .{ 250 | .num = std.fmt.parseFloat(f64, str.data) catch |err| 251 | return ctx.throwFmt("cannot cast string to num: {s}", .{@errorName(err)}), 252 | }, 253 | .bool => unreachable, 254 | .str => unreachable, // already string 255 | .tuple, 256 | .map, 257 | .list, 258 | => return ctx.frame.fatal(ctx.vm, "TODO more casts from string"), 259 | else => unreachable, 260 | }; 261 | return new_val; 262 | } 263 | 264 | pub fn from(val: *Value, vm: *Vm) Vm.Error!*Value { 265 | const str = try vm.gc.alloc(.str); 266 | 267 | if (val == Value.Null) { 268 | str.* = Value.string("null"); 269 | } else if (val == Value.True) { 270 | str.* = Value.string("true"); 271 | } else if (val == Value.False) { 272 | str.* = Value.string("false"); 273 | } else { 274 | var b = builder(vm.gc.gpa); 275 | try val.dump(b.writer(), default_dump_depth); 276 | str.* = .{ .str = b.finish() }; 277 | } 278 | return str; 279 | } 280 | 281 | pub fn in(str: *const String, val: *const Value) bool { 282 | if (val.* != .str) return false; 283 | return mem.indexOf(u8, str.data, val.str.data) != null; 284 | } 285 | -------------------------------------------------------------------------------- /src/Tree.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Allocator = std.mem.Allocator; 3 | 4 | const bog = @import("bog.zig"); 5 | const Token = bog.Token; 6 | 7 | pub const Tree = @This(); 8 | 9 | tokens: Token.List, 10 | nodes: Node.List, 11 | extra: []Node.Index, 12 | root_nodes: []Node.Index, 13 | 14 | /// not owned by the tree 15 | source: []const u8, 16 | path: []const u8, 17 | 18 | pub fn deinit(tree: *Tree, gpa: Allocator) void { 19 | tree.tokens.deinit(gpa); 20 | tree.nodes.deinit(gpa); 21 | gpa.free(tree.extra); 22 | gpa.free(tree.root_nodes); 23 | tree.* = undefined; 24 | } 25 | 26 | pub const render = @import("render.zig").render; 27 | 28 | pub fn tokenSlice(tree: Tree, tok: Token.Index) []const u8 { 29 | const starts = tree.tokens.items(.start); 30 | const ends = tree.tokens.items(.end); 31 | return tree.source[starts[tok]..ends[tok]]; 32 | } 33 | 34 | pub fn firstToken(tree: Tree, node: Node.Index) Token.Index { 35 | const toks = tree.nodes.items(.token); 36 | const ids = tree.nodes.items(.id); 37 | const data = tree.nodes.items(.data); 38 | var cur = node; 39 | while (true) switch (ids[cur]) { 40 | .bool_not_expr, 41 | .bit_not_expr, 42 | .negate_expr, 43 | .spread_expr, 44 | .await_expr, 45 | .decl, 46 | .ident_expr, 47 | .discard_expr, 48 | .return_expr, 49 | .break_expr, 50 | .continue_expr, 51 | .throw_expr, 52 | .suspend_expr, 53 | .resume_expr, 54 | .fn_expr, 55 | .fn_expr_one, 56 | .paren_expr, 57 | .tuple_expr, 58 | .tuple_expr_two, 59 | .list_expr, 60 | .list_expr_two, 61 | .map_expr, 62 | .map_expr_two, 63 | .error_expr, 64 | .string_expr, 65 | .int_expr, 66 | .num_expr, 67 | .true_expr, 68 | .false_expr, 69 | .this_expr, 70 | .null_expr, 71 | .if_expr, 72 | .if_else_expr, 73 | .if_let_expr, 74 | .if_let_else_expr, 75 | .while_expr, 76 | .while_let_expr, 77 | .for_expr, 78 | .for_let_expr, 79 | .match_expr, 80 | .match_expr_one, 81 | .try_expr, 82 | .try_expr_one, 83 | .catch_expr, 84 | .catch_let_expr, 85 | .block_stmt, 86 | .block_stmt_two, 87 | .import_expr, 88 | => return toks[cur], 89 | .mut_ident_expr, 90 | .enum_expr, 91 | .match_case_catch_all, 92 | => return tree.prevToken(toks[cur]), 93 | .call_expr => cur = tree.extra[data[cur].range.start], 94 | .async_call_expr => { 95 | cur = tree.extra[data[cur].range.start]; 96 | const first_token = tree.firstToken(cur); 97 | return tree.prevToken(first_token); 98 | }, 99 | .member_access_expr => cur = data[cur].un, 100 | .assign, 101 | .add_assign, 102 | .sub_assign, 103 | .mul_assign, 104 | .pow_assign, 105 | .div_assign, 106 | .div_floor_assign, 107 | .rem_assign, 108 | .l_shift_assign, 109 | .r_shift_assign, 110 | .bit_and_assign, 111 | .bit_or_assign, 112 | .bit_xor_assign, 113 | .array_access_expr, 114 | .call_expr_one, 115 | .match_case_one, 116 | .range_expr_end, 117 | .range_expr_step, 118 | .bool_or_expr, 119 | .bool_and_expr, 120 | .less_than_expr, 121 | .less_than_equal_expr, 122 | .greater_than_expr, 123 | .greater_than_equal_expr, 124 | .equal_expr, 125 | .not_equal_expr, 126 | .in_expr, 127 | .is_expr, 128 | .as_expr, 129 | .bit_and_expr, 130 | .bit_or_expr, 131 | .bit_xor_expr, 132 | .l_shift_expr, 133 | .r_shift_expr, 134 | .add_expr, 135 | .sub_expr, 136 | .mul_expr, 137 | .div_expr, 138 | .div_floor_expr, 139 | .rem_expr, 140 | .pow_expr, 141 | => cur = data[cur].bin.lhs, 142 | .map_item_expr => if (data[cur].bin.lhs != 0) { 143 | cur = data[cur].bin.lhs; 144 | } else { 145 | cur = data[cur].bin.rhs; 146 | }, 147 | .match_case_let, .async_call_expr_one => { 148 | cur = data[cur].bin.lhs; 149 | const first_token = tree.firstToken(cur); 150 | return tree.prevToken(first_token); 151 | }, 152 | .match_case => { 153 | cur = tree.extra[data[cur].range.start]; 154 | const first_token = tree.firstToken(cur); 155 | return tree.prevToken(first_token); 156 | }, 157 | .format_expr => { 158 | const strs = data[cur].format.str(tree.extra); 159 | return strs[0]; 160 | }, 161 | .range_expr => cur = data[cur].cond.cond, 162 | }; 163 | } 164 | 165 | pub fn lastToken(tree: Tree, node: Node.Index) Token.Index { 166 | const tokens = tree.nodes.items(.token); 167 | const ids = tree.nodes.items(.id); 168 | const data = tree.nodes.items(.data); 169 | const tok_ids = tree.tokens.items(.id); 170 | var cur = node; 171 | while (true) switch (ids[cur]) { 172 | .ident_expr, 173 | .mut_ident_expr, 174 | .discard_expr, 175 | .string_expr, 176 | .int_expr, 177 | .num_expr, 178 | .true_expr, 179 | .false_expr, 180 | .this_expr, 181 | .null_expr, 182 | .break_expr, 183 | .continue_expr, 184 | .suspend_expr, 185 | .member_access_expr, 186 | => return tokens[cur], 187 | .match_case, 188 | .try_expr, 189 | .fn_expr, 190 | .match_expr, 191 | .block_stmt, 192 | => cur = tree.extra[data[cur].range.end - 1], 193 | .fn_expr_one => cur = data[cur].bin.rhs, 194 | .paren_expr => return tree.nextToken(tree.lastToken(data[cur].un)), 195 | .tuple_expr, 196 | .list_expr, 197 | .map_expr, 198 | .call_expr, 199 | .async_call_expr, 200 | => { 201 | const next = tree.nextToken(tree.lastToken(tree.extra[data[cur].range.end - 1])); 202 | return if (tok_ids[next] == .comma) 203 | tree.nextToken(next) 204 | else 205 | next; 206 | }, 207 | .array_access_expr => return tree.nextToken(tree.lastToken(data[cur].bin.rhs)), 208 | .block_stmt_two => if (data[cur].bin.rhs != 0) { 209 | cur = data[cur].bin.rhs; 210 | } else { 211 | cur = data[cur].bin.lhs; 212 | }, 213 | .tuple_expr_two, 214 | .list_expr_two, 215 | .map_expr_two, 216 | => if (data[cur].bin.rhs != 0) { 217 | const next = tree.nextToken(tree.lastToken(data[cur].bin.rhs)); 218 | return if (tok_ids[next] == .comma) 219 | tree.nextToken(next) 220 | else 221 | next; 222 | } else if (data[cur].bin.lhs != 0) { 223 | const next = tree.nextToken(tree.lastToken(data[cur].bin.lhs)); 224 | return if (tok_ids[next] == .comma) 225 | tree.nextToken(next) 226 | else 227 | next; 228 | } else { 229 | return tree.nextToken(tokens[cur]); 230 | }, 231 | .call_expr_one, .async_call_expr_one => if (data[cur].bin.rhs != 0) { 232 | const next = tree.nextToken(tree.lastToken(data[cur].bin.rhs)); 233 | return if (tok_ids[next] == .comma) 234 | tree.nextToken(next) 235 | else 236 | next; 237 | } else { 238 | return tree.nextToken(tokens[cur]); 239 | }, 240 | .try_expr_one => if (data[cur].bin.rhs != 0) { 241 | cur = data[cur].bin.rhs; 242 | } else { 243 | cur = data[cur].bin.lhs; 244 | }, 245 | .format_expr => { 246 | const strs = data[cur].format.str(tree.extra); 247 | return strs[strs.len - 1]; 248 | }, 249 | .if_let_else_expr => cur = tree.extra[data[cur].cond.extra + 1], 250 | .while_let_expr, 251 | .for_let_expr, 252 | .if_else_expr, 253 | .if_let_expr, 254 | .range_expr, 255 | => cur = tree.extra[data[cur].cond.extra + 1], 256 | .error_expr, 257 | .enum_expr, 258 | .return_expr, 259 | => if (data[cur].un != 0) { 260 | cur = data[cur].un; 261 | } else { 262 | return tokens[cur]; 263 | }, 264 | .throw_expr, 265 | .resume_expr, 266 | .bool_not_expr, 267 | .bit_not_expr, 268 | .negate_expr, 269 | .spread_expr, 270 | .await_expr, 271 | .match_case_catch_all, 272 | .import_expr, 273 | => cur = data[cur].un, 274 | .is_expr, .as_expr => return tokens[cur], 275 | .decl, 276 | .assign, 277 | .add_assign, 278 | .sub_assign, 279 | .mul_assign, 280 | .pow_assign, 281 | .div_assign, 282 | .div_floor_assign, 283 | .rem_assign, 284 | .l_shift_assign, 285 | .r_shift_assign, 286 | .bit_and_assign, 287 | .bit_or_assign, 288 | .bit_xor_assign, 289 | .bool_or_expr, 290 | .bool_and_expr, 291 | .less_than_expr, 292 | .less_than_equal_expr, 293 | .greater_than_expr, 294 | .greater_than_equal_expr, 295 | .equal_expr, 296 | .not_equal_expr, 297 | .in_expr, 298 | .bit_and_expr, 299 | .bit_or_expr, 300 | .bit_xor_expr, 301 | .l_shift_expr, 302 | .r_shift_expr, 303 | .add_expr, 304 | .sub_expr, 305 | .mul_expr, 306 | .div_expr, 307 | .div_floor_expr, 308 | .rem_expr, 309 | .pow_expr, 310 | .match_expr_one, 311 | .match_case_let, 312 | .match_case_one, 313 | .catch_expr, 314 | .catch_let_expr, 315 | .if_expr, 316 | .for_expr, 317 | .while_expr, 318 | .map_item_expr, 319 | => cur = data[cur].bin.rhs, 320 | .range_expr_step => if (data[cur].bin.rhs != 0) { 321 | cur = data[cur].bin.rhs; 322 | } else { 323 | const last = tree.lastToken(data[cur].bin.lhs); 324 | const next = tree.nextToken(last); 325 | return if (tok_ids[next] == .colon) 326 | next 327 | else 328 | last; 329 | }, 330 | .range_expr_end => if (data[cur].bin.rhs != 0) { 331 | cur = data[cur].bin.rhs; 332 | } else { 333 | cur = data[cur].bin.lhs; 334 | const last = tree.lastToken(cur); 335 | const next = tree.nextToken(last); 336 | return if (tok_ids[next] == .colon) 337 | next 338 | else 339 | last; 340 | }, 341 | }; 342 | } 343 | 344 | pub fn prevToken(tree: Tree, tok: Token.Index) Token.Index { 345 | const ids = tree.tokens.items(.id); 346 | var i = tok - 1; 347 | while (true) switch (ids[i]) { 348 | // zig fmt: off 349 | .indent_1, .indent_2, .indent_3, .indent_4, .indent_5, 350 | .indent_6, .indent_7, .indent_8, .indent_9, .indent_10, 351 | .indent_11, .indent_12, .indent_13, .indent_14, .indent_15, 352 | .indent_16, .indent_17, .indent_18, .indent_19, .indent_20, 353 | .indent_21, .indent_22, .indent_23, .indent_24, .indent_25, 354 | .indent_26, .indent_27, .indent_28, .indent_29, .indent_30, 355 | .indent_31, .indent_32, .nl => i -= 1, 356 | else => return i, 357 | // zig fmt: on 358 | }; 359 | } 360 | 361 | pub fn nextToken(tree: Tree, tok: Token.Index) Token.Index { 362 | const ids = tree.tokens.items(.id); 363 | var i = tok + 1; 364 | while (true) switch (ids[i]) { 365 | // zig fmt: off 366 | .indent_1, .indent_2, .indent_3, .indent_4, .indent_5, 367 | .indent_6, .indent_7, .indent_8, .indent_9, .indent_10, 368 | .indent_11, .indent_12, .indent_13, .indent_14, .indent_15, 369 | .indent_16, .indent_17, .indent_18, .indent_19, .indent_20, 370 | .indent_21, .indent_22, .indent_23, .indent_24, .indent_25, 371 | .indent_26, .indent_27, .indent_28, .indent_29, .indent_30, 372 | .indent_31, .indent_32, .nl => i += 1, 373 | else => return i, 374 | // zig fmt: on 375 | }; 376 | } 377 | 378 | pub fn lineDist(tree: Tree, a: Token.Index, b: Token.Index) u32 { 379 | var count: u32 = 0; 380 | const ids = tree.tokens.items(.id); 381 | var i = a; 382 | while (i < b) : (i += 1) { 383 | count += @intFromBool(ids[i] == .nl); 384 | } 385 | return count; 386 | } 387 | 388 | pub const Node = struct { 389 | id: Id, 390 | token: Token.Index, 391 | data: Data, 392 | 393 | pub const Data = union { 394 | un: Index, 395 | bin: struct { 396 | lhs: Index, 397 | rhs: Index, 398 | }, 399 | range: struct { 400 | start: u32, 401 | end: u32, 402 | }, 403 | cond: struct { 404 | cond: Index, 405 | extra: u32, 406 | }, 407 | format: struct { 408 | fmt_start: u32, 409 | args_start: u32, 410 | 411 | pub fn str(f: @This(), extra: []Index) []Token.Index { 412 | return @ptrCast(extra[f.fmt_start..f.args_start]); 413 | } 414 | 415 | pub fn exprs(f: @This(), extra: []Index) []Index { 416 | // there are N -1 expressions for N string parts 417 | return extra[f.args_start..][0 .. f.args_start - f.fmt_start - 1]; 418 | } 419 | }, 420 | }; 421 | 422 | pub const List = @import("multi_array_list.zig").MultiArrayList(Node); 423 | 424 | pub const Index = u32; 425 | 426 | pub const Id = enum(u8) { 427 | /// let lhs = rhs 428 | decl, 429 | 430 | // statements 431 | 432 | /// NL extra[start..end] NL 433 | block_stmt, 434 | /// NL lhs NL rhs NL, rhs may be omitted 435 | block_stmt_two, 436 | 437 | // assignment expressions 438 | 439 | /// lhs = rhs 440 | assign, 441 | /// lhs += rhs 442 | add_assign, 443 | /// lhs -= rhs 444 | sub_assign, 445 | /// lhs *= rhs 446 | mul_assign, 447 | /// lhs **= rhs 448 | pow_assign, 449 | /// lhs /= rhs 450 | div_assign, 451 | /// lhs //= rhs 452 | div_floor_assign, 453 | /// lhs %= rhs 454 | rem_assign, 455 | /// lhs <<= rhs 456 | l_shift_assign, 457 | /// lhs >>= rhs 458 | r_shift_assign, 459 | /// lhs &= rhs 460 | bit_and_assign, 461 | /// lhs |= rhs 462 | bit_or_assign, 463 | /// lhs ^= rhs 464 | bit_xor_assign, 465 | 466 | // expressions 467 | 468 | /// token 469 | ident_expr, 470 | /// mut token 471 | mut_ident_expr, 472 | /// _ 473 | discard_expr, 474 | /// return un, un may be omitted 475 | return_expr, 476 | /// break 477 | break_expr, 478 | /// continue 479 | continue_expr, 480 | /// throw un 481 | throw_expr, 482 | /// suspend 483 | suspend_expr, 484 | /// resume_un 485 | resume_expr, 486 | /// fn (extra[start..end-1]) extra[end] 487 | fn_expr, 488 | /// fn (lhs) rhs, lhs may be omitted 489 | fn_expr_one, 490 | /// ( data.un ) 491 | paren_expr, 492 | /// ( extra[start..end]) 493 | tuple_expr, 494 | /// (lhr, rhs) rhs may be omitted 495 | tuple_expr_two, 496 | /// [extra[start..end]] 497 | list_expr, 498 | /// [lhs, rhs] both may be omitted 499 | list_expr_two, 500 | /// { extra[start..end] } 501 | map_expr, 502 | /// { lhs, rhs } both may be omitted 503 | map_expr_two, 504 | /// lhs = rhs, lhs may be omitted 505 | map_item_expr, 506 | /// token 507 | string_expr, 508 | /// token 509 | int_expr, 510 | /// token 511 | num_expr, 512 | /// true 513 | true_expr, 514 | /// false 515 | false_expr, 516 | /// this 517 | this_expr, 518 | /// null 519 | null_expr, 520 | /// error un, un may be omitted 521 | error_expr, 522 | /// @token un, un may be omitted 523 | enum_expr, 524 | /// import un 525 | import_expr, 526 | /// lhs[rhs] 527 | array_access_expr, 528 | /// lhs.token 529 | member_access_expr, 530 | /// extra[start](extra[start+1]..extra[end]) 531 | call_expr, 532 | /// lhs(rhs) rhs may be omitted 533 | call_expr_one, 534 | /// async extra[start](extra[start+1]..extra[end]) 535 | async_call_expr, 536 | /// async lhs(rhs) rhs may be omitted 537 | async_call_expr_one, 538 | /// if lhs rhs 539 | if_expr, 540 | /// if cond extra[0] else extra[1] 541 | if_else_expr, 542 | /// if let extra[0] = cond extra[1] 543 | if_let_expr, 544 | /// if let extra[0] = cond extra[1] else extra[2] 545 | if_let_else_expr, 546 | /// while lhs rhs 547 | while_expr, 548 | /// while let extra[0] cond extra[1] 549 | while_let_expr, 550 | /// for lhs rhs 551 | for_expr, 552 | /// for let extra[0] cond extra[1] 553 | for_let_expr, 554 | /// match extra[start] extra[start+1..end] 555 | match_expr, 556 | /// match lhs rhs 557 | match_expr_one, 558 | /// let lhs => rhs 559 | match_case_let, 560 | /// _ => un 561 | match_case_catch_all, 562 | /// extra[start..end -1] => extra[end] 563 | match_case, 564 | /// lhs => rhs 565 | match_case_one, 566 | /// try extra[start] extra[start+1..end] 567 | try_expr, 568 | /// try lhs rhs, rhs may be omitted 569 | try_expr_one, 570 | /// catch lhs rhs, lhs may be omitted 571 | catch_expr, 572 | /// catch let lhs rhs 573 | catch_let_expr, 574 | /// f" format " 575 | format_expr, 576 | /// cond : extra[0] : extra[1] 577 | range_expr, 578 | /// lhs : rhs 579 | range_expr_end, 580 | /// lhs : : rhs 581 | range_expr_step, 582 | 583 | // unary expressions 584 | 585 | /// not un 586 | bool_not_expr, 587 | /// ~un 588 | bit_not_expr, 589 | /// -un 590 | negate_expr, 591 | /// ...un 592 | spread_expr, 593 | /// await un 594 | await_expr, 595 | 596 | // binary expressions 597 | 598 | /// lhs or rhs 599 | bool_or_expr, 600 | /// lhs and rhs 601 | bool_and_expr, 602 | /// lhs < rhs 603 | less_than_expr, 604 | /// lhs <= rhs 605 | less_than_equal_expr, 606 | /// lhs > rhs 607 | greater_than_expr, 608 | /// lhs >= rhs 609 | greater_than_equal_expr, 610 | /// lhs == rhs 611 | equal_expr, 612 | /// lhs != rhs 613 | not_equal_expr, 614 | /// lhs in rhs 615 | in_expr, 616 | /// lhs is type name 617 | is_expr, 618 | /// lhs as type name 619 | as_expr, 620 | /// lhs & rhs 621 | bit_and_expr, 622 | /// lhs | rhs 623 | bit_or_expr, 624 | /// lhs ^ rhs 625 | bit_xor_expr, 626 | /// lhs << rhs 627 | l_shift_expr, 628 | /// lhs >> rhs 629 | r_shift_expr, 630 | /// lhs + rhs 631 | add_expr, 632 | /// lhs - rhs 633 | sub_expr, 634 | /// lhs * rhs 635 | mul_expr, 636 | /// lhs / rhs 637 | div_expr, 638 | /// lhs // rhs 639 | div_floor_expr, 640 | /// lhs % rhs 641 | rem_expr, 642 | /// lhs ** rhs 643 | pow_expr, 644 | }; 645 | }; 646 | 647 | pub const Range = struct { 648 | start: Node.Index, 649 | colon_1: Token.Index, 650 | end: ?Node.Index, 651 | colon_2: ?Token.Index, 652 | step: ?Node.Index, 653 | 654 | pub fn get(tree: Tree, node: Node.Index) Range { 655 | const tokens = tree.nodes.items(.token); 656 | var range = Range{ 657 | .start = undefined, 658 | .colon_1 = tokens[node], 659 | .end = null, 660 | .colon_2 = null, 661 | .step = null, 662 | }; 663 | const data = tree.nodes.items(.data); 664 | switch (tree.nodes.items(.id)[node]) { 665 | .range_expr => { 666 | range.start = data[node].cond.cond; 667 | range.end = tree.extra[data[node].cond.extra]; 668 | range.step = tree.extra[data[node].cond.extra + 1]; 669 | range.colon_2 = tree.prevToken(tree.firstToken(range.step.?)); 670 | }, 671 | .range_expr_end => { 672 | range.start = data[node].bin.lhs; 673 | range.end = data[node].bin.rhs; 674 | }, 675 | .range_expr_step => { 676 | range.start = data[node].bin.lhs; 677 | if (data[node].bin.rhs != 0) range.step = data[node].bin.rhs; 678 | if (range.step) |some| { 679 | range.colon_2 = tree.prevToken(tree.firstToken(some)); 680 | } 681 | }, 682 | else => unreachable, 683 | } 684 | return range; 685 | } 686 | }; 687 | 688 | pub const For = struct { 689 | for_tok: Token.Index, 690 | let_tok: Token.Index, 691 | capture: ?Node.Index, 692 | in_tok: Token.Index, 693 | cond: Node.Index, 694 | body: Node.Index, 695 | 696 | pub fn get(tree: Tree, node: Node.Index) For { 697 | const tokens = tree.nodes.items(.token); 698 | var for_expr: For = undefined; 699 | for_expr.for_tok = tokens[node]; 700 | 701 | const data = tree.nodes.items(.data); 702 | if (tree.nodes.items(.id)[node] == .for_let_expr) { 703 | for_expr.let_tok = tree.nextToken(tokens[node]); 704 | for_expr.capture = tree.extra[data[node].cond.extra]; 705 | for_expr.in_tok = tree.nextToken(tree.lastToken(for_expr.capture.?)); 706 | for_expr.cond = data[node].cond.cond; 707 | for_expr.body = tree.extra[data[node].cond.extra + 1]; 708 | } else { 709 | for_expr.cond = data[node].bin.lhs; 710 | for_expr.capture = null; 711 | for_expr.body = data[node].bin.rhs; 712 | } 713 | return for_expr; 714 | } 715 | }; 716 | 717 | pub const While = struct { 718 | while_tok: Token.Index, 719 | let_tok: Token.Index, 720 | capture: ?Node.Index, 721 | eq_tok: Token.Index, 722 | cond: Node.Index, 723 | body: Node.Index, 724 | 725 | pub fn get(tree: Tree, node: Node.Index) While { 726 | const tokens = tree.nodes.items(.token); 727 | var while_expr: While = undefined; 728 | while_expr.while_tok = tokens[node]; 729 | 730 | const data = tree.nodes.items(.data); 731 | if (tree.nodes.items(.id)[node] == .while_let_expr) { 732 | while_expr.let_tok = tree.nextToken(tokens[node]); 733 | while_expr.capture = tree.extra[data[node].cond.extra]; 734 | while_expr.eq_tok = tree.nextToken(tree.lastToken(while_expr.capture.?)); 735 | while_expr.cond = data[node].cond.cond; 736 | while_expr.body = tree.extra[data[node].cond.extra + 1]; 737 | } else { 738 | while_expr.cond = data[node].bin.lhs; 739 | while_expr.capture = null; 740 | while_expr.body = data[node].bin.rhs; 741 | } 742 | return while_expr; 743 | } 744 | }; 745 | 746 | pub const If = struct { 747 | if_tok: Token.Index, 748 | let_tok: Token.Index, 749 | capture: ?Node.Index, 750 | eq_tok: Token.Index, 751 | cond: Node.Index, 752 | then_body: Node.Index, 753 | else_tok: Token.Index, 754 | else_body: ?Node.Index, 755 | 756 | pub fn get(tree: Tree, node: Node.Index) If { 757 | const tokens = tree.nodes.items(.token); 758 | var if_expr: If = undefined; 759 | if_expr.if_tok = tokens[node]; 760 | 761 | const data = tree.nodes.items(.data); 762 | switch (tree.nodes.items(.id)[node]) { 763 | .if_expr => { 764 | if_expr.capture = null; 765 | if_expr.cond = data[node].bin.lhs; 766 | if_expr.then_body = data[node].bin.rhs; 767 | if_expr.else_body = null; 768 | }, 769 | .if_else_expr => { 770 | if_expr.capture = null; 771 | if_expr.cond = data[node].cond.cond; 772 | if_expr.then_body = tree.extra[data[node].cond.extra]; 773 | if_expr.else_tok = tree.nextToken(tree.lastToken(if_expr.then_body)); 774 | if_expr.else_body = tree.extra[data[node].cond.extra + 1]; 775 | }, 776 | .if_let_expr => { 777 | if_expr.let_tok = tree.nextToken(tokens[node]); 778 | if_expr.capture = tree.extra[data[node].cond.extra]; 779 | if_expr.eq_tok = tree.nextToken(tree.lastToken(if_expr.capture.?)); 780 | if_expr.cond = data[node].cond.cond; 781 | if_expr.then_body = tree.extra[data[node].cond.extra + 1]; 782 | if_expr.else_body = null; 783 | }, 784 | .if_let_else_expr => { 785 | if_expr.let_tok = tree.nextToken(tokens[node]); 786 | if_expr.capture = tree.extra[data[node].cond.extra]; 787 | if_expr.eq_tok = tree.nextToken(tree.lastToken(if_expr.capture.?)); 788 | if_expr.cond = data[node].cond.cond; 789 | if_expr.then_body = tree.extra[data[node].cond.extra + 1]; 790 | if_expr.else_tok = tree.nextToken(tree.lastToken(if_expr.then_body)); 791 | if_expr.else_body = tree.extra[data[node].cond.extra + 2]; 792 | }, 793 | else => unreachable, 794 | } 795 | return if_expr; 796 | } 797 | }; 798 | 799 | pub fn nodeItems(tree: Tree, node: Node.Index, buf: *[2]Node.Index) []const Node.Index { 800 | const data = tree.nodes.items(.data); 801 | switch (tree.nodes.items(.id)[node]) { 802 | .tuple_expr_two, 803 | .list_expr_two, 804 | .map_expr_two, 805 | .block_stmt_two, 806 | .try_expr_one, 807 | .fn_expr_one, 808 | .call_expr_one, 809 | .async_call_expr_one, 810 | .match_expr_one, 811 | .match_case_one, 812 | => { 813 | buf[0] = data[node].bin.lhs; 814 | buf[1] = data[node].bin.rhs; 815 | if (buf[1] != 0) { 816 | return buf[0..2]; 817 | } else if (buf[0] != 0) { 818 | return buf[0..1]; 819 | } else { 820 | return buf[0..0]; 821 | } 822 | }, 823 | .match_case, 824 | .tuple_expr, 825 | .list_expr, 826 | .map_expr, 827 | .block_stmt, 828 | .try_expr, 829 | .fn_expr, 830 | .call_expr, 831 | .async_call_expr, 832 | .match_expr, 833 | => return tree.extra[data[node].range.start..data[node].range.end], 834 | else => unreachable, 835 | } 836 | } 837 | -------------------------------------------------------------------------------- /src/bog.zig: -------------------------------------------------------------------------------- 1 | const zig_std = @import("std"); 2 | const Allocator = zig_std.mem.Allocator; 3 | 4 | const tokenizer = @import("tokenizer.zig"); 5 | pub const Token = tokenizer.Token; 6 | pub const Tokenizer = tokenizer.Tokenizer; 7 | pub const tokenize = tokenizer.tokenize; 8 | 9 | const parser = @import("parser.zig"); 10 | pub const Parser = parser.Parser; 11 | pub const parse = parser.parse; 12 | 13 | pub const Tree = @import("Tree.zig"); 14 | pub const Node = Tree.Node; 15 | 16 | pub const Compiler = @import("Compiler.zig"); 17 | pub const compile = Compiler.compile; 18 | 19 | const value = @import("value.zig"); 20 | pub const Value = value.Value; 21 | pub const Type = value.Type; 22 | 23 | pub const Vm = @import("Vm.zig"); 24 | 25 | pub const Gc = @import("Gc.zig"); 26 | 27 | pub const Bytecode = @import("Bytecode.zig"); 28 | 29 | pub const repl = @import("repl.zig"); 30 | 31 | pub const std = @import("std.zig"); 32 | 33 | /// file extension of bog text files 34 | pub const extension = ".bog"; 35 | 36 | pub const version = zig_std.builtin.Version{ 37 | .major = 0, 38 | .minor = 0, 39 | .patch = 1, 40 | }; 41 | 42 | pub const Errors = struct { 43 | list: List = .{}, 44 | arena: zig_std.heap.ArenaAllocator, 45 | 46 | pub const Kind = enum { 47 | err, 48 | note, 49 | trace, 50 | }; 51 | 52 | const List = zig_std.ArrayListUnmanaged(struct { 53 | msg: @import("String.zig"), 54 | line_num: u32, 55 | col_num: u32, 56 | kind: Kind, 57 | path: []const u8, 58 | line: []const u8, 59 | }); 60 | 61 | pub fn init(alloc: Allocator) Errors { 62 | return .{ .arena = zig_std.heap.ArenaAllocator.init(alloc) }; 63 | } 64 | 65 | pub fn deinit(self: *Errors) void { 66 | for (self.list.items) |*err| { 67 | err.msg.deinit(self.arena.child_allocator); 68 | } 69 | self.list.deinit(self.arena.child_allocator); 70 | self.arena.deinit(); 71 | } 72 | 73 | pub fn add( 74 | self: *Errors, 75 | msg: @import("String.zig"), 76 | source: []const u8, 77 | path: []const u8, 78 | byte_offset: u32, 79 | kind: Kind, 80 | ) !void { 81 | var start: u32 = 0; 82 | // find the start of the line which is either a newline or a splice 83 | var line_num: u32 = 1; 84 | var i: u32 = 0; 85 | while (i < byte_offset) : (i += 1) { 86 | if (source[i] == '\n') { 87 | start = i + 1; 88 | line_num += 1; 89 | } 90 | } 91 | const col_num = byte_offset - start; 92 | 93 | // find the end of the line 94 | while (i < source.len) : (i += 1) { 95 | if (source[i] == '\n') break; 96 | } 97 | try self.list.append(self.arena.child_allocator, .{ 98 | .msg = msg, 99 | .line = try self.arena.allocator().dupe(u8, source[start..i]), 100 | .path = try self.arena.allocator().dupe(u8, path), 101 | .line_num = line_num, 102 | .col_num = col_num + 1, 103 | .kind = kind, 104 | }); 105 | } 106 | 107 | pub fn render(self: *Errors, writer: anytype) !void { 108 | // TODO should be an arg 109 | const tty = zig_std.io.tty.detectConfig(zig_std.io.getStdErr()); 110 | const gpa = self.arena.child_allocator; 111 | for (self.list.items) |*e| { 112 | const prefix = if (zig_std.fs.path.dirname(e.path) == null and e.path[0] != '<') "." ++ zig_std.fs.path.sep_str else ""; 113 | switch (e.kind) { 114 | .err => { 115 | try tty.setColor(writer, .white); 116 | try writer.print("{s}{s}:{d}:{d}: ", .{ prefix, e.path, e.line_num, e.col_num }); 117 | try tty.setColor(writer, .red); 118 | try writer.writeAll("error: "); 119 | try tty.setColor(writer, .white); 120 | }, 121 | .note => { 122 | try tty.setColor(writer, .white); 123 | try writer.print("{s}{s}:{d}:{d}: ", .{ prefix, e.path, e.line_num, e.col_num }); 124 | try tty.setColor(writer, .cyan); 125 | try writer.writeAll("note: "); 126 | try tty.setColor(writer, .white); 127 | }, 128 | .trace => { 129 | try writer.print("{s}{s}:{d}:{d}: ", .{ prefix, e.path, e.line_num, e.col_num }); 130 | }, 131 | } 132 | try writer.print("{s}\n", .{e.msg.data}); 133 | try tty.setColor(writer, .reset); 134 | 135 | try writer.print("{s}\n", .{e.line}); 136 | try writer.writeByteNTimes(' ', e.col_num - 1); 137 | if (e.kind != .trace) try tty.setColor(writer, .green); 138 | try writer.writeAll("^\n"); 139 | try tty.setColor(writer, .reset); 140 | e.msg.deinit(gpa); 141 | } 142 | self.list.items.len = 0; 143 | self.arena.deinit(); 144 | self.arena = zig_std.heap.ArenaAllocator.init(gpa); 145 | } 146 | }; 147 | -------------------------------------------------------------------------------- /src/lib.zig: -------------------------------------------------------------------------------- 1 | //! ABI WARNING -- REMEMBER TO CHANGE include/bog.h 2 | const std = @import("std"); 3 | const span = std.mem.span; 4 | const bog = @import("bog.zig"); 5 | const gpa = std.heap.c_allocator; 6 | 7 | const Error = enum(c_int) { 8 | None, 9 | OutOfMemory, 10 | TokenizeError, 11 | ParseError, 12 | CompileError, 13 | RuntimeError, 14 | NotAMap, 15 | NoSuchMember, 16 | NotAFunction, 17 | InvalidArgCount, 18 | NativeFunctionsUnsupported, 19 | IoError, 20 | }; 21 | 22 | export fn bog_Vm_init(vm: **bog.Vm, import_files: bool) Error { 23 | const ptr = gpa.create(bog.Vm) catch |e| switch (e) { 24 | error.OutOfMemory => return .OutOfMemory, 25 | }; 26 | ptr.* = bog.Vm.init(gpa, .{ .import_files = import_files }); 27 | 28 | vm.* = ptr; 29 | return .None; 30 | } 31 | 32 | export fn bog_Vm_deinit(vm: *bog.Vm) void { 33 | vm.deinit(); 34 | } 35 | 36 | fn bog_Vm_addStd(vm: *bog.Vm) callconv(.C) Error { 37 | vm.addStd() catch |e| switch (e) { 38 | error.OutOfMemory => return .OutOfMemory, 39 | }; 40 | return .None; 41 | } 42 | 43 | fn bog_Vm_addStdNoIo(vm: *bog.Vm) callconv(.C) Error { 44 | vm.addStdNoIo() catch |e| switch (e) { 45 | error.OutOfMemory => return .OutOfMemory, 46 | }; 47 | return .None; 48 | } 49 | 50 | comptime { 51 | const build_options = @import("build_options"); 52 | if (!build_options.no_std) 53 | @export(&bog_Vm_addStd, .{ .name = "bog_Vm_addStd" }); 54 | if (!build_options.no_std_no_io) 55 | @export(&bog_Vm_addStdNoIo, .{ .name = "bog_Vm_addStdNoIo" }); 56 | } 57 | 58 | export fn bog_Vm_run(vm: *bog.Vm, res: **bog.Value, source: [*:0]const u8) Error { 59 | res.* = vm.compileAndRun(span(source)) catch |e| switch (e) { 60 | error.OutOfMemory => return .OutOfMemory, 61 | error.TokenizeError => return .TokenizeError, 62 | error.ParseError => return .ParseError, 63 | error.CompileError => return .CompileError, 64 | else => return .RuntimeError, 65 | }; 66 | 67 | return .None; 68 | } 69 | 70 | export fn bog_Vm_call(vm: *bog.Vm, res: **bog.Value, container: *bog.Value, func_name: [*:0]const u8) Error { 71 | _ = vm; 72 | _ = res; 73 | _ = container; 74 | _ = func_name; 75 | if (true) return .RuntimeError; 76 | // res.* = vm.run(container, span(func_name), .{}) catch |e| switch (e) { 77 | // error.OutOfMemory => return .OutOfMemory, 78 | // error.NotAMap => return .NotAMap, 79 | // error.NoSuchMember => return .NoSuchMember, 80 | // error.NotAFunction => return .NotAFunction, 81 | // error.InvalidArgCount => return .InvalidArgCount, 82 | // error.NativeFunctionsUnsupported => return .NativeFunctionsUnsupported, 83 | // }; 84 | 85 | return .None; 86 | } 87 | 88 | export fn bog_Vm_renderErrors(vm: *bog.Vm, out: *std.c.FILE) Error { 89 | vm.errors.render(std.io.cWriter(out)) catch return .IoError; 90 | 91 | return .None; 92 | } 93 | 94 | export fn bog_Errors_init(errors: **bog.Errors) Error { 95 | const ptr = gpa.create(bog.Errors) catch |e| switch (e) { 96 | error.OutOfMemory => return .OutOfMemory, 97 | }; 98 | ptr.* = bog.Errors.init(gpa); 99 | 100 | errors.* = ptr; 101 | return .None; 102 | } 103 | 104 | export fn bog_Errors_deinit(errors: *bog.Errors) void { 105 | errors.deinit(); 106 | } 107 | 108 | export fn bog_Errors_render(errors: *bog.Errors, out: *std.c.FILE) Error { 109 | errors.render(std.io.cWriter(out)) catch return .IoError; 110 | 111 | return .None; 112 | } 113 | 114 | export fn bog_parse(tree: **bog.Tree, source: [*:0]const u8, errors: *bog.Errors) Error { 115 | tree.* = gpa.create(bog.Tree) catch return .OutOfMemory; 116 | tree.*.* = bog.parse(gpa, span(source), "dunno?", errors) catch |e| switch (e) { 117 | error.OutOfMemory => return .OutOfMemory, 118 | error.TokenizeError => return .TokenizeError, 119 | error.ParseError => return .ParseError, 120 | error.NeedInput => unreachable, 121 | }; 122 | 123 | return .None; 124 | } 125 | 126 | export fn bog_Tree_deinit(tree: *bog.Tree) void { 127 | tree.deinit(gpa); 128 | gpa.destroy(tree); 129 | } 130 | 131 | export fn bog_Tree_render(tree: *bog.Tree, out: *std.c.FILE, changed: ?*bool) Error { 132 | const c = tree.render(std.io.cWriter(out)) catch return .IoError; 133 | if (changed) |some| { 134 | some.* = c; 135 | } 136 | return .None; 137 | } 138 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const process = std.process; 3 | const mem = std.mem; 4 | const bog = @import("bog"); 5 | const repl = bog.repl; 6 | 7 | const is_debug = @import("builtin").mode == .Debug; 8 | var state = std.heap.GeneralPurposeAllocator(.{}){}; 9 | 10 | pub fn main() !void { 11 | const gpa = state.allocator(); 12 | 13 | const args = try process.argsAlloc(gpa); 14 | defer process.argsFree(gpa, args); 15 | 16 | if (args.len > 1) { 17 | if (mem.eql(u8, args[1], "fmt")) { 18 | return fmt(gpa, args[2..]); 19 | } 20 | if (mem.eql(u8, args[1], "help") or mem.eql(u8, args[1], "--help")) { 21 | return help(); 22 | } 23 | if (is_debug) { 24 | if (mem.eql(u8, args[1], "debug:dump")) { 25 | return debugDump(gpa, args[2..]); 26 | } 27 | if (mem.eql(u8, args[1], "debug:tokens")) { 28 | return debugTokens(gpa, args[2..]); 29 | } 30 | } 31 | if (!mem.startsWith(u8, "-", args[1])) { 32 | return run(gpa, args[1..]); 33 | } 34 | } 35 | 36 | try repl.run(gpa, std.io.getStdIn(), std.io.getStdOut()); 37 | } 38 | 39 | const usage = 40 | \\usage: bog [command] [options] [-- [args]] 41 | \\ 42 | \\Commands: 43 | \\ 44 | \\ fmt [source] Parse file and render it 45 | \\ run [source] Run file 46 | \\ 47 | \\ 48 | ; 49 | 50 | fn help() !void { 51 | try std.io.getStdOut().writer().writeAll(usage); 52 | process.exit(0); 53 | } 54 | 55 | fn run(gpa: std.mem.Allocator, args: []const []const u8) !void { 56 | std.debug.assert(args.len > 0); 57 | const file_name = args[0]; 58 | 59 | var vm = bog.Vm.init(gpa, .{ .import_files = true }); 60 | defer vm.deinit(); 61 | try vm.addStd(); 62 | const S = struct { 63 | var _args: []const []const u8 = undefined; 64 | 65 | fn argsToBog(ctx: bog.Vm.Context) bog.Vm.Error!*bog.Value { 66 | const ret = try ctx.vm.gc.alloc(.list); 67 | ret.* = .{ .list = .{} }; 68 | try ret.list.inner.ensureTotalCapacity(ctx.vm.gc.gpa, _args.len); 69 | 70 | for (_args) |arg| { 71 | const str = try ctx.vm.gc.alloc(.str); 72 | str.* = bog.Value.string(arg); 73 | ret.list.inner.appendAssumeCapacity(str); 74 | } 75 | return ret; 76 | } 77 | }; 78 | S._args = args[0..]; 79 | try vm.imports.putNoClobber(vm.gc.gpa, "args", S.argsToBog); 80 | 81 | const res = vm.compileAndRun(file_name) catch |e| switch (e) { 82 | error.FatalError, error.TokenizeError, error.ParseError, error.CompileError => { 83 | vm.errors.render(std.io.getStdErr().writer()) catch {}; 84 | process.exit(1); 85 | }, 86 | error.OutOfMemory => return error.OutOfMemory, 87 | else => { 88 | fatal("cannot run '{s}': {s}", .{ file_name, @errorName(e) }); 89 | }, 90 | }; 91 | 92 | switch (res.*) { 93 | .int => |int| { 94 | if (int >= 0 and int < std.math.maxInt(u8)) { 95 | process.exit(@intCast(int)); 96 | } else { 97 | fatal("invalid exit code: {}", .{int}); 98 | } 99 | }, 100 | .err => |err| { 101 | const stderr = std.io.getStdErr().writer(); 102 | try stderr.writeAll("script exited with error: "); 103 | try err.dump(stderr, 4); 104 | try stderr.writeAll("\n"); 105 | process.exit(1); 106 | }, 107 | .null => {}, 108 | else => fatal("invalid return type '{s}'", .{res.typeName()}), 109 | } 110 | } 111 | 112 | const usage_fmt = 113 | \\usage: bog fmt [file]... 114 | \\ 115 | \\ Formats the input files. 116 | \\ 117 | ; 118 | 119 | fn fmt(gpa: std.mem.Allocator, args: []const []const u8) !void { 120 | if (args.len == 0) fatal("expected at least one file", .{}); 121 | 122 | var any_err = false; 123 | for (args) |arg| { 124 | any_err = (try fmtFile(gpa, arg)) or any_err; 125 | } 126 | if (any_err) process.exit(1); 127 | } 128 | 129 | const FmtError = std.mem.Allocator.Error || std.fs.File.OpenError || std.fs.File.Writer.Error || 130 | std.fs.Dir.OpenError || std.fs.File.GetSeekPosError || std.fs.Dir.Iterator.Error || 131 | std.fs.File.Reader.Error || error{EndOfStream}; 132 | 133 | fn fmtFile(gpa: std.mem.Allocator, name: []const u8) FmtError!bool { 134 | const source = std.fs.cwd().readFileAlloc(gpa, name, 1024 * 1024) catch |e| switch (e) { 135 | error.OutOfMemory => return error.OutOfMemory, 136 | error.IsDir => { 137 | var dir = std.fs.cwd().openDir(name, .{ .iterate = true }) catch |e2| { 138 | try std.io.getStdErr().writer().print("unable to open '{s}': {}\n", .{ name, e2 }); 139 | return e2; 140 | }; 141 | var any_err = false; 142 | defer dir.close(); 143 | var it = dir.iterate(); 144 | while (try it.next()) |entry| if (entry.kind == .directory or std.mem.endsWith(u8, entry.name, bog.extension)) { 145 | const full_path = try std.fs.path.join(gpa, &[_][]const u8{ name, entry.name }); 146 | defer gpa.free(full_path); 147 | any_err = (try fmtFile(gpa, full_path)) or any_err; 148 | }; 149 | return any_err; 150 | }, 151 | else => |err| { 152 | try std.io.getStdErr().writer().print("unable to open '{s}': {}\n", .{ name, err }); 153 | return err; 154 | }, 155 | }; 156 | defer gpa.free(source); 157 | 158 | var errors = bog.Errors.init(gpa); 159 | defer errors.deinit(); 160 | 161 | var tree = bog.parse(gpa, source, name, &errors) catch |e| switch (e) { 162 | error.NeedInput => unreachable, 163 | error.TokenizeError, error.ParseError => { 164 | try errors.render(std.io.getStdErr().writer()); 165 | return true; 166 | }, 167 | error.OutOfMemory => return error.OutOfMemory, 168 | }; 169 | defer tree.deinit(gpa); 170 | 171 | var buf = std.ArrayList(u8).init(gpa); 172 | defer buf.deinit(); 173 | 174 | // TODO add check mode 175 | _ = try tree.render(buf.writer()); 176 | 177 | const file = try std.fs.cwd().createFile(name, .{}); 178 | defer file.close(); 179 | 180 | try file.writeAll(buf.items); 181 | return false; 182 | } 183 | 184 | fn fatal(comptime msg: []const u8, args: anytype) noreturn { 185 | std.io.getStdErr().writer().print(msg ++ "\n", args) catch {}; 186 | process.exit(1); 187 | } 188 | 189 | fn getFileName(usage_arg: []const u8, args: []const []const u8) []const u8 { 190 | if (args.len != 1) { 191 | fatal("{s}", .{usage_arg}); 192 | } 193 | return args[0]; 194 | } 195 | 196 | const usage_debug = 197 | \\usage: bog debug:[command] [file] 198 | \\ 199 | ; 200 | 201 | fn debugDump(gpa: std.mem.Allocator, args: []const []const u8) !void { 202 | const file_name = getFileName(usage_debug, args); 203 | 204 | var errors = bog.Errors.init(gpa); 205 | defer errors.deinit(); 206 | var mod = mod: { 207 | const source = std.fs.cwd().readFileAlloc(gpa, file_name, 1024 * 1024) catch |e| switch (e) { 208 | error.OutOfMemory => return error.OutOfMemory, 209 | else => |err| { 210 | fatal("unable to open '{s}': {}", .{ file_name, err }); 211 | }, 212 | }; 213 | errdefer gpa.free(source); 214 | 215 | break :mod bog.compile(gpa, source, file_name, &errors) catch |e| switch (e) { 216 | error.OutOfMemory => return error.OutOfMemory, 217 | error.TokenizeError, error.ParseError, error.CompileError => { 218 | try errors.render(std.io.getStdErr().writer()); 219 | process.exit(1); 220 | }, 221 | }; 222 | }; 223 | defer mod.deinit(gpa); 224 | 225 | mod.dump(mod.main, 0); 226 | } 227 | 228 | fn debugTokens(gpa: std.mem.Allocator, args: []const []const u8) !void { 229 | const file_name = getFileName(usage_debug, args); 230 | 231 | const source = std.fs.cwd().readFileAlloc(gpa, file_name, 1024 * 1024) catch |e| switch (e) { 232 | error.OutOfMemory => return error.OutOfMemory, 233 | else => |err| { 234 | fatal("unable to open '{s}': {}", .{ file_name, err }); 235 | }, 236 | }; 237 | defer gpa.free(source); 238 | 239 | var errors = bog.Errors.init(gpa); 240 | defer errors.deinit(); 241 | 242 | var tokens = bog.tokenize(gpa, source, file_name, &errors) catch |e| switch (e) { 243 | error.OutOfMemory => return error.OutOfMemory, 244 | error.TokenizeError => { 245 | try errors.render(std.io.getStdErr().writer()); 246 | process.exit(1); 247 | }, 248 | }; 249 | defer tokens.deinit(gpa); 250 | 251 | const stream = std.io.getStdOut().writer(); 252 | const starts = tokens.items(.start); 253 | const ends = tokens.items(.end); 254 | for (tokens.items(.id), 0..) |id, i| { 255 | switch (id) { 256 | .nl, 257 | .eof, 258 | // zig fmt: off 259 | .indent_1, .indent_2, .indent_3, .indent_4, .indent_5, 260 | .indent_6, .indent_7, .indent_8, .indent_9, .indent_10, 261 | .indent_11, .indent_12, .indent_13, .indent_14, .indent_15, 262 | .indent_16, .indent_17, .indent_18, .indent_19, .indent_20, 263 | .indent_21, .indent_22, .indent_23, .indent_24, .indent_25, 264 | .indent_26, .indent_27, .indent_28, .indent_29, .indent_30, 265 | .indent_31, .indent_32, 266 | // zig fmt: on 267 | => try stream.print("{s}\n", .{@tagName(id)}), 268 | else => try stream.print("{s} |{s}|\n", .{ @tagName(id), source[starts[i]..ends[i]] }), 269 | } 270 | } 271 | } 272 | 273 | comptime { 274 | _ = main; 275 | } 276 | -------------------------------------------------------------------------------- /src/multi_array_list.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | const assert = std.debug.assert; 4 | const meta = std.meta; 5 | const mem = std.mem; 6 | const Allocator = mem.Allocator; 7 | const testing = std.testing; 8 | 9 | /// A MultiArrayList stores a list of a struct type. 10 | /// Instead of storing a single list of items, MultiArrayList 11 | /// stores separate lists for each field of the struct. 12 | /// This allows for memory savings if the struct has padding, 13 | /// and also improves cache usage if only some fields are needed 14 | /// for a computation. The primary API for accessing fields is 15 | /// the `slice()` function, which computes the start pointers 16 | /// for the array of each field. From the slice you can call 17 | /// `.items(.)` to obtain a slice of field values. 18 | pub fn MultiArrayList(comptime S: type) type { 19 | return struct { 20 | bytes: [*]align(@alignOf(S)) u8 = undefined, 21 | len: u32 = 0, 22 | capacity: u32 = 0, 23 | 24 | pub const Elem = S; 25 | 26 | pub const Field = meta.FieldEnum(S); 27 | 28 | /// A MultiArrayList.Slice contains cached start pointers for each field in the list. 29 | /// These pointers are not normally stored to reduce the size of the list in memory. 30 | /// If you are accessing multiple fields, call slice() first to compute the pointers, 31 | /// and then get the field arrays from the slice. 32 | pub const Slice = struct { 33 | /// This array is indexed by the field index which can be obtained 34 | /// by using @intFromEnum() on the Field enum 35 | ptrs: [fields.len][*]u8, 36 | len: u32, 37 | capacity: u32, 38 | 39 | pub fn items(self: Slice, comptime field: Field) []FieldType(field) { 40 | const F = FieldType(field); 41 | if (self.capacity == 0) { 42 | return &[_]F{}; 43 | } 44 | const byte_ptr = self.ptrs[@intFromEnum(field)]; 45 | const casted_ptr: [*]F = if (@sizeOf(F) == 0) 46 | undefined 47 | else 48 | @ptrCast(@alignCast(byte_ptr)); 49 | return casted_ptr[0..self.len]; 50 | } 51 | 52 | pub fn toMultiArrayList(self: Slice) Self { 53 | if (self.ptrs.len == 0) { 54 | return .{}; 55 | } 56 | const unaligned_ptr = self.ptrs[sizes.fields[0]]; 57 | return .{ 58 | .bytes = @ptrCast(@alignCast(unaligned_ptr)), 59 | .len = self.len, 60 | .capacity = self.capacity, 61 | }; 62 | } 63 | 64 | pub fn deinit(self: *Slice, gpa: Allocator) void { 65 | var other = self.toMultiArrayList(); 66 | other.deinit(gpa); 67 | self.* = undefined; 68 | } 69 | }; 70 | 71 | const Self = @This(); 72 | 73 | const fields = meta.fields(S); 74 | /// `sizes.bytes` is an array of @sizeOf each S field. Sorted by alignment, descending. 75 | /// `sizes.fields` is an array mapping from `sizes.bytes` array index to field index. 76 | const sizes = blk: { 77 | const Data = struct { 78 | size: u32, 79 | size_index: u32, 80 | alignment: u32, 81 | }; 82 | var data: [fields.len]Data = undefined; 83 | for (fields, 0..) |field_info, i| { 84 | data[i] = .{ 85 | .size = @sizeOf(field_info.type), 86 | .size_index = i, 87 | .alignment = if (@sizeOf(field_info.type) == 0) 1 else field_info.alignment, 88 | }; 89 | } 90 | const Sort = struct { 91 | fn lessThan(_: void, lhs: Data, rhs: Data) bool { 92 | return lhs.alignment > rhs.alignment; 93 | } 94 | }; 95 | std.mem.sort(Data, &data, {}, Sort.lessThan); 96 | var sizes_bytes: [fields.len]u32 = undefined; 97 | var field_indexes: [fields.len]u32 = undefined; 98 | for (data, 0..) |elem, i| { 99 | sizes_bytes[i] = elem.size; 100 | field_indexes[i] = elem.size_index; 101 | } 102 | break :blk .{ 103 | .bytes = sizes_bytes, 104 | .fields = field_indexes, 105 | }; 106 | }; 107 | 108 | /// Release all allocated memory. 109 | pub fn deinit(self: *Self, gpa: Allocator) void { 110 | gpa.free(self.allocatedBytes()); 111 | self.* = undefined; 112 | } 113 | 114 | /// The caller owns the returned memory. Empties this MultiArrayList. 115 | pub fn toOwnedSlice(self: *Self) Slice { 116 | const result = self.slice(); 117 | self.* = .{}; 118 | return result; 119 | } 120 | 121 | /// Compute pointers to the start of each field of the array. 122 | /// If you need to access multiple fields, calling this may 123 | /// be more efficient than calling `items()` multiple times. 124 | pub fn slice(self: Self) Slice { 125 | var result: Slice = .{ 126 | .ptrs = undefined, 127 | .len = self.len, 128 | .capacity = self.capacity, 129 | }; 130 | var ptr: [*]u8 = self.bytes; 131 | for (sizes.bytes, 0..) |field_size, i| { 132 | result.ptrs[sizes.fields[i]] = ptr; 133 | ptr += field_size * self.capacity; 134 | } 135 | return result; 136 | } 137 | 138 | /// Get the slice of values for a specified field. 139 | /// If you need multiple fields, consider calling slice() 140 | /// instead. 141 | pub fn items(self: Self, comptime field: Field) []FieldType(field) { 142 | return self.slice().items(field); 143 | } 144 | 145 | /// Overwrite one array element with new data. 146 | pub fn set(self: *Self, index: u32, elem: S) void { 147 | const slices = self.slice(); 148 | inline for (fields, 0..) |field_info, i| { 149 | slices.items(@enumFromInt(i))[index] = @field(elem, field_info.name); 150 | } 151 | } 152 | 153 | /// Obtain all the data for one array element. 154 | pub fn get(self: Self, index: u32) S { 155 | const slices = self.slice(); 156 | var result: S = undefined; 157 | inline for (fields, 0..) |field_info, i| { 158 | @field(result, field_info.name) = slices.items(@enumFromInt(i))[index]; 159 | } 160 | return result; 161 | } 162 | 163 | /// Extend the list by 1 element. Allocates more memory as necessary. 164 | pub fn append(self: *Self, gpa: Allocator, elem: S) !void { 165 | try self.ensureUnusedCapacity(gpa, 1); 166 | self.appendAssumeCapacity(elem); 167 | } 168 | 169 | /// Extend the list by 1 element, but asserting `self.capacity` 170 | /// is sufficient to hold an additional item. 171 | pub fn appendAssumeCapacity(self: *Self, elem: S) void { 172 | assert(self.len < self.capacity); 173 | self.len += 1; 174 | self.set(self.len - 1, elem); 175 | } 176 | 177 | /// Extend the list by 1 element, asserting `self.capacity` 178 | /// is sufficient to hold an additional item. Returns the 179 | /// newly reserved index with uninitialized data. 180 | pub fn addOneAssumeCapacity(self: *Self) u32 { 181 | assert(self.len < self.capacity); 182 | const index = self.len; 183 | self.len += 1; 184 | return index; 185 | } 186 | 187 | /// Inserts an item into an ordered list. Shifts all elements 188 | /// after and including the specified index back by one and 189 | /// sets the given index to the specified element. May reallocate 190 | /// and invalidate iterators. 191 | pub fn insert(self: *Self, gpa: Allocator, index: u32, elem: S) !void { 192 | try self.ensureUnusedCapacity(gpa, 1); 193 | self.insertAssumeCapacity(index, elem); 194 | } 195 | 196 | /// Inserts an item into an ordered list which has room for it. 197 | /// Shifts all elements after and including the specified index 198 | /// back by one and sets the given index to the specified element. 199 | /// Will not reallocate the array, does not invalidate iterators. 200 | pub fn insertAssumeCapacity(self: *Self, index: u32, elem: S) void { 201 | assert(self.len < self.capacity); 202 | assert(index <= self.len); 203 | self.len += 1; 204 | const slices = self.slice(); 205 | inline for (fields, 0..) |field_info, field_index| { 206 | const field_slice = slices.items(@enumFromInt(field_index)); 207 | var i: u32 = self.len - 1; 208 | while (i > index) : (i -= 1) { 209 | field_slice[i] = field_slice[i - 1]; 210 | } 211 | field_slice[index] = @field(elem, field_info.name); 212 | } 213 | } 214 | 215 | /// Remove the specified item from the list, swapping the last 216 | /// item in the list into its position. Fast, but does not 217 | /// retain list ordering. 218 | pub fn swapRemove(self: *Self, index: u32) void { 219 | const slices = self.slice(); 220 | inline for (fields, 0..) |_, i| { 221 | const field_slice = slices.items(@enumFromInt(i)); 222 | field_slice[index] = field_slice[self.len - 1]; 223 | field_slice[self.len - 1] = undefined; 224 | } 225 | self.len -= 1; 226 | } 227 | 228 | /// Remove the specified item from the list, shifting items 229 | /// after it to preserve order. 230 | pub fn orderedRemove(self: *Self, index: u32) void { 231 | const slices = self.slice(); 232 | inline for (fields, 0..) |_, field_index| { 233 | const field_slice = slices.items(@enumFromInt(field_index)); 234 | var i = index; 235 | while (i < self.len - 1) : (i += 1) { 236 | field_slice[i] = field_slice[i + 1]; 237 | } 238 | field_slice[i] = undefined; 239 | } 240 | self.len -= 1; 241 | } 242 | 243 | /// Adjust the list's length to `new_len`. 244 | /// Does not initialize added items, if any. 245 | pub fn resize(self: *Self, gpa: Allocator, new_len: u32) !void { 246 | try self.ensureTotalCapacity(gpa, new_len); 247 | self.len = new_len; 248 | } 249 | 250 | /// Attempt to reduce allocated capacity to `new_len`. 251 | /// If `new_len` is greater than zero, this may fail to reduce the capacity, 252 | /// but the data remains intact and the length is updated to new_len. 253 | pub fn shrinkAndFree(self: *Self, gpa: Allocator, new_len: u32) void { 254 | if (new_len == 0) { 255 | gpa.free(self.allocatedBytes()); 256 | self.* = .{}; 257 | return; 258 | } 259 | assert(new_len <= self.capacity); 260 | assert(new_len <= self.len); 261 | 262 | const other_bytes = gpa.allocAdvanced( 263 | u8, 264 | @alignOf(S), 265 | capacityInBytes(new_len), 266 | .exact, 267 | ) catch { 268 | const self_slice = self.slice(); 269 | inline for (fields, 0..) |field_info, i| { 270 | if (@sizeOf(field_info.type) != 0) { 271 | const field: Field = @enumFromInt(i); 272 | const dest_slice = self_slice.items(field)[new_len..]; 273 | // We use memset here for more efficient codegen in safety-checked, 274 | // valgrind-enabled builds. Otherwise the valgrind client request 275 | // will be repeated for every element. 276 | @memset(dest_slice, undefined); 277 | } 278 | } 279 | self.len = new_len; 280 | return; 281 | }; 282 | var other = Self{ 283 | .bytes = other_bytes.ptr, 284 | .capacity = new_len, 285 | .len = new_len, 286 | }; 287 | self.len = new_len; 288 | const self_slice = self.slice(); 289 | const other_slice = other.slice(); 290 | inline for (fields, 0..) |field_info, i| { 291 | if (@sizeOf(field_info.type) != 0) { 292 | const field: Field = @enumFromInt(i); 293 | @memcpy(other_slice.items(field), self_slice.items(field)); 294 | } 295 | } 296 | gpa.free(self.allocatedBytes()); 297 | self.* = other; 298 | } 299 | 300 | /// Reduce length to `new_len`. 301 | /// Invalidates pointers to elements `items[new_len..]`. 302 | /// Keeps capacity the same. 303 | pub fn shrinkRetainingCapacity(self: *Self, new_len: u32) void { 304 | self.len = new_len; 305 | } 306 | 307 | /// Modify the array so that it can hold at least `new_capacity` items. 308 | /// Implements super-linear growth to achieve amortized O(1) append operations. 309 | /// Invalidates pointers if additional memory is needed. 310 | pub fn ensureTotalCapacity(self: *Self, gpa: Allocator, new_capacity: u32) !void { 311 | var better_capacity = self.capacity; 312 | if (better_capacity >= new_capacity) return; 313 | 314 | while (true) { 315 | better_capacity += better_capacity / 2 + 8; 316 | if (better_capacity >= new_capacity) break; 317 | } 318 | 319 | return self.setCapacity(gpa, better_capacity); 320 | } 321 | 322 | /// Modify the array so that it can hold at least `additional_count` **more** items. 323 | /// Invalidates pointers if additional memory is needed. 324 | pub fn ensureUnusedCapacity(self: *Self, gpa: Allocator, additional_count: u32) !void { 325 | return self.ensureTotalCapacity(gpa, self.len + additional_count); 326 | } 327 | 328 | /// Modify the array so that it can hold exactly `new_capacity` items. 329 | /// Invalidates pointers if additional memory is needed. 330 | /// `new_capacity` must be greater or equal to `len`. 331 | pub fn setCapacity(self: *Self, gpa: Allocator, new_capacity: u32) !void { 332 | assert(new_capacity >= self.len); 333 | const new_bytes = try gpa.alignedAlloc( 334 | u8, 335 | @alignOf(S), 336 | capacityInBytes(new_capacity), 337 | ); 338 | if (self.len == 0) { 339 | gpa.free(self.allocatedBytes()); 340 | self.bytes = new_bytes.ptr; 341 | self.capacity = new_capacity; 342 | return; 343 | } 344 | var other = Self{ 345 | .bytes = new_bytes.ptr, 346 | .capacity = new_capacity, 347 | .len = self.len, 348 | }; 349 | const self_slice = self.slice(); 350 | const other_slice = other.slice(); 351 | inline for (fields, 0..) |field_info, i| { 352 | if (@sizeOf(field_info.type) != 0) { 353 | const field: Field = @enumFromInt(i); 354 | @memcpy(other_slice.items(field), self_slice.items(field)); 355 | } 356 | } 357 | gpa.free(self.allocatedBytes()); 358 | self.* = other; 359 | } 360 | 361 | /// Create a copy of this list with a new backing store, 362 | /// using the specified allocator. 363 | pub fn clone(self: Self, gpa: Allocator) !Self { 364 | var result = Self{}; 365 | errdefer result.deinit(gpa); 366 | try result.ensureTotalCapacity(gpa, self.len); 367 | result.len = self.len; 368 | const self_slice = self.slice(); 369 | const result_slice = result.slice(); 370 | inline for (fields, 0..) |field_info, i| { 371 | if (@sizeOf(field_info.type) != 0) { 372 | const field: Field = @enumFromInt(i); 373 | @memcpy(result_slice.items(field), self_slice.items(field)); 374 | } 375 | } 376 | return result; 377 | } 378 | 379 | /// `ctx` has the following method: 380 | /// `fn lessThan(ctx: @TypeOf(ctx), a_index: u32, b_index: u32) bool` 381 | pub fn sort(self: Self, ctx: anytype) void { 382 | const SortContext = struct { 383 | sub_ctx: @TypeOf(ctx), 384 | slice: Slice, 385 | 386 | pub fn swap(sc: @This(), a_index: u32, b_index: u32) void { 387 | inline for (fields, 0..) |field_info, i| { 388 | if (@sizeOf(field_info.type) != 0) { 389 | const field: Field = @enumFromInt(i); 390 | const ptr = sc.slice.items(field); 391 | mem.swap(field_info.type, &ptr[a_index], &ptr[b_index]); 392 | } 393 | } 394 | } 395 | 396 | pub fn lessThan(sc: @This(), a_index: u32, b_index: u32) bool { 397 | return sc.sub_ctx.lessThan(a_index, b_index); 398 | } 399 | }; 400 | 401 | std.sort.sortContext(self.len, SortContext{ 402 | .sub_ctx = ctx, 403 | .slice = self.slice(), 404 | }); 405 | } 406 | 407 | fn capacityInBytes(capacity: u32) u32 { 408 | if (builtin.zig_backend == .stage2_c) { 409 | var bytes: u32 = 0; 410 | for (sizes.bytes) |size| bytes += size * capacity; 411 | return bytes; 412 | } else { 413 | const sizes_vector: @Vector(sizes.bytes.len, u32) = sizes.bytes; 414 | const capacity_vector: @Vector(sizes.bytes.len, u32) = @splat(capacity); 415 | return @reduce(.Add, capacity_vector * sizes_vector); 416 | } 417 | } 418 | 419 | fn allocatedBytes(self: Self) []align(@alignOf(S)) u8 { 420 | return self.bytes[0..capacityInBytes(self.capacity)]; 421 | } 422 | 423 | fn FieldType(comptime field: Field) type { 424 | return meta.fieldInfo(S, field).type; 425 | } 426 | }; 427 | } 428 | -------------------------------------------------------------------------------- /src/repl.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const File = std.fs.File; 3 | const ArrayList = std.ArrayList; 4 | const Allocator = std.mem.Allocator; 5 | const bog = @import("bog.zig"); 6 | const Vm = bog.Vm; 7 | const Errors = bog.Errors; 8 | const linenoise = @import("linenoise"); 9 | const builtin = @import("builtin"); 10 | 11 | pub fn run(gpa: Allocator, in: File, out: File) !void { 12 | var repl: Repl = undefined; 13 | try repl.init(gpa); 14 | defer repl.deinit(); 15 | 16 | const writer = out.writer(); 17 | 18 | repl.vm.gc.stack_protect_start = @frameAddress(); 19 | 20 | const frame_val = try repl.vm.gc.alloc(.frame); 21 | frame_val.* = .{ .frame = &repl.frame }; 22 | defer frame_val.* = .{ .int = 0 }; // clear frame 23 | 24 | while (true) { 25 | defer { 26 | repl.arena.deinit(); 27 | repl.arena = std.heap.ArenaAllocator.init(gpa); 28 | } 29 | repl.handleLine(in, out) catch |err| switch (err) { 30 | error.EndOfStream => return, 31 | error.TokenizeError, error.ParseError, error.CompileError => try repl.vm.errors.render(writer), 32 | error.FatalError => { 33 | repl.frame.stack.items[0] = bog.Value.Null; 34 | try repl.vm.errors.render(writer); 35 | }, 36 | else => |e| return e, 37 | }; 38 | } 39 | } 40 | 41 | pub const Repl = struct { 42 | buffer: ArrayList(u8), 43 | ln: linenoise.Linenoise, 44 | tokenizer: bog.Tokenizer, 45 | parser: bog.Parser, 46 | tree: bog.Tree, 47 | compiler: bog.Compiler, 48 | bytecode: bog.Bytecode, 49 | code: bog.Compiler.Code, 50 | arena: std.heap.ArenaAllocator, 51 | vm: Vm, 52 | frame: Vm.Frame, 53 | 54 | const tokenize = @import("tokenizer.zig").tokenizeRepl; 55 | const parse = @import("parser.zig").parseRepl; 56 | const compile = @import("Compiler.zig").compileRepl; 57 | 58 | fn init(repl: *Repl, gpa: Allocator) !void { 59 | repl.buffer = try ArrayList(u8).initCapacity(gpa, std.heap.pageSize()); 60 | errdefer repl.buffer.deinit(); 61 | 62 | if (builtin.os.tag != .windows) { 63 | repl.ln = linenoise.Linenoise.init(gpa); 64 | } 65 | 66 | repl.tokenizer = .{ 67 | .errors = &repl.vm.errors, 68 | .path = "", 69 | .it = .{ 70 | .i = 0, 71 | .bytes = "", 72 | }, 73 | .repl = true, 74 | }; 75 | 76 | repl.parser = .{ 77 | .errors = &repl.vm.errors, 78 | .source = "", 79 | .path = "", 80 | .tok_ids = &.{}, 81 | .tok_starts = &.{}, 82 | .extra = std.ArrayList(bog.Node.Index).init(gpa), 83 | .node_buf = std.ArrayList(bog.Node.Index).init(gpa), 84 | .repl = true, 85 | }; 86 | 87 | repl.compiler = bog.Compiler{ 88 | .tree = &repl.tree, 89 | .errors = &repl.vm.errors, 90 | .gpa = gpa, 91 | .arena = repl.arena.allocator(), 92 | .code = &repl.code, 93 | .params = 1, // ans 94 | }; 95 | errdefer repl.compiler.deinit(); 96 | 97 | try repl.compiler.scopes.append(gpa, .{ 98 | .symbol = .{ 99 | .name = "ans", 100 | .ref = @enumFromInt(0), 101 | .mut = false, 102 | .val = undefined, 103 | }, 104 | }); 105 | try repl.compiler.globals.append(gpa, .{ 106 | .name = "ans", 107 | .ref = @enumFromInt(0), 108 | .mut = false, 109 | .val = undefined, 110 | }); 111 | repl.code = .{}; 112 | // repl.bytecode is initialized by compileRepl 113 | 114 | repl.arena = std.heap.ArenaAllocator.init(gpa); 115 | errdefer repl.arena.deinit(); 116 | 117 | repl.vm = Vm.init(gpa, .{ .repl = true, .import_files = true }); 118 | errdefer repl.vm.deinit(); 119 | try repl.vm.addStd(); 120 | 121 | repl.frame = .{ 122 | .this = bog.Value.Null, 123 | .mod = &repl.bytecode, 124 | .body = &.{}, 125 | .caller_frame = null, 126 | .module_frame = undefined, 127 | .captures = &.{}, 128 | .params = 1, 129 | }; 130 | errdefer repl.frame.deinit(&repl.vm); 131 | repl.frame.module_frame = &repl.frame; 132 | 133 | try repl.frame.stack.append(gpa, bog.Value.Null); 134 | } 135 | 136 | fn deinit(repl: *Repl) void { 137 | const gpa = repl.vm.gc.gpa; 138 | repl.buffer.deinit(); 139 | repl.ln.deinit(); 140 | repl.tokenizer.tokens.deinit(gpa); 141 | repl.parser.node_buf.deinit(); 142 | repl.parser.extra.deinit(); 143 | repl.parser.nodes.deinit(gpa); 144 | repl.compiler.deinit(); 145 | repl.code.deinit(gpa); 146 | repl.arena.deinit(); 147 | repl.frame.deinit(&repl.vm); 148 | repl.vm.deinit(); 149 | repl.* = undefined; 150 | } 151 | 152 | fn handleLine(repl: *Repl, in: File, out: File) !void { 153 | const buffer_start = repl.buffer.items.len; 154 | errdefer |e| if (e == error.CompileError) { 155 | repl.buffer.shrinkAndFree(buffer_start); 156 | }; 157 | 158 | try repl.readLine(in, out, ">>> "); 159 | const node = while (true) { 160 | if (try repl.tokenize()) { 161 | if (repl.parse()) |some| { 162 | break some orelse return; 163 | } else |err| switch (err) { 164 | error.NeedInput => {}, 165 | else => |e| return @as(@TypeOf(e)!void, e), 166 | } 167 | } 168 | 169 | try repl.readLine(in, out, "... "); 170 | } else unreachable; 171 | try repl.compile(node); 172 | 173 | const res = try repl.vm.run(&repl.frame); 174 | repl.frame.stack.items[0] = res; 175 | if (res == bog.Value.Null) return; 176 | 177 | const writer = out.writer(); 178 | try res.dump(writer, 2); 179 | try writer.writeByte('\n'); 180 | } 181 | 182 | fn readLine(repl: *Repl, in: File, out: File, prompt: []const u8) !void { 183 | _ = in; 184 | _ = out; 185 | if (repl.ln.linenoise(prompt)) |maybe_line| { 186 | const line = maybe_line orelse return error.EndOfStream; 187 | defer repl.ln.allocator.free(line); 188 | 189 | try repl.buffer.appendSlice(line); 190 | try repl.buffer.append('\n'); 191 | 192 | try repl.ln.history.add(line); 193 | } else |err| { 194 | switch (err) { 195 | error.CtrlC => return error.EndOfStream, 196 | else => |e| return e, 197 | } 198 | } 199 | } 200 | }; 201 | -------------------------------------------------------------------------------- /src/std.zig: -------------------------------------------------------------------------------- 1 | pub const io = @import("std/io.zig"); 2 | pub const fs = @import("std/fs.zig"); 3 | pub const math = @import("std/math.zig"); 4 | pub const os = @import("std/os.zig"); 5 | pub const map = @import("std/map.zig"); 6 | pub const debug = @import("std/debug.zig"); 7 | pub const json = @import("std/json.zig"); 8 | pub const gc = struct { 9 | pub fn collect(ctx: @import("bog.zig").Vm.Context) i64 { 10 | return @intCast(ctx.vm.gc.collect()); 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /src/std/debug.zig: -------------------------------------------------------------------------------- 1 | pub fn assert(val: bool) !void { 2 | if (!val) return error.AssertionFailed; 3 | } 4 | -------------------------------------------------------------------------------- /src/std/fs.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const bog = @import("../bog.zig"); 3 | const Value = bog.Value; 4 | const Vm = bog.Vm; 5 | 6 | pub fn open(ctx: Vm.Context, path: []const u8) !*Value { 7 | // TODO take options as parameters 8 | const res = try ctx.vm.gc.alloc(.native_val); 9 | res.* = .{ .native_val = .{ 10 | .vtable = Value.NativeVal.VTable.make(File), 11 | .type_id = Value.NativeVal.typeId(File), 12 | .ptr = try ctx.vm.gc.gpa.create(File), 13 | } }; 14 | const file = Value.NativeVal.unwrap(res.native_val.ptr, File); 15 | file.* = .{ 16 | .base = try std.fs.cwd().openFile(path, .{ .mode = .read_write }), 17 | .state = .open, 18 | }; 19 | return res; 20 | } 21 | 22 | const File = struct { 23 | base: std.fs.File, 24 | state: enum { open, closed }, 25 | 26 | pub fn typeName(_: *anyopaque) []const u8 { 27 | return "File"; 28 | } 29 | pub fn deinit(a: *anyopaque, gpa: std.mem.Allocator) void { 30 | gpa.destroy(Value.NativeVal.unwrap(a, File)); 31 | } 32 | 33 | pub fn get(_: *anyopaque, ctx: Vm.Context, index: *const Value, res: *?*Value) Value.NativeError!void { 34 | switch (index.*) { 35 | .str => |*s| { 36 | if (res.* == null) { 37 | res.* = try ctx.vm.gc.alloc(.int); 38 | } 39 | 40 | inline for (@typeInfo(methods).@"struct".decls) |method| { 41 | if (std.mem.eql(u8, s.data, method.name)) { 42 | res.* = try Value.zigFnToBog(ctx.vm, @field(methods, method.name)); 43 | return; 44 | } 45 | } else { 46 | return ctx.throw("no such property"); 47 | } 48 | }, 49 | else => return ctx.throw("invalid index type"), 50 | } 51 | } 52 | 53 | pub const methods = struct { 54 | pub fn close(file: Value.This(*File), ctx: Vm.Context) !void { 55 | if (file.t.state == .closed) return ctx.throw("closing an already closed file"); 56 | file.t.state = .closed; 57 | file.t.base.close(); 58 | } 59 | pub fn read(file: Value.This(*File), ctx: Vm.Context) ![]u8 { 60 | if (file.t.state == .closed) return ctx.throw("reading from a closed file"); 61 | return file.t.base.readToEndAlloc(ctx.vm.gc.gpa, std.math.maxInt(u32)); 62 | } 63 | pub fn write(file: Value.This(*File), ctx: Vm.Context, str: []const u8) !void { 64 | if (file.t.state == .closed) return ctx.throw("writing to a closed file"); 65 | try file.t.base.writeAll(str); 66 | } 67 | }; 68 | }; 69 | -------------------------------------------------------------------------------- /src/std/io.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const bog = @import("../bog.zig"); 3 | const Value = bog.Value; 4 | const Vm = bog.Vm; 5 | 6 | pub fn print(vals: Value.Variadic(*Value)) !void { 7 | var buf_writer = std.io.bufferedWriter(std.io.getStdOut().writer()); 8 | const writer = buf_writer.writer(); 9 | for (vals.t, 0..) |val, i| { 10 | if (i != 0) try writer.writeByte(' '); 11 | if (val.* == .str) { 12 | try writer.writeAll(val.str.data); 13 | } else { 14 | try val.dump(writer, 4); 15 | } 16 | } 17 | try writer.writeByte('\n'); 18 | try buf_writer.flush(); 19 | } 20 | 21 | pub fn input(ctx: Vm.Context, prompt: []const u8) ![]u8 { 22 | try std.io.getStdOut().writer().writeAll(prompt); 23 | 24 | return try std.io.getStdIn().reader().readUntilDelimiterAlloc(ctx.vm.gc.gpa, '\n', 1024 * 1024); 25 | } 26 | -------------------------------------------------------------------------------- /src/std/json.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const bog = @import("../bog.zig"); 3 | const Value = bog.Value; 4 | const Vm = bog.Vm; 5 | 6 | pub fn parse(ctx: Vm.Context, str: []const u8) !*Value { 7 | var scanner = std.json.Scanner.initCompleteInput(ctx.vm.gc.gpa, str); 8 | return parseInternal(ctx.vm, &scanner); 9 | } 10 | 11 | const Error = error{ UnexpectedToken, UnexpectedEndOfJson } || std.mem.Allocator.Error || std.json.Scanner.AllocIntoArrayListError || 12 | std.fmt.ParseIntError; 13 | fn parseInternal(vm: *Vm, scanner: *std.json.Scanner) Error!*Value { 14 | switch (try scanner.next()) { 15 | .object_begin => { 16 | const res = try vm.gc.alloc(.map); 17 | res.* = .{ .map = .{} }; 18 | 19 | while (true) { 20 | switch (try scanner.peekNextTokenType()) { 21 | .object_end => { 22 | _ = try scanner.next(); 23 | break; 24 | }, 25 | else => {}, 26 | } 27 | const key = try parseInternal(vm, scanner); 28 | const val = try parseInternal(vm, scanner); 29 | try res.map.put(vm.gc.gpa, key, val); 30 | } 31 | return res; 32 | }, 33 | .array_begin => { 34 | const res = try vm.gc.alloc(.list); 35 | res.* = .{ .list = .{} }; 36 | 37 | while (true) { 38 | switch (try scanner.peekNextTokenType()) { 39 | .array_end => { 40 | _ = try scanner.next(); 41 | break; 42 | }, 43 | else => {}, 44 | } 45 | try res.list.inner.append(vm.gc.gpa, try parseInternal(vm, scanner)); 46 | } 47 | return res; 48 | }, 49 | .object_end, .array_end => return error.UnexpectedToken, 50 | .string => { 51 | var b = Value.String.builder(vm.gc.gpa); 52 | errdefer b.cancel(); 53 | _ = try scanner.allocNextIntoArrayList(&b.inner, .alloc_always); 54 | const ret = try vm.gc.alloc(.str); 55 | ret.* = .{ .str = b.finish() }; 56 | return ret; 57 | }, 58 | .number => |bytes| { 59 | const val = try vm.gc.alloc(.int); 60 | val.* = .{ .int = std.fmt.parseInt(i64, bytes, 10) catch { 61 | val.* = .{ .num = try std.fmt.parseFloat(f64, bytes) }; 62 | return val; 63 | } }; 64 | return val; 65 | }, 66 | .true => return Value.True, 67 | .false => return Value.False, 68 | .null => return Value.Null, 69 | .end_of_document => return Value.Null, 70 | 71 | .partial_number, 72 | .allocated_number, 73 | .partial_string, 74 | .partial_string_escaped_1, 75 | .partial_string_escaped_2, 76 | .partial_string_escaped_3, 77 | .partial_string_escaped_4, 78 | .allocated_string, 79 | => unreachable, 80 | } 81 | } 82 | 83 | pub fn stringify(ctx: Vm.Context, val: *Value) !Value.String { 84 | var b = Value.String.builder(ctx.vm.gc.gpa); 85 | errdefer b.cancel(); 86 | try std.json.stringify(val, .{}, b.writer()); 87 | return b.finish(); 88 | } 89 | -------------------------------------------------------------------------------- /src/std/map.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const bog = @import("../bog.zig"); 3 | const Value = bog.Value; 4 | const Vm = bog.Vm; 5 | 6 | /// Creates a list of the maps keys 7 | pub fn keys(ctx: Vm.Context, map: *const Value.Map) !*Value { 8 | const gc = &ctx.vm.gc; 9 | var ret = try gc.alloc(.list); 10 | ret.* = .{ .list = .{} }; 11 | try ret.list.inner.resize(gc.gpa, map.count()); 12 | const items = ret.list.inner.items; 13 | 14 | var i: usize = 0; 15 | var iter = map.iterator(); 16 | while (iter.next()) |e| : (i += 1) { 17 | items[i] = try gc.dupe(e.key_ptr.*); 18 | } 19 | 20 | return ret; 21 | } 22 | 23 | /// Creates a list of the maps values 24 | pub fn values(ctx: Vm.Context, map: *const Value.Map) !*Value { 25 | const gc = &ctx.vm.gc; 26 | var ret = try gc.alloc(.list); 27 | ret.* = .{ .list = .{} }; 28 | try ret.list.inner.resize(gc.gpa, map.count()); 29 | const items = ret.list.inner.items; 30 | 31 | var i: usize = 0; 32 | var iter = map.iterator(); 33 | while (iter.next()) |e| : (i += 1) { 34 | items[i] = try gc.dupe(e.value_ptr.*); 35 | } 36 | 37 | return ret; 38 | } 39 | 40 | /// Creates a list of kv pairs 41 | pub fn entries(ctx: Vm.Context, map: *const Value.Map) !*Value { 42 | const gc = &ctx.vm.gc; 43 | var ret = try ctx.vm.gc.alloc(.list); 44 | ret.* = .{ .list = .{} }; 45 | try ret.list.inner.resize(gc.gpa, map.count()); 46 | const items = ret.list.inner.items; 47 | 48 | var i: usize = 0; 49 | var iter = map.iterator(); 50 | while (iter.next()) |e| : (i += 1) { 51 | var entry = try gc.alloc(.map); 52 | entry.* = .{ .map = .{} }; 53 | try entry.map.ensureTotalCapacity(gc.gpa, 2); 54 | 55 | const val_str = Value.string("value"); 56 | const key_str = Value.string("key"); 57 | entry.map.putAssumeCapacityNoClobber(try gc.dupe(&key_str), try gc.dupe(e.key_ptr.*)); 58 | entry.map.putAssumeCapacityNoClobber(try gc.dupe(&val_str), try gc.dupe(e.value_ptr.*)); 59 | 60 | items[i] = entry; 61 | } 62 | 63 | return ret; 64 | } 65 | 66 | /// Returns the amount of key value pairs in the map. 67 | pub fn size(map: *const Value.Map) !i64 { 68 | return @intCast(map.count()); 69 | } 70 | -------------------------------------------------------------------------------- /src/std/math.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const math = std.math; 3 | const bog = @import("../bog.zig"); 4 | const Value = bog.Value; 5 | const Vm = bog.Vm; 6 | 7 | /// Euler's number (e) 8 | pub const e = math.e; 9 | 10 | /// Archimedes' constant (π) 11 | pub const pi = math.pi; 12 | 13 | /// Circle constant (τ) 14 | pub const tau = math.tau; 15 | 16 | /// log2(e) 17 | pub const log2e = math.log2e; 18 | 19 | /// log10(e) 20 | pub const log10e = math.log10e; 21 | 22 | /// ln(2) 23 | pub const ln2 = math.ln2; 24 | 25 | /// ln(10) 26 | pub const ln10 = math.ln10; 27 | 28 | /// 2/sqrt(π) 29 | pub const two_sqrtpi = math.two_sqrtpi; 30 | 31 | /// sqrt(2) 32 | pub const sqrt2 = math.sqrt2; 33 | 34 | /// 1/sqrt(2) 35 | pub const sqrt1_2 = math.sqrt1_2; 36 | 37 | pub fn sqrt(ctx: Vm.Context, val: *Value) !*Value { 38 | return switch (val.*) { 39 | .int => |i| { 40 | const res = try ctx.vm.gc.alloc(.int); 41 | res.* = .{ .int = std.math.sqrt(@as(u64, @intCast(i))) }; 42 | return res; 43 | }, 44 | .num => |n| { 45 | const res = try ctx.vm.gc.alloc(.num); 46 | res.* = .{ .num = std.math.sqrt(n) }; 47 | return res; 48 | }, 49 | else => return ctx.throwFmt("sqrt expects a number, got '{s}'", .{val.typeName()}), 50 | }; 51 | } 52 | 53 | pub fn isNan(val: f64) bool { 54 | return math.isNan(val); 55 | } 56 | 57 | pub fn isSignalNan(val: f64) bool { 58 | return math.isSignalNan(val); 59 | } 60 | 61 | pub fn ceil(val: f64) f64 { 62 | return math.ceil(val); 63 | } 64 | 65 | pub fn floor(val: f64) f64 { 66 | return math.floor(val); 67 | } 68 | 69 | pub fn trunc(val: f64) f64 { 70 | return math.trunc(val); 71 | } 72 | 73 | pub fn round(val: f64) f64 { 74 | return math.round(val); 75 | } 76 | 77 | pub fn isFinite(val: f64) bool { 78 | return math.isFinite(val); 79 | } 80 | 81 | pub fn isInf(val: f64) bool { 82 | return math.isInf(val); 83 | } 84 | 85 | pub fn isPositiveInf(val: f64) bool { 86 | return math.isPositiveInf(val); 87 | } 88 | 89 | pub fn isNegativeInf(val: f64) bool { 90 | return math.isNegativeInf(val); 91 | } 92 | 93 | pub fn isNormal(val: f64) bool { 94 | return math.isNormal(val); 95 | } 96 | 97 | pub fn signbit(val: f64) bool { 98 | return math.signbit(val); 99 | } 100 | 101 | pub fn scalbn(val: f64, n: i32) f64 { 102 | return math.scalbn(val, n); 103 | } 104 | 105 | pub fn cbrt(val: f64) f64 { 106 | return math.cbrt(val); 107 | } 108 | 109 | pub fn acos(val: f64) f64 { 110 | return math.acos(val); 111 | } 112 | 113 | pub fn asin(val: f64) f64 { 114 | return math.asin(val); 115 | } 116 | 117 | pub fn atan(val: f64) f64 { 118 | return math.atan(val); 119 | } 120 | 121 | pub fn atan2(y: f64, x: f64) f64 { 122 | return math.atan2(y, x); 123 | } 124 | 125 | pub fn hypot(x: f64, y: f64) f64 { 126 | return math.hypot(x, y); 127 | } 128 | 129 | pub fn exp(val: f64) f64 { 130 | return math.exp(val); 131 | } 132 | 133 | pub fn exp2(val: f64) f64 { 134 | return math.exp2(val); 135 | } 136 | 137 | pub fn expm1(val: f64) f64 { 138 | return math.expm1(val); 139 | } 140 | 141 | pub fn ilogb(val: f64) i32 { 142 | return math.ilogb(val); 143 | } 144 | 145 | pub fn log(base: f64, val: f64) f64 { 146 | return math.log(f64, base, val); 147 | } 148 | 149 | pub fn log2(val: f64) f64 { 150 | return math.log2(val); 151 | } 152 | 153 | pub fn log10(val: f64) f64 { 154 | return math.log10(val); 155 | } 156 | 157 | pub fn log1p(val: f64) f64 { 158 | return math.log1p(val); 159 | } 160 | 161 | pub fn fma(x: f64, y: f64, z: f64) f64 { 162 | return @mulAdd(f64, x, y, z); 163 | } 164 | 165 | pub fn asinh(val: f64) f64 { 166 | return math.asinh(val); 167 | } 168 | 169 | pub fn acosh(val: f64) f64 { 170 | return math.acosh(val); 171 | } 172 | 173 | pub fn atanh(val: f64) f64 { 174 | return math.atanh(val); 175 | } 176 | 177 | pub fn sinh(val: f64) f64 { 178 | return math.sinh(val); 179 | } 180 | 181 | pub fn cosh(val: f64) f64 { 182 | return math.cosh(val); 183 | } 184 | 185 | pub fn tanh(val: f64) f64 { 186 | return math.tanh(val); 187 | } 188 | 189 | pub fn cos(val: f64) f64 { 190 | return math.cos(val); 191 | } 192 | 193 | pub fn sin(val: f64) f64 { 194 | return math.sin(val); 195 | } 196 | 197 | pub fn tan(val: f64) f64 { 198 | return math.tan(val); 199 | } 200 | 201 | pub fn clz(val: i64) i64 { 202 | return @clz(val); 203 | } 204 | 205 | pub fn ctz(val: i64) i64 { 206 | return @ctz(val); 207 | } 208 | 209 | pub fn popcount(val: i64) i64 { 210 | return @popCount(val); 211 | } 212 | -------------------------------------------------------------------------------- /src/std/os.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const bog = @import("../bog.zig"); 3 | const Value = bog.Value; 4 | const Vm = bog.Vm; 5 | 6 | pub const name = @import("builtin").os.tag; 7 | -------------------------------------------------------------------------------- /tests/behavior.zig: -------------------------------------------------------------------------------- 1 | test "suspend/resume" { 2 | try expectOutput( 3 | \\let mut count = 0 4 | \\let foo = fn() 5 | \\ count += 1 6 | \\ suspend 7 | \\ count += 1 8 | \\ 9 | \\let foo_frame = async foo() 10 | \\if count != 1 throw "bad" 11 | \\resume foo_frame 12 | \\if count != 2 throw "bad" 13 | , "null"); 14 | } 15 | 16 | test "simple values are duped before being added to an aggregate value" { 17 | try expectOutput( 18 | \\let b = [] 19 | \\for 0:2 20 | \\ let mut a = 1 21 | \\ b.append(a) 22 | \\ a += 1 23 | \\return b 24 | , "[1, 1]"); 25 | } 26 | 27 | test "cur_fn == null doesn't imply that identifier should be made global" { 28 | try expectOutput( 29 | \\let x = for let v in "oo" v 30 | \\let b = fn() 31 | \\ let v = 1 32 | \\ return v 33 | \\return b() 34 | , "1"); 35 | } 36 | 37 | test "spread" { 38 | try expectOutput( 39 | \\let foo = [1, 2, 3] 40 | \\let bar = [...foo, 4, 5, 6] 41 | \\return bar 42 | , "[1, 2, 3, 4, 5, 6]"); 43 | 44 | try expectOutput( 45 | \\let strs = ["foo", "bar"] 46 | \\let join1 = "".join(...strs) 47 | \\let join2 = "".join("foo", "bar") 48 | \\return join1 == join2 49 | , "true"); 50 | 51 | try expectOutput( 52 | \\let foo = [1, 2, 3] 53 | \\let bar = fn(a, b, c...) [a, b, ...c] 54 | \\if bar(...foo) != [1, 2, 3] throw "bad" 55 | \\if bar(4, ...foo) != [4, 1, 2, 3] throw "bad" 56 | \\if bar(4, 5, ...foo) != [4, 5, 1, 2, 3] throw "bad" 57 | \\if bar(4, 5, 6, ...foo) != [4, 5, 6, 1, 2, 3] throw "bad" 58 | \\if bar(4, 5, 6, 7, ...foo) != [4, 5, 6, 7, 1, 2, 3] throw "bad" 59 | , "null"); 60 | 61 | try expectOutput( 62 | \\let foo = [1, 2, 3] 63 | \\let str = "{} + {} = {}" 64 | \\return str.format(...foo) 65 | , "\"1 + 2 = 3\""); 66 | 67 | try expectOutput( 68 | \\let a = [1, 2, 3, 4, 5] 69 | \\let [b, c, ...rest] = a 70 | \\return rest 71 | , "[3, 4, 5]"); 72 | } 73 | 74 | test "variadic functions" { 75 | try expectOutput( 76 | \\let foo = fn(_, args...) args 77 | \\return foo(1, 2, 3, 4, 5) 78 | , "[2, 3, 4, 5]"); 79 | } 80 | 81 | test "capture" { 82 | try expectOutput( 83 | \\let foo = fn() 84 | \\ let a = 1 85 | \\ return fn() 86 | \\ return a + 1 87 | \\ 88 | \\return foo()() 89 | , "2"); 90 | } 91 | 92 | test "nested for loop" { 93 | try expectOutput( 94 | \\let mut i = 0 95 | \\for 1:5 96 | \\ for 1:5 97 | \\ i += 1 98 | \\return i 99 | , "16"); 100 | } 101 | 102 | test "range destructuring" { 103 | try expectOutput( 104 | \\let (start:end:step) = 1:2:3 105 | \\return start+end+step 106 | , "6"); 107 | } 108 | 109 | test "continue" { 110 | try expectOutput( 111 | \\let mut i = 0 112 | \\while i < 1 113 | \\ i += 1 114 | \\ continue 115 | , "null"); 116 | try expectOutput( 117 | \\for let i in 0:1 118 | \\ continue 119 | , "null"); 120 | } 121 | 122 | test "std.gc" { 123 | if (@import("builtin").os.tag == .windows) { 124 | // TODO this gives a different result on windows 125 | return error.SkipZigTest; 126 | } 127 | try expectOutput( 128 | \\let {collect} = import "std.gc" 129 | \\let json = import "std.json" 130 | \\ 131 | \\let makeGarbage = fn() 132 | \\ json.stringify({"a" = [2, "foo", null]}) 133 | \\ 134 | \\for 0:5 makeGarbage() 135 | \\return collect() 136 | , 137 | \\53 138 | ); 139 | } 140 | 141 | test "std.json" { 142 | try expectOutput( 143 | \\let json = import "std.json" 144 | \\return json.parse("{\"a\":[2,\"foo\",null]}") 145 | , 146 | \\{"a" = [2, "foo", null]} 147 | ); 148 | try expectOutput( 149 | \\let json = import "std.json" 150 | \\return json.stringify({"a" = [2, "foo", null]}) 151 | , 152 | \\"{\"a\":[2,\"foo\",null]}" 153 | ); 154 | } 155 | 156 | test "boolean short-circuit" { 157 | try expectOutput( 158 | \\let foo = fn() true 159 | \\let bar = fn() error("should not be evaluated") 160 | \\return foo() or bar() 161 | , 162 | \\true 163 | ); 164 | try expectOutput( 165 | \\let foo = fn() false 166 | \\let bar = fn() error("should not be evaluated") 167 | \\return foo() and bar() 168 | , 169 | \\false 170 | ); 171 | } 172 | 173 | test "match" { 174 | try expectOutput( 175 | \\let getNum = fn (arg) 176 | \\ return match (arg) 177 | \\ 1, 2 => 69 178 | \\ 12 => 42 179 | \\ 10004 => 17 180 | \\ _ => 0 181 | \\ 182 | \\let arr = [] 183 | \\arr.append(getNum(1)) 184 | \\arr.append(getNum(2)) 185 | \\arr.append(getNum(12)) 186 | \\arr.append(getNum(10004)) 187 | \\arr.append(getNum(9)) 188 | \\return arr 189 | , 190 | \\[69, 69, 42, 17, 0] 191 | ); 192 | try expectOutput( 193 | \\let getNum = fn() 42 194 | \\ 195 | \\match getNum() 196 | \\ 1 => error 197 | \\ let val => return val 198 | , 199 | \\42 200 | ); 201 | } 202 | 203 | test "try catch" { 204 | try expectOutput( 205 | \\let fails_on_1 = fn(arg) if (arg == 1) error(69) 206 | \\let fails_on_2 = fn(arg) if (arg == 2) error(42) 207 | \\let fails_on_3 = fn(arg) if (arg == 3) error(17) 208 | \\ 209 | \\let foo = fn(arg) 210 | \\ try 211 | \\ fails_on_1(arg) 212 | \\ fails_on_2(arg) 213 | \\ fails_on_3(arg) 214 | \\ catch let err 215 | \\ return err 216 | \\ 217 | \\ return 99 218 | \\ 219 | \\return for let i in 0:4 foo(i) 220 | , 221 | \\[99, 69, 42, 17] 222 | ); 223 | } 224 | 225 | test "string join" { 226 | try expectOutput( 227 | \\return ",".join(1 as str, "bar", [2] as str) 228 | , 229 | \\"1,bar,[2]" 230 | ); 231 | } 232 | 233 | test "format string" { 234 | try expectOutput( 235 | \\return f"foo{255:X}bar" 236 | , 237 | \\"fooFFbar" 238 | ); 239 | try expectOutput( 240 | \\return "foo{X}bar".format(255) 241 | , 242 | \\"fooFFbar" 243 | ); 244 | } 245 | 246 | test "range" { 247 | try expectOutput( 248 | \\return for let i in 0:7:2 i 249 | , 250 | \\[0, 2, 4, 6] 251 | ); 252 | try expectOutput( 253 | \\return 1 in 0:2 254 | , 255 | \\true 256 | ); 257 | try expectOutput( 258 | \\return 1 in 0:2:2 259 | , 260 | \\false 261 | ); 262 | } 263 | 264 | test "list comprehension as function argument" { 265 | try expectOutput( 266 | \\let map = {1 = 2, 3 = 4, 5 = 6} 267 | \\return (fn(l)l)(for let (k, v) in map {key = k, val = v}) 268 | , 269 | \\[{"key" = 1, "val" = 2}, {"key" = 3, "val" = 4}, {"key" = 5, "val" = 6}] 270 | ); 271 | } 272 | 273 | test "map iterator" { 274 | try expectOutput( 275 | \\let map = {1 = 2, 3 = 4, 5 = 6} 276 | \\let mut sum = 0 277 | \\for let (k, v) in map 278 | \\ sum += k 279 | \\ sum *= v 280 | \\return sum 281 | , 282 | \\150 283 | ); 284 | } 285 | 286 | test "list comprehension" { 287 | try expectOutput( 288 | \\return for let c in "hello" c 289 | , 290 | \\["h", "e", "l", "l", "o"] 291 | ); 292 | try expectOutput( 293 | \\let mut i = 0 294 | \\return while (i < 10) 295 | \\ i += 1 296 | \\ i 297 | , 298 | \\[1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 299 | ); 300 | } 301 | 302 | test "list.append" { 303 | try expectOutput( 304 | \\let list = [1, 2] 305 | \\list.append(3) 306 | \\return list 307 | , 308 | \\[1, 2, 3] 309 | ); 310 | } 311 | 312 | test "std.map" { 313 | try expectOutput( 314 | \\let val = {foo = 2, bar = 3, 0 = 515, [1] = [2]} 315 | \\let map = import "std.map" 316 | \\let {assert} = import "std.debug" 317 | \\let keys = map.keys(val) 318 | \\assert(keys is list and keys.len == 4) 319 | \\let values = map.values(val) 320 | \\assert(values is list and values.len == 4) 321 | \\let entries = map.entries(val) 322 | \\assert(entries is list and entries.len == 4) 323 | \\let entry = entries[0] 324 | \\assert(entry is map and map.size(entry) == 2) 325 | , 326 | \\null 327 | ); 328 | } 329 | 330 | test "modifying collections" { 331 | try expectOutput( 332 | \\let x = [0] 333 | \\let y = [x] 334 | \\x[0] = 1 335 | \\return y 336 | , 337 | \\[[1]] 338 | ); 339 | try expectOutput( 340 | \\let foo = [1] 341 | \\let bar = fn(list) list[0] = 2 342 | \\bar(foo) 343 | \\return foo 344 | , 345 | \\[2] 346 | ); 347 | } 348 | 349 | test "tagged values" { 350 | try expectOutput( 351 | \\return @something{foo = 69} 352 | , 353 | \\@something{"foo" = 69} 354 | ); 355 | try expectOutput( 356 | \\let foo = @foo 357 | \\let bar = @bar 358 | \\return foo == bar 359 | , 360 | \\false 361 | ); 362 | try expectOutput( 363 | \\let foo = @foo[1] 364 | \\let bar = @foo[1] 365 | \\return foo == bar 366 | , 367 | \\true 368 | ); 369 | try expectOutput( 370 | \\let foo = @foo[1, 2] 371 | \\let @foo[bar, baz] = foo 372 | \\return bar + baz 373 | , 374 | \\3 375 | ); 376 | } 377 | 378 | test "error map" { 379 | try expectOutput( 380 | \\let foo = error{a = 2} 381 | \\let error{bar = a} = foo 382 | \\return bar * 2 383 | , 384 | \\4 385 | ); 386 | } 387 | 388 | test "call bog function" { 389 | try expectCallOutput( 390 | \\return { 391 | \\ foo = 2, 392 | \\ doTheTest = fn(num) this.foo + num 393 | \\} 394 | , .{1}, 395 | \\3 396 | ); 397 | } 398 | 399 | test "containers do not overwrite memoized values" { 400 | try expectOutput( 401 | \\let x = [true] 402 | \\x[0] = 1 403 | \\return true 404 | , 405 | \\true 406 | ); 407 | } 408 | 409 | test "this" { 410 | try expectOutput( 411 | \\let x = { 412 | \\ a = 69, 413 | \\ y = 420, 414 | \\ foo = fn() ( 415 | \\ [0][0] 416 | \\ return this.a * this.y 417 | \\ ), 418 | \\ 419 | \\} 420 | \\return x.foo() 421 | , 422 | \\28980 423 | ); 424 | } 425 | 426 | test "closures" { 427 | try expectOutput( 428 | \\let x = 2 429 | \\let foo = fn() x + 5 430 | \\return foo() 431 | , 432 | \\7 433 | ); 434 | try expectOutput( 435 | \\let x = 2 436 | \\let foo = fn() 437 | \\ return fn() 438 | \\ return x + 5 439 | \\ 440 | \\let bar = foo() 441 | \\return bar() 442 | , 443 | \\7 444 | ); 445 | } 446 | 447 | test "map" { 448 | try expectOutput( 449 | \\let y = 2 450 | \\let map = {1 = 2, y} 451 | \\map["foo"] = "bar" 452 | \\return map 453 | , 454 | \\{1 = 2, "y" = 2, "foo" = "bar"} 455 | ); 456 | try expectOutput( 457 | \\let y = 2 458 | \\let map = {1 = 2, x = y} 459 | \\let {x} = map 460 | \\let {foo = x} = map 461 | \\return x == foo 462 | , 463 | \\true 464 | ); 465 | } 466 | 467 | test "property access of list" { 468 | try expectOutput( 469 | \\let list = [1, true, "hello"] 470 | \\return list.len 471 | , 472 | \\3 473 | ); 474 | try expectOutput( 475 | \\let y = [1,2,3] 476 | \\y[-1] = 4 477 | \\y["len"] 478 | \\return y 479 | , 480 | \\[1, 2, 4] 481 | ); 482 | } 483 | 484 | test "string for iter" { 485 | try expectOutput( 486 | \\let mut sum = 0 487 | \\for let c in "hellö wörld" 488 | \\ if (c == "h") sum += 1 489 | \\ else if (c == "e") sum += 2 490 | \\ else if (c == "l") sum += 3 491 | \\ else if (c == "ö") sum += 4 492 | \\ else if (c == "w") sum += 5 493 | \\ else if (c == "d") sum += 6 494 | \\ 495 | \\return sum 496 | , 497 | \\31 498 | ); 499 | } 500 | 501 | test "for loops" { 502 | try expectOutput( 503 | \\let mut sum = 0 504 | \\for let x in [1, 2, 3] 505 | \\ sum += x 506 | \\ 507 | \\return sum 508 | , 509 | \\6 510 | ); 511 | try expectOutput( 512 | \\let mut sum = 0 513 | \\for let (x,y) in [(1, 2), (2, 3), (5, 6)] 514 | \\ sum += x * y 515 | \\ 516 | \\return sum 517 | , 518 | \\38 519 | ); 520 | } 521 | 522 | test "error destructure" { 523 | try expectOutput( 524 | \\let err = fn() error(2) 525 | \\ 526 | \\let error(y) = err() 527 | \\return y + 2 528 | , 529 | \\4 530 | ); 531 | } 532 | 533 | test "while let" { 534 | try expectOutput( 535 | \\let getSome = fn(val) if val != 0 val - 1 536 | \\ 537 | \\let mut val = 10 538 | \\while let newVal = getSome(val) 539 | \\ val = newVal 540 | \\return val 541 | , 542 | \\0 543 | ); 544 | } 545 | 546 | test "if let" { 547 | try expectOutput( 548 | \\let maybeInc = fn(val) 549 | \\ if let y = val 550 | \\ return y + 4 551 | \\ return 2 552 | \\ 553 | \\return maybeInc(null) + maybeInc(4) 554 | , 555 | \\10 556 | ); 557 | } 558 | 559 | test "fibonacci" { 560 | try expectOutput( 561 | \\let fib = fn(n) 562 | \\ if n < 2 return n 563 | \\ return fib(n - 1) + fib(n-2) 564 | \\ 565 | \\return fib(10) 566 | , 567 | \\55 568 | ); 569 | } 570 | 571 | test "const value not modified by function" { 572 | try expectOutput( 573 | \\let x = 2 574 | \\let inc = fn(mut n) 575 | \\ n += 1 576 | \\inc(x) 577 | \\return x 578 | , 579 | \\2 580 | ); 581 | try expectOutput( 582 | \\let x = [2] 583 | \\let inc = fn(l) l.append(1) 584 | \\inc(x) 585 | \\return x 586 | , 587 | \\[2, 1] 588 | ); 589 | } 590 | 591 | test "in" { 592 | try expectOutput( 593 | \\let y = [1, 2, 3] 594 | \\if not true in y 595 | \\ y[-2] = false 596 | \\return y == [1, false, 3] 597 | , 598 | \\true 599 | ); 600 | } 601 | 602 | test "get/set" { 603 | try expectOutput( 604 | \\let y = [1, 2, 3] 605 | \\y[-2] = true 606 | \\return y[1] 607 | , 608 | \\true 609 | ); 610 | } 611 | 612 | test "mixed num and int" { 613 | try expectOutput( 614 | \\let mut y = 2 615 | \\y /= 5 616 | \\return y 617 | , 618 | \\0.4 619 | ); 620 | try expectOutput( 621 | \\let mut y = 2 622 | \\y **= 0.5 623 | \\return y 624 | , 625 | \\1.4142135623730951 626 | ); 627 | } 628 | 629 | test "copy on assign" { 630 | try expectOutput( 631 | \\let x = 2 632 | \\let mut y = x 633 | \\y += 2 634 | \\return x 635 | , 636 | \\2 637 | ); 638 | try expectOutput( 639 | \\let mut y = 2 640 | \\let inc = fn (mut a) 641 | \\ a += 2 642 | \\inc(y) 643 | \\return y 644 | , 645 | \\2 646 | ); 647 | } 648 | 649 | test "strings" { 650 | try expectOutput( 651 | \\let a = "hello" 652 | \\return if a == "world" 2 as str else 1.5 as str 653 | , 654 | \\"1.5" 655 | ); 656 | } 657 | 658 | test "comparison" { 659 | try expectOutput( 660 | \\let mut a = 0 661 | \\while a != 1000 662 | \\ a += 1 663 | \\return a 664 | , 665 | \\1000 666 | ); 667 | } 668 | 669 | test "while loop" { 670 | try expectOutput( 671 | \\let cond = true 672 | \\while cond 673 | \\ if not cond 674 | \\ break 675 | \\ else 676 | \\ let x = 2 677 | \\ break 678 | \\return true 679 | , 680 | \\true 681 | ); 682 | } 683 | 684 | test "subscript" { 685 | try expectOutput( 686 | \\let y = (1, 2) 687 | \\return y[-1] 688 | , 689 | \\2 690 | ); 691 | } 692 | 693 | test "assert" { 694 | try expectOutput( 695 | \\let assert = fn (ok) if not ok error(false) 696 | \\return assert(not false) 697 | , 698 | \\null 699 | ); 700 | } 701 | 702 | test "functions" { 703 | try expectOutput( 704 | \\let add = fn ((a,b)) a + b 705 | \\let tuplify = fn (a,b) (a,b) 706 | \\return add(tuplify(1, 2)) 707 | , 708 | \\3 709 | ); 710 | try expectOutput( 711 | \\let add = fn (a,b) a + b 712 | \\let sub = fn (a,b) a - b 713 | \\return sub(add(3, 4), add(1,2)) 714 | , 715 | \\4 716 | ); 717 | } 718 | 719 | test "type casting" { 720 | try expectOutput( 721 | \\return 1 as null 722 | , 723 | \\null 724 | ); 725 | try expectOutput( 726 | \\return 1 as bool 727 | , 728 | \\true 729 | ); 730 | try expectOutput( 731 | \\let y = 2.5 732 | \\return y as int 733 | , 734 | \\2 735 | ); 736 | try expectOutput( 737 | \\let x = 2.5 as int 738 | \\let y = x as num 739 | \\return y 740 | , 741 | \\2 742 | ); 743 | } 744 | 745 | test "type checking" { 746 | try expectOutput( 747 | \\return 1 is int 748 | , 749 | \\true 750 | ); 751 | try expectOutput( 752 | \\return 1 is num 753 | , 754 | \\false 755 | ); 756 | try expectOutput( 757 | \\return (1,) is tuple 758 | , 759 | \\true 760 | ); 761 | } 762 | 763 | test "tuple destructuring" { 764 | try expectOutput( 765 | \\let (a, b, _, c, _) = (1, 2, 3, 4, 5) 766 | \\return (a + b) * c 767 | , 768 | \\12 769 | ); 770 | } 771 | 772 | test "tuple" { 773 | try expectOutput( 774 | \\return (1, 2, 3, 4, 22.400) 775 | , 776 | \\(1, 2, 3, 4, 22.4) 777 | ); 778 | } 779 | 780 | test "bool if" { 781 | try expectOutput( 782 | \\let x = not false 783 | \\return 3 + if not x 2 else if x 4 else 9 784 | , 785 | \\7 786 | ); 787 | } 788 | 789 | test "assignment" { 790 | try expectOutput( 791 | \\let mut x = 2 792 | \\let y = -3 793 | \\x **= -y 794 | \\return x 795 | , 796 | \\8 797 | ); 798 | } 799 | 800 | test "basic math" { 801 | try expectOutput( 802 | \\let x = 2 803 | \\let y = x * 5 804 | \\let z = 90 805 | \\return y // x * z 806 | , 807 | \\450 808 | ); 809 | } 810 | 811 | test "basic variables" { 812 | try expectOutput( 813 | \\let x = 12 814 | \\return x 815 | , 816 | \\12 817 | ); 818 | try expectOutput( 819 | \\let x = true 820 | \\return not x 821 | , 822 | \\false 823 | ); 824 | } 825 | 826 | test "number literals" { 827 | try expectOutput( 828 | \\return 12 829 | , 830 | \\12 831 | ); 832 | try expectOutput( 833 | \\return 0x12 834 | , 835 | \\18 836 | ); 837 | try expectOutput( 838 | \\return 0o12 839 | , 840 | \\10 841 | ); 842 | } 843 | 844 | test "constant values" { 845 | try expectOutput( 846 | \\return true 847 | , 848 | \\true 849 | ); 850 | try expectOutput( 851 | \\return not true 852 | , 853 | \\false 854 | ); 855 | try expectOutput( 856 | \\return -12 857 | , 858 | \\-12 859 | ); 860 | } 861 | 862 | const std = @import("std"); 863 | const mem = std.mem; 864 | const warn = std.debug.warn; 865 | const testing = std.testing; 866 | const bog = @import("bog"); 867 | const Vm = bog.Vm; 868 | 869 | fn expectCallOutput(source: []const u8, args: anytype, expected: []const u8) !void { 870 | const alloc = std.testing.allocator; 871 | var vm = Vm.init(alloc, .{}); 872 | var mod = bog.compile(alloc, source, "", &vm.errors) catch |e| switch (e) { 873 | else => return e, 874 | error.TokenizeError, error.ParseError, error.CompileError => { 875 | vm.errors.render(std.io.getStdErr().writer()) catch {}; 876 | return error.TestFailed; 877 | }, 878 | }; 879 | mod.debug_info.source = ""; 880 | defer mod.deinit(alloc); 881 | defer vm.deinit(); 882 | vm.addStd() catch unreachable; 883 | 884 | var frame = bog.Vm.Frame{ 885 | .this = bog.Value.Null, 886 | .mod = &mod, 887 | .body = mod.main, 888 | .caller_frame = null, 889 | .module_frame = undefined, 890 | .captures = &.{}, 891 | .params = 0, 892 | }; 893 | defer frame.deinit(&vm); 894 | frame.module_frame = &frame; 895 | 896 | vm.gc.stack_protect_start = @frameAddress(); 897 | 898 | const frame_val = try vm.gc.alloc(.frame); 899 | frame_val.* = .{ .frame = &frame }; 900 | defer frame_val.* = .{ .int = 0 }; // clear frame 901 | 902 | const res = vm.run(&frame) catch |e| switch (e) { 903 | else => return e, 904 | error.FatalError => { 905 | vm.errors.render(std.io.getStdErr().writer()) catch {}; 906 | return error.TestFailed; 907 | }, 908 | }; 909 | 910 | if (true) return error.SkipZigTest; 911 | 912 | const call_res = vm.call(res, "doTheTest", args) catch |e| switch (e) { 913 | else => return e, 914 | error.FatalError => { 915 | vm.errors.render(std.io.getStdErr().writer()) catch {}; 916 | return error.TestFailed; 917 | }, 918 | }; 919 | var out_buf = std.ArrayList(u8).init(std.testing.allocator); 920 | defer out_buf.deinit(); 921 | try call_res.dump(out_buf.writer(), 2); 922 | try testing.expectEqualStrings(expected, out_buf.items); 923 | } 924 | 925 | fn expectOutput(source: []const u8, expected: []const u8) !void { 926 | const alloc = std.testing.allocator; 927 | var vm = Vm.init(alloc, .{}); 928 | var mod = bog.compile(alloc, source, "", &vm.errors) catch |e| switch (e) { 929 | else => return e, 930 | error.TokenizeError, error.ParseError, error.CompileError => { 931 | vm.errors.render(std.io.getStdErr().writer()) catch {}; 932 | return error.TestFailed; 933 | }, 934 | }; 935 | mod.debug_info.source = ""; 936 | defer mod.deinit(alloc); 937 | defer vm.deinit(); 938 | vm.addStd() catch unreachable; 939 | 940 | var frame = bog.Vm.Frame{ 941 | .this = bog.Value.Null, 942 | .mod = &mod, 943 | .body = mod.main, 944 | .caller_frame = null, 945 | .module_frame = undefined, 946 | .captures = &.{}, 947 | .params = 0, 948 | }; 949 | defer frame.deinit(&vm); 950 | frame.module_frame = &frame; 951 | 952 | vm.gc.stack_protect_start = @frameAddress(); 953 | 954 | const frame_val = try vm.gc.alloc(.frame); 955 | frame_val.* = .{ .frame = &frame }; 956 | defer frame_val.* = .{ .int = 0 }; // clear frame 957 | 958 | const res = vm.run(&frame) catch |e| switch (e) { 959 | else => return e, 960 | error.FatalError => { 961 | vm.errors.render(std.io.getStdErr().writer()) catch {}; 962 | return error.TestFailed; 963 | }, 964 | }; 965 | 966 | var out_buf = std.ArrayList(u8).init(alloc); 967 | defer out_buf.deinit(); 968 | try res.dump(out_buf.writer(), 2); 969 | try testing.expectEqualStrings(expected, out_buf.items); 970 | } 971 | -------------------------------------------------------------------------------- /tests/error.zig: -------------------------------------------------------------------------------- 1 | test "redeclaration" { 2 | try expectError( 3 | \\let i = 0 4 | \\let i = 1 5 | , 6 | \\redeclaration of 'i' 7 | ); 8 | } 9 | 10 | test "unexpected token" { 11 | try expectError( 12 | \\if (a b 13 | , 14 | \\expected ',', found 'Identifier' 15 | ); 16 | } 17 | 18 | test "unexpected arg count" { 19 | try expectError( 20 | \\let foo = fn (a, b) a + b 21 | \\foo(1) 22 | , 23 | \\expected 2 args, got 1 24 | ); 25 | } 26 | 27 | test "extra cases after catch-all" { 28 | try expectError( 29 | \\match (1) 30 | \\ let val => null 31 | \\ 1 => null 32 | \\ 33 | , 34 | \\additional cases after a catch-all case 35 | ); 36 | } 37 | 38 | test "extra handlers after catch-all" { 39 | try expectError( 40 | \\let foo = fn() null 41 | \\try 42 | \\ foo() 43 | \\catch 44 | \\ 2 45 | \\catch 1 46 | \\ 3 47 | \\ 48 | , 49 | \\additional handlers after a catch-all handler 50 | ); 51 | } 52 | 53 | test "invalid tag unwrap" { 54 | try expectError( 55 | \\let foo = @bar[2] 56 | \\let @foo[baz] = foo 57 | , 58 | \\invalid tag 59 | ); 60 | } 61 | 62 | test "missing capture" { 63 | try expectError( 64 | \\let error = [2] 65 | , 66 | \\expected a destructuring 67 | ); 68 | try expectError( 69 | \\let @foo = [2] 70 | , 71 | \\expected a destructuring 72 | ); 73 | } 74 | 75 | test "break outside loop" { 76 | try expectError( 77 | \\break 78 | , 79 | \\break outside of loop 80 | ); 81 | } 82 | 83 | test "use of undeclared identifier" { 84 | try expectError( 85 | \\foo 86 | , 87 | \\use of undeclared identifier 88 | ); 89 | } 90 | 91 | test "invalid map" { 92 | try expectError( 93 | \\let y = {1} 94 | , 95 | \\expected a key 96 | ); 97 | } 98 | 99 | test "index out of bounds" { 100 | try expectError( 101 | \\let y = [0, 0, 0] 102 | \\y[y["len"]] = true 103 | , 104 | \\index out of bounds 105 | ); 106 | } 107 | 108 | test "invalid type" { 109 | try expectError( 110 | \\1 + true 111 | , 112 | \\expected a number 113 | ); 114 | } 115 | 116 | const std = @import("std"); 117 | const mem = std.mem; 118 | const warn = std.debug.warn; 119 | const testing = std.testing; 120 | const bog = @import("bog"); 121 | const Vm = bog.Vm; 122 | const Errors = bog.Errors; 123 | 124 | fn expectError(source: []const u8, expected: []const u8) !void { 125 | const alloc = std.testing.allocator; 126 | var vm = Vm.init(alloc, .{}); 127 | var mod = bog.compile(alloc, source, "", &vm.errors) catch |e| switch (e) { 128 | else => return error.UnexpectedError, 129 | error.TokenizeError, error.ParseError, error.CompileError => { 130 | const result = vm.errors.list.items[0].msg; 131 | try std.testing.expectEqualStrings(expected, result.data); 132 | vm.errors.deinit(); 133 | return; 134 | }, 135 | }; 136 | defer { 137 | mod.debug_info.source = ""; 138 | mod.deinit(alloc); 139 | } 140 | defer vm.deinit(); 141 | 142 | var frame = bog.Vm.Frame{ 143 | .this = bog.Value.Null, 144 | .mod = &mod, 145 | .body = mod.main, 146 | .caller_frame = null, 147 | .module_frame = undefined, 148 | .captures = &.{}, 149 | .params = 0, 150 | }; 151 | defer frame.deinit(&vm); 152 | frame.module_frame = &frame; 153 | 154 | vm.gc.stack_protect_start = @frameAddress(); 155 | 156 | const frame_val = try vm.gc.alloc(.frame); 157 | frame_val.* = .{ .frame = &frame }; 158 | defer frame_val.* = .{ .int = 0 }; // clear frame 159 | 160 | _ = vm.run(&frame) catch |e| switch (e) { 161 | else => return error.UnexpectedError, 162 | error.FatalError => { 163 | const result = vm.errors.list.items[0].msg; 164 | try std.testing.expectEqualStrings(expected, result.data); 165 | return; 166 | }, 167 | }; 168 | 169 | return error.ExpectedError; 170 | } 171 | -------------------------------------------------------------------------------- /tests/fmt.zig: -------------------------------------------------------------------------------- 1 | test "variadic functions" { 2 | try testCanonical( 3 | \\let foo = fn() null 4 | \\let bar = fn(a...) a 5 | \\let baz = fn(a, b...) a 6 | \\baz(1, ..."foo") 7 | \\ 8 | ); 9 | } 10 | 11 | test "format string" { 12 | try testTransform( 13 | \\f"foo {{1:2}:32} bar { 2 14 | \\* 15 | \\3:4} baz \t" 16 | \\ 17 | \\ 18 | , 19 | \\f"foo {{1:2}:32} bar {2 * 20 | \\ 3:4} baz \t" 21 | \\ 22 | ); 23 | } 24 | 25 | test "try-catch" { 26 | try testCanonical( 27 | \\try 28 | \\ assert(x == y) 29 | \\ assert(x != z) 30 | \\catch "assertion failure" 31 | \\ print("assertion failure") 32 | \\catch 1 33 | \\ print("got one") 34 | \\catch let err 35 | \\ print(err) 36 | \\ 37 | \\ 38 | \\ 39 | ); 40 | try testCanonical( 41 | \\try assert(x == y) catch print("assertion failure") 42 | \\ 43 | ); 44 | try testCanonical( 45 | \\try 46 | \\ assert(x == y) 47 | \\catch 48 | \\ print("them all") 49 | \\ 50 | \\ 51 | \\ 52 | ); 53 | } 54 | 55 | test "ranges" { 56 | try testCanonical( 57 | \\1:2:3 58 | \\1::3 59 | \\1:2 60 | \\1: 61 | \\ 62 | ); 63 | } 64 | 65 | test "ignore comments in indent blocks" { 66 | if (true) return error.SkipZigTest; 67 | try testTransform( 68 | \\const foo = fn() 69 | \\ #quux 70 | \\#foo bar 71 | \\ #quux 72 | \\#foo bar 73 | \\ #quux 74 | \\ return 2 75 | , // TODO improve comment rendering 76 | \\const foo = fn() 77 | \\#quux 78 | \\#foo bar 79 | \\#quux 80 | \\#foo bar 81 | \\#quux 82 | \\ return 2 83 | \\ 84 | \\ 85 | \\ 86 | ); 87 | } 88 | 89 | test "tag" { 90 | try testCanonical( 91 | \\@foo 92 | \\@bar(foo) 93 | \\@baz(2.4, "foo") 94 | \\@qux[1, 2] 95 | \\@quux{foo = bar} 96 | \\ 97 | ); 98 | } 99 | 100 | test "different error initializations" { 101 | try testCanonical( 102 | \\error 103 | \\error(foo) 104 | \\error(2.4, "foo") 105 | \\error[1, 2] 106 | \\error{foo = bar} 107 | \\ 108 | ); 109 | } 110 | 111 | test "nested blocks and matches" { 112 | try testCanonical( 113 | \\if false 114 | \\ if true 115 | \\ match 2 116 | \\ true => a 117 | \\ false => b 118 | \\ 119 | \\ 120 | \\ else 121 | \\ 2 122 | \\ 123 | \\ 124 | \\ 125 | ); 126 | } 127 | 128 | test "comments after expression" { 129 | if (true) return error.SkipZigTest; 130 | try testCanonical( 131 | \\a 132 | \\#foo 133 | \\#bar 134 | \\ 135 | ); 136 | } 137 | 138 | test "two empty lines after block" { 139 | try testTransform( 140 | \\let foo = fn(a) 141 | \\ a * 4 142 | \\let bar = 2 143 | , 144 | \\let foo = fn(a) 145 | \\ a * 4 146 | \\ 147 | \\ 148 | \\let bar = 2 149 | \\ 150 | ); 151 | } 152 | 153 | test "respect new lines" { 154 | if (true) return error.SkipZigTest; 155 | try testCanonical( 156 | \\const foo = 1 157 | \\ 158 | \\const bar = 2 159 | \\ 160 | ); 161 | try testTransform( 162 | \\const foo = 1 163 | \\ 164 | \\ 165 | \\const bar = 2 166 | , 167 | \\const foo = 1 168 | \\ 169 | \\const bar = 2 170 | \\ 171 | ); 172 | } 173 | 174 | test "nested blocks" { 175 | try testCanonical( 176 | \\if false 177 | \\ if false 178 | \\ 3 179 | \\ else if true 180 | \\ 4 181 | \\ else 182 | \\ 5 183 | \\ 184 | \\ 185 | \\ 186 | ); 187 | } 188 | 189 | test "preserve comment after comma" { 190 | if (true) return error.SkipZigTest; 191 | try testTransform( 192 | \\(1, #hello world 193 | \\ 2) 194 | \\ 195 | , 196 | \\( 197 | \\ 1, #hello world 198 | \\ 2, 199 | \\) 200 | \\ 201 | ); 202 | try testTransform( 203 | \\(1#hello world 204 | \\ , 2) 205 | \\ 206 | , 207 | \\( 208 | \\ 1, #hello world 209 | \\ 2, 210 | \\) 211 | \\ 212 | ); 213 | } 214 | 215 | test "preserve comments" { 216 | if (true) return error.SkipZigTest; 217 | try testCanonical( 218 | \\#some comment 219 | \\123 + 220 | \\ #another comment 221 | \\ #third comment 222 | \\ 2 223 | \\#fourth comment 224 | \\#fifth comment 225 | \\ 226 | ); 227 | } 228 | 229 | test "match" { 230 | try testCanonical( 231 | \\match 2 232 | \\ let (x, 2) => x + 4 233 | \\ 2, 3 => 1 234 | \\ _ => null 235 | \\ 236 | \\ 237 | \\ 238 | ); 239 | } 240 | 241 | test "if" { 242 | try testCanonical( 243 | \\if foo bar else baz 244 | \\if let foo = bar() baz 245 | \\ 246 | ); 247 | } 248 | 249 | test "tuples, lists, maps" { 250 | try testCanonical( 251 | \\(a, b) 252 | \\[a, b] 253 | \\{a = b, c = d} 254 | \\ 255 | ); 256 | try testTransform( 257 | \\(a,b,c,) 258 | , 259 | \\( 260 | \\ a, 261 | \\ b, 262 | \\ c, 263 | \\) 264 | \\ 265 | ); 266 | } 267 | 268 | test "functions" { 269 | try testCanonical( 270 | \\let foo = fn(arg1, arg2, _, arg3) (arg1, arg2, arg3) 271 | \\let bar = fn(val) 272 | \\ val * 45 273 | \\ 274 | \\ 275 | \\ 276 | ); 277 | } 278 | 279 | test "unicode identifiers" { 280 | try testTransform( 281 | \\öäöäö;öö 282 | , 283 | \\öäöäö 284 | \\öö 285 | \\ 286 | ); 287 | } 288 | 289 | test "trailing comma in call" { 290 | try testCanonical( 291 | \\foo(2, 3) 292 | \\bar( 293 | \\ 2, 294 | \\ 3, 295 | \\) 296 | \\ 297 | ); 298 | try testTransform( 299 | \\foo(2, 3,) 300 | \\bar( 301 | \\ 2, 302 | \\ 3 303 | \\) 304 | \\ 305 | , 306 | \\foo( 307 | \\ 2, 308 | \\ 3, 309 | \\) 310 | \\bar( 311 | \\ 2, 312 | \\ 3, 313 | \\) 314 | \\ 315 | ); 316 | } 317 | 318 | test "loops" { 319 | try testCanonical( 320 | \\while true break 321 | \\return 123 // 4 322 | \\for let foo in arr foo + 2 323 | \\for 1:3 continue 324 | \\ 325 | ); 326 | } 327 | 328 | test "declarations" { 329 | try testCanonical( 330 | \\let bar = import "args" 331 | \\let foo = bar + 2 332 | \\let err = error(foo) 333 | \\ 334 | ); 335 | } 336 | 337 | test "suffix ops" { 338 | try testCanonical( 339 | \\foo[2].bar(2).baz[5 + 5] 340 | \\ 341 | ); 342 | } 343 | 344 | test "prefix ops" { 345 | try testCanonical( 346 | \\not true 347 | \\-2 348 | \\ 349 | ); 350 | } 351 | 352 | test "infix ops" { 353 | try testCanonical( 354 | \\123 + 2 * 3 / (4 as num) + null 355 | \\ 356 | ); 357 | } 358 | 359 | const std = @import("std"); 360 | const mem = std.mem; 361 | const warn = std.debug.warn; 362 | const bog = @import("bog"); 363 | 364 | fn testTransform(source: []const u8, expected: []const u8) !void { 365 | _ = bog.Vm; // avoid false dependency loop 366 | var errors = bog.Errors.init(std.testing.allocator); 367 | defer errors.deinit(); 368 | var tree = bog.parse(std.testing.allocator, source, "", &errors) catch |e| switch (e) { 369 | else => @panic("test failure"), 370 | error.TokenizeError, error.ParseError => { 371 | errors.render(std.io.getStdErr().writer()) catch {}; 372 | @panic("test failure"); 373 | }, 374 | }; 375 | defer tree.deinit(std.testing.allocator); 376 | 377 | var out_buf = std.ArrayList(u8).init(std.testing.allocator); 378 | defer out_buf.deinit(); 379 | _ = tree.render(out_buf.writer()) catch @panic("test failure"); 380 | try std.testing.expectEqualStrings(expected, out_buf.items); 381 | } 382 | 383 | fn testCanonical(source: []const u8) !void { 384 | try testTransform(source, source); 385 | } 386 | --------------------------------------------------------------------------------