From f77f9d4dff105950f9ebeaf0a65aca81f9d2c8ff Mon Sep 17 00:00:00 2001 From: allexanderbergmns Date: Mon, 24 Aug 2026 15:35:11 +0200 Subject: [PATCH] Enhacnementsand toolchain --- .gitignore | 3 + README.MD | 163 ++++++++++++- assembler/assembler.v | 311 +++++++++++++++++++++++++ bin/tl_alloc.v | 52 +++++ bin/tl_loader.v | 71 ++++-- compiler/compiler.v | 350 ++++++++++++++++++++++++++++ compiler/lexer.v | 275 ++++++++++++++++++++++ compiler/parser.v | 388 +++++++++++++++++++++++++++++++ examples/fib.vr | 12 + examples/hello.vr | 74 ++++++ examples/lib.vr | 17 ++ examples/math.vasm | 29 +++ examples/tests.vr | 41 ++++ examples/use_lib.vr | 23 ++ linker/linker.v | 61 +++++ main.v | 483 ++++++++++++++++++++++++++++++++++++-- obj/obj.v | 233 +++++++++++++++++++ v.mod | 7 + vm/vm.v | 523 ++++++++++++++++++++++++++++++++++++++++++ 19 files changed, 3071 insertions(+), 45 deletions(-) create mode 100644 .gitignore create mode 100644 assembler/assembler.v create mode 100644 bin/tl_alloc.v create mode 100644 compiler/compiler.v create mode 100644 compiler/lexer.v create mode 100644 compiler/parser.v create mode 100644 examples/fib.vr create mode 100644 examples/hello.vr create mode 100644 examples/lib.vr create mode 100644 examples/math.vasm create mode 100644 examples/tests.vr create mode 100644 examples/use_lib.vr create mode 100644 linker/linker.v create mode 100644 obj/obj.v create mode 100644 v.mod create mode 100644 vm/vm.v diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fc9ecbd --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +bin/vr +*.vobj +*.vbin diff --git a/README.MD b/README.MD index a29687a..95757d0 100644 --- a/README.MD +++ b/README.MD @@ -1 +1,162 @@ -// SOON \ No newline at end of file +# vuurraaf/v + +A complete toolchain for **VuurRaaf**, written in V from scratch: a compiler, +an assembler, a linker, and a stack-based runtime. Everything — including the +object file format and the virtual machine — lives in this repository. + +``` +.vr --compiler--> .vobj --linker--> .vbin --vm--> output +.vasm --assembler--> .vobj +``` + +## Build + +Requires [V](https://vlang.io) (`v` in your PATH). + +```bash +v -o bin/vr . # build the toolchain +./bin/vr up # or rebuild from inside the toolchain +./bin/vr symlink # optionally symlink bin/vr into your PATH +``` + +> Note: build with `v .` from the project root. Building via an explicit file +> or path argument makes V pick tcc without the Boehm GC, a combination that +> miscompiles this codebase (`vr up` already does the right thing). + +## Usage + +``` +vr compile [-o out.vobj] source -> object +vr assemble [-o out.vobj] assembly -> object +vr link [more.vobj ...] [-o out] objects -> executable (.vbin) +vr run compile+link+run, or run a binary +vr debug run with an instruction trace +vr test run every test_* function +vr bench [iterations] benchmark main() +vr clean remove .vobj/.vbin artifacts +vr up rebuild bin/vr +vr symlink link bin/vr into your PATH +vr config [set ] toolchain config (outdir, verbose) +vr info | loader | alloc | version | help +``` + +Quick start: + +```bash +./bin/vr run examples/hello.vr # run a program +./bin/vr test examples/tests.vr # run the tests (one fails on purpose) +./bin/vr debug examples/hello.vr # watch every bytecode instruction + +# the assembler path +./bin/vr assemble examples/math.vasm -o math.vobj +./bin/vr link math.vobj -o math.vbin +./bin/vr run math.vbin + +# multi-file programs (functions in one file may call functions in another) +./bin/vr compile examples/lib.vr -o lib.vobj +./bin/vr compile examples/use_lib.vr -o use_lib.vobj +./bin/vr link lib.vobj use_lib.vobj -o use_lib.vbin +./bin/vr run use_lib.vbin +``` + +## The VuurRaaf language + +A small, V-flavored language. Everything is a 64-bit integer or a string +(strings concatenate with `+` and compare with `==`/`!=`; `print`/`println` +accept both). + +``` +fn greet(name) { + println("hello, " + name + "!") +} + +fn fib(n) { + if n < 2 { + return n + } + return fib(n - 1) + fib(n - 2) +} + +fn main() { + let x = 6 * 7 // let with type inference + assert x == 42 // checked by the vm at runtime + let big = x > 40 and x < 50 // and / or / not, short-circuiting + if big { + println("x is big") + } else { + println("x is small") + } + let i = 0 + while i < 3 { + println("counting " + i) // "counting 0", ... + i = i + 1 + } + println(fib(10)) // 55 +} +``` + +- functions: `fn name(a, b) { ... }` with `return expr` +- variables: `let name = expr`, reassignment `name = expr` +- operators: `+ - * / %`, `== != < <= > >=`, `and or not`, unary `-` +- statements: `let`, assignment, `if/else`, `while`, `return`, `assert`, calls, + `print(...)` / `println(...)` +- comments: `//` + +## Assembly + +`.vasm` files talk to the VM directly. Labels, `.global` exports, and the full +opcode set: + +``` +; comment +.global main +main: + push_int 42 + call helper 1 ; call + println + halt + +.global helper +helper: + enter 0 ; reserve extra locals (args were copied in by `call`) + load 0 + retv +``` + +Opcodes: `halt push_int push_str load store pop dup add sub mul div mod neg +eq ne lt le gt ge and or not jmp jz jnz call ret retv print println assert +enter`. + +## Formats + +- **VROBJ** (`.vobj`) — linker input: bytecode, exported symbols (function + name -> code offset), string constants, and relocations (call sites and + string references). +- **VRBIN** (`.vbin`) — the executable: function table, string table, bytecode. + +## Architecture + +| module | role | +|--------------|-------------------------------------------------------------| +| `compiler/` | lexer, recursive-descent parser, bytecode codegen (VROBJ) | +| `assembler/` | `.vasm` -> VROBJ | +| `linker/` | resolves relocations, rebases strings, emits VRBIN | +| `vm/` | stack VM: tagged values, call frames, string heap, tracing | +| `obj/` | VROBJ/VRBIN binary formats | +| `bin/` | small standalone tools: `tl_alloc.v`, `tl_loader.v` | + +The VM is a stack machine with 64-bit tagged values (numbers are stored +shifted left so no integer collides with a string handle). Calls push a frame +(return address, base pointer, argc), copy arguments into local slots, and +reserve extra locals with `enter n`. `vr debug` prints every instruction with +the stack contents. + +## Repository layout + +``` +main.v CLI entry point (vr ...) +v.mod module definition +compiler/ assembler/ linker/ vm/ obj/ the toolchain itself +bin/ built binary + standalone tools +examples/ runnable examples (hello, lib, asm, tests, fib) +``` diff --git a/assembler/assembler.v b/assembler/assembler.v new file mode 100644 index 0000000..4d75412 --- /dev/null +++ b/assembler/assembler.v @@ -0,0 +1,311 @@ +// assembler.v — assembles VuurRaaf assembly (.vasm) into VROBJ object files. +// +// Assembly syntax: +// ; comment +// .global main ; export the label as a linkable symbol +// main: ; label definition (local unless .global) +// push_int 42 +// push_str "hello" +// call helper 1 ; call +// println +// halt +// +// Jumps (jmp/jz/jnz) target local labels and are resolved here; calls to +// exported symbols become relocations resolved by the linker. +module assembler + +import os +import obj + +pub fn assemble(src string) !obj.Obj { + lines := src.split_into_lines() + // pass 1: collect labels, globals, and code offsets + mut labels := map[string]int{} + mut globals := map[string]bool{} + mut offset := 0 + for raw in lines { + line := clean(raw) + if line.len == 0 { + continue + } + if line.starts_with('.global') { + name := line.all_after('.global').trim_space() + if name.len == 0 { + return error('`.global` needs a name') + } + globals[name] = true + continue + } + if line.ends_with(':') { + name := line[..line.len - 1].trim_space() + if name in labels { + return error('duplicate label "${name}"') + } + labels[name] = offset + continue + } + offset += instr_len(line)! + } + // pass 2: emit code + mut o := obj.Obj{} + for raw in lines { + line := clean(raw) + if line.len == 0 || line.starts_with('.global') || line.ends_with(':') { + continue + } + parts := split_line(line)! + op := parts[0] + arg := if parts.len > 1 { parts[1] } else { '' } + match op { + 'halt' { + o.code << u8(0) + } + 'push_int' { + val := parse_int(arg, 'push_int')! + o.code << u8(1) + o.code << obj.encode_i64(val) + } + 'push_str' { + s := unquote(arg)! + o.code << u8(2) + o.code << obj.encode_i64(0) // placeholder — rebased by the linker + o.relocs << obj.Reloc{ offset: u32(o.code.len) - 8, name: s, kind: 1 } + } + 'load' { + o.code << u8(3) + o.code << obj.encode_i64(parse_int(arg, 'load')!) + } + 'store' { + o.code << u8(4) + o.code << obj.encode_i64(parse_int(arg, 'store')!) + } + 'pop' { + o.code << u8(5) + } + 'dup' { + o.code << u8(6) + } + 'add' { + o.code << u8(7) + } + 'sub' { + o.code << u8(8) + } + 'mul' { + o.code << u8(9) + } + 'div' { + o.code << u8(10) + } + 'mod' { + o.code << u8(11) + } + 'neg' { + o.code << u8(12) + } + 'eq' { + o.code << u8(13) + } + 'ne' { + o.code << u8(14) + } + 'lt' { + o.code << u8(15) + } + 'le' { + o.code << u8(16) + } + 'gt' { + o.code << u8(17) + } + 'ge' { + o.code << u8(18) + } + 'and' { + o.code << u8(19) + } + 'or' { + o.code << u8(20) + } + 'not' { + o.code << u8(21) + } + 'jmp' { + o.code << u8(22) + o.code << obj.encode_i64(i64(label(labels, arg)!)) + } + 'jz' { + o.code << u8(23) + o.code << obj.encode_i64(i64(label(labels, arg)!)) + } + 'jnz' { + o.code << u8(24) + o.code << obj.encode_i64(i64(label(labels, arg)!)) + } + 'call' { + o.code << u8(25) + o.code << obj.encode_i64(0) // placeholder for the target + if arg in labels && arg !in globals { + obj.patch_i64(mut o.code, u32(o.code.len) - 8, i64(labels[arg])) + } else { + // reference to an exported symbol — resolved at link time + o.relocs << obj.Reloc{ offset: u32(o.code.len) - 8, name: arg, kind: 0 } + } + argc := if parts.len > 2 { parse_int(parts[2], 'call argc')! } else { i64(0) } + o.code << obj.encode_i64(argc) + } + 'ret' { + o.code << u8(26) + } + 'retv' { + o.code << u8(27) + } + 'print' { + o.code << u8(28) + } + 'println' { + o.code << u8(29) + } + 'assert' { + o.code << u8(30) + } + 'enter' { + o.code << u8(31) + o.code << obj.encode_i64(parse_int(arg, 'enter')!) + } + else { + return error('unknown instruction "${op}"') + } + } + } + // export the globals as symbols + for name in globals.keys() { + entry := labels[name] or { return error('.global "${name}" is not a defined label') } + o.symbols << obj.Symbol{ name: name, entry: entry } + } + return o +} + +pub fn assemble_file(path string) !obj.Obj { + src := os.read_file(path)! + return assemble(src)! +} + +// --------------------------------------------------------------------------- + +fn clean(line string) string { + // strip ';' comments and trim + if idx := line.index(';') { + return line[..idx].trim_space() + } + return line.trim_space() +} + +// split_line splits an assembly line into whitespace-separated fields while +// keeping a double-quoted string (which may contain spaces) as a single field. +fn split_line(line string) ![]string { + mut parts := []string{} + mut i := 0 + for i < line.len { + for i < line.len && (line[i] == ` ` || line[i] == `\t`) { + i++ + } + if i >= line.len { + break + } + if line[i] == `"` { + // scan to the closing quote + start := i + i++ + for i < line.len && line[i] != `"` { + i++ + } + i++ + parts << line[start..if i <= line.len { i } else { line.len }] + } else { + start := i + for i < line.len && line[i] != ` ` && line[i] != `\t` { + i++ + } + parts << line[start..i] + } + } + return parts +} + +fn instr_len(line string) !int { + parts := split_line(line)! + op := parts[0] + match op { + 'halt', 'pop', 'dup', 'add', 'sub', 'mul', 'div', 'mod', 'neg', 'eq', 'ne', 'lt', 'le', + 'gt', 'ge', 'and', 'or', 'not', 'ret', 'retv', 'print', 'println', 'assert' { + return 1 + } + 'push_int', 'push_str', 'load', 'store', 'jmp', 'jz', 'jnz', 'enter' { + return 9 + } + 'call' { + return 17 // opcode + target(8) + argc(8) + } + else { + return error('unknown instruction "${op}"') + } + } +} + +fn label(labels map[string]int, name string) !int { + return labels[name] or { return error('unknown label "${name}"') } +} + +fn parse_int(s string, what string) !i64 { + if s.len == 0 { + return error('${what}: bad integer "${s}"') + } + start := if s[0] == `-` { 1 } else { 0 } + if start >= s.len { + return error('${what}: bad integer "${s}"') + } + for i in start..s.len { + if s[i] < `0` || s[i] > `9` { + return error('${what}: bad integer "${s}"') + } + } + return s.i64() +} + +fn unquote(s string) !string { + if s.len < 2 || s[0] != `"` || s[s.len - 1] != `"` { + return error('expected a string literal, got "${s}"') + } + inner := s[1..s.len - 1] + mut out := '' + mut i := 0 + for i < inner.len { + c := inner[i] + if c == `\\` && i + 1 < inner.len { + i++ + match inner[i] { + `n` { + out += '\n' + } + `t` { + out += '\t' + } + `"` { + out += '"' + } + `\\` { + out += '\\' + } + else { + out += '\\${inner[i].ascii_str()}' + } + } + } else { + out += c.ascii_str() + } + i++ + } + return out +} + diff --git a/bin/tl_alloc.v b/bin/tl_alloc.v new file mode 100644 index 0000000..3407a9e --- /dev/null +++ b/bin/tl_alloc.v @@ -0,0 +1,52 @@ +// toolchain alloc +// A tiny byte arena for toolchain scratch memory: allocate fixed-size blocks +// from a pre-sized arena and release them all at once. +// Run standalone with: v run bin/tl_alloc.v + +module main + +struct Arena { +mut: + data []u8 + used int +} + +// new_arena reserves `cap` bytes of scratch memory. +fn new_arena(cap int) Arena { + return Arena{ + data: []u8{len: cap} + used: 0 + } +} + +// alloc returns an offset into the arena for a block of `n` bytes. +fn (mut a Arena) alloc(n int) !int { + if a.used + n > a.data.len { + return error('arena out of memory: need ${n} bytes, ${a.data.len - a.used} left') + } + off := a.used + a.used += n + return off +} + +// free_all releases everything allocated so far. +fn (mut a Arena) free_all() { + a.used = 0 +} + +fn (a Arena) stats() string { + return 'arena: ${a.used} / ${a.data.len} bytes used' +} + +fn main() { + mut arena := new_arena(1024 * 1024) // 1 MiB + println('toolchain alloc: arena ready') + println(arena.stats()) + off1 := arena.alloc(64) or { eprintln(err); exit(1) } + off2 := arena.alloc(4096) or { eprintln(err); exit(1) } + println('allocated 64 bytes at offset ${off1}') + println('allocated 4096 bytes at offset ${off2}') + println(arena.stats()) + arena.free_all() + println('after free_all: ${arena.stats()}') +} diff --git a/bin/tl_loader.v b/bin/tl_loader.v index 0ba2f36..67f2db1 100644 --- a/bin/tl_loader.v +++ b/bin/tl_loader.v @@ -1,28 +1,53 @@ // toolchain loader +// Discovers and describes the toolchain components baked into the binary. +// Run standalone with: v run bin/tl_loader.v -module tl_loader +module main import os -import v.vmod -pub fn load_toolchain() { - // Load the toolchain modules and initialize them - // We call the package functions to load the toolchain modules and initialize them - // each package has their own load argument, which is passed to the package function. - toolchain_loader(load) - toolchain_alloc(load) - toolchain_compile(load) - toolchain_assemble(load) - toolchain_link(load) - toolchain_run(load) - toolchain_debug(load) - toolchain_help(load) - toolchain_version(load) - toolchain_info(load) - toolchain_config(load) - toolchain_test(load) - toolchain_bench(load) - toolchain_clean(load) - toolchain_up(load) - toolchain_symlink(load) -} \ No newline at end of file +struct Component { +mut: + name string + role string + path string + enabled bool +} + +// load_toolchain returns the components that make up this toolchain. +fn load_toolchain() []Component { + root := os.dir(os.executable()) + return [ + Component{ + name: 'compiler' + role: 'lexer, parser, bytecode codegen' + path: os.join_path(root, 'compiler') + enabled: true + }, + Component{ + name: 'assembler' + role: 'vasm -> vobj' + path: os.join_path(root, 'assembler') + enabled: true + }, + Component{ + name: 'linker' + role: 'vobj set -> vbin' + path: os.join_path(root, 'linker') + enabled: true + }, + Component{ + name: 'vm' + role: 'stack-based runtime' + path: os.join_path(root, 'vm') + enabled: true + }, + ] +} + +fn main() { + for c in load_toolchain() { + status := if c.enabled { 'loaded' } else { 'disabled' } + println('[${status}] ${c.name:-12} ${c.role} (${c.path})') + } +} diff --git a/compiler/compiler.v b/compiler/compiler.v new file mode 100644 index 0000000..c5d7dbe --- /dev/null +++ b/compiler/compiler.v @@ -0,0 +1,350 @@ +// compiler.v — bytecode code generator for VuurRaaf. +// +// Compiles a parsed program into a VROBJ object file: flat bytecode plus a +// symbol per function and a relocation per call site. Call targets are left as +// relocations and resolved by the linker, so functions may live in other files. +module compiler + +import os +import obj + +// opcodes — keep in sync with vm/vm.v and assembler/assembler.v +const op_halt = u8(0) +const op_push_i = u8(1) +const op_push_s = u8(2) +const op_load = u8(3) +const op_store = u8(4) +const op_pop = u8(5) +const op_dup = u8(6) +const op_add = u8(7) +const op_sub = u8(8) +const op_mul = u8(9) +const op_div = u8(10) +const op_mod = u8(11) +const op_neg = u8(12) +const op_eq = u8(13) +const op_ne = u8(14) +const op_lt = u8(15) +const op_le = u8(16) +const op_gt = u8(17) +const op_ge = u8(18) +const op_and = u8(19) +const op_or = u8(20) +const op_not = u8(21) +const op_jmp = u8(22) +const op_jz = u8(23) +const op_jnz = u8(24) +const op_call = u8(25) +const op_ret = u8(26) +const op_retv = u8(27) +const op_print = u8(28) +const op_println = u8(29) +const op_assert = u8(30) +const op_enter = u8(31) + +// compile parses and compiles VuurRaaf source into an object file. +pub fn compile(src string) !obj.Obj { + toks := tokenize(src)! + prog := parse(toks)! + return gen(prog) +} + +pub fn compile_file(path string) !obj.Obj { + src := os.read_file(path)! + return compile(src)! +} + +// --------------------------------------------------------------------------- + +struct Fixup { + name string + off u32 +} + +struct Gen { +mut: + code []u8 + strings []string + str_map map[string]int + symbols []obj.Symbol + relocs []obj.Reloc + locals map[string]int + local_cnt int + argc int + cur_fn string + labels map[string]int + fixups []Fixup + enter_off u32 + next_lbl int +} + +fn gen(prog Program) !obj.Obj { + mut g := Gen{} + for fd in prog.fns { + g.gen_fn(fd)! + } + return obj.Obj{ + symbols: g.symbols + strings: g.strings + code: g.code + relocs: g.relocs + } +} + +fn (mut g Gen) gen_fn(fd FnDecl) ! { + g.cur_fn = fd.name + g.symbols << obj.Symbol{ name: fd.name, entry: g.code.len } + g.locals.clear() + g.local_cnt = 0 + g.argc = fd.params.len + for i, p in fd.params { + g.locals[p] = i + } + g.local_cnt = g.argc + // `enter n` reserves the non-parameter locals; n is patched once the body + // has been scanned. + g.code << op_enter + g.enter_off = u32(g.code.len) + g.code << obj.encode_i64(0) + for st in fd.body { + g.gen_stmt(st)! + } + g.code << op_ret // trailing return for fall-through + obj.patch_i64(mut g.code, g.enter_off, i64(g.local_cnt - g.argc)) + // resolve intra-function jump targets + for f in g.fixups { + target := g.labels[f.name] or { + return error('internal error: unresolved label ${f.name} in fn ${fd.name}') + } + obj.patch_i64(mut g.code, f.off, i64(target)) + } + g.fixups.clear() + g.labels.clear() + g.cur_fn = '' +} + +fn (mut g Gen) gen_stmt(st Stmt) ! { + match st.kind { + .expr_stmt { + g.gen_expr(st.expr)! + // print/println already consume their value; everything else + // leaves one on the stack that must be discarded + if st.expr.kind == .call && (st.expr.name == 'print' || st.expr.name == 'println') { + // nothing to discard + } else { + g.code << op_pop + } + } + .let_stmt { + g.gen_expr(st.expr)! + idx := g.local_cnt + g.local_cnt++ + g.locals[st.target] = idx + g.code << op_store + g.code << obj.encode_i64(i64(idx)) + } + .assign_stmt { + idx := g.locals[st.target] or { + return error('unknown variable "${st.target}" at line ${st.line}') + } + g.gen_expr(st.expr)! + g.code << op_store + g.code << obj.encode_i64(i64(idx)) + } + .if_stmt { + else_l := g.new_label() + end_l := g.new_label() + g.gen_expr(st.cond)! + g.code << op_jz + g.code << obj.encode_i64(0) + g.fixups << Fixup{ name: else_l, off: u32(g.code.len) - 8 } + for s in st.body { + g.gen_stmt(s)! + } + g.code << op_jmp + g.code << obj.encode_i64(0) + g.fixups << Fixup{ name: end_l, off: u32(g.code.len) - 8 } + g.emit_label(else_l) + for s in st.els { + g.gen_stmt(s)! + } + g.emit_label(end_l) + } + .while_stmt { + loop_l := g.new_label() + end_l := g.new_label() + g.emit_label(loop_l) + g.gen_expr(st.cond)! + g.code << op_jz + g.code << obj.encode_i64(0) + g.fixups << Fixup{ name: end_l, off: u32(g.code.len) - 8 } + for s in st.body { + g.gen_stmt(s)! + } + g.code << op_jmp + g.code << obj.encode_i64(0) + g.fixups << Fixup{ name: loop_l, off: u32(g.code.len) - 8 } + g.emit_label(end_l) + } + .ret_stmt { + if st.has_val { + g.gen_expr(st.expr)! + g.code << op_retv + } else { + g.code << op_ret + } + } + .assert_stmt { + g.gen_expr(st.expr)! + g.code << op_assert + } + } +} + +fn (mut g Gen) gen_expr(e Expr) ! { + match e.kind { + .int_lit { + g.code << op_push_i + g.code << obj.encode_i64(e.int_v) + } + .str_lit { + // the index is a placeholder; the linker rebases it via a string + // relocation so multi-file links keep working + g.code << op_push_s + g.code << obj.encode_i64(0) + g.relocs << obj.Reloc{ offset: u32(g.code.len) - 8, name: e.str_v, kind: 1 } + } + .bool_lit { + g.code << op_push_i + g.code << obj.encode_i64(e.int_v) + } + .ident { + idx := g.locals[e.name] or { + return error('unknown variable "${e.name}" at line ${e.line}') + } + g.code << op_load + g.code << obj.encode_i64(i64(idx)) + } + .unary { + g.gen_expr(*e.right)! + if e.op == .kw_not { + g.code << op_not + } else { + g.code << op_neg + } + } + .binary { + g.gen_binary(e)! + } + .call { + g.gen_call(e)! + } + } +} + +fn (mut g Gen) gen_call(e Expr) ! { + if e.name == 'print' || e.name == 'println' { + if e.args.len != 1 { + return error('${e.name}() takes exactly one argument (line ${e.line})') + } + g.gen_expr(e.args[0])! + g.code << if e.name == 'print' { op_print } else { op_println } + return + } + for a in e.args { + g.gen_expr(a)! + } + g.code << op_call + g.code << obj.encode_i64(0) // placeholder — patched by the linker + g.relocs << obj.Reloc{ offset: u32(g.code.len) - 8, name: e.name, kind: 0 } + g.code << obj.encode_i64(i64(e.args.len)) // argc +} + +fn (mut g Gen) gen_binary(e Expr) ! { + match e.op { + .kw_and { + // a and b → short-circuit: if !a or !b then 0 else 1 + false_l := g.new_label() + end_l := g.new_label() + g.gen_expr(*e.left)! + g.code << op_jz + g.code << obj.encode_i64(0) + g.fixups << Fixup{ name: false_l, off: u32(g.code.len) - 8 } + g.gen_expr(*e.right)! + g.code << op_jz + g.code << obj.encode_i64(0) + g.fixups << Fixup{ name: false_l, off: u32(g.code.len) - 8 } + g.code << op_push_i + g.code << obj.encode_i64(1) + g.code << op_jmp + g.code << obj.encode_i64(0) + g.fixups << Fixup{ name: end_l, off: u32(g.code.len) - 8 } + g.emit_label(false_l) + g.code << op_push_i + g.code << obj.encode_i64(0) + g.emit_label(end_l) + } + .kw_or { + // a or b → short-circuit: if a or b then 1 else 0 + true_l := g.new_label() + end_l := g.new_label() + g.gen_expr(*e.left)! + g.code << op_jnz + g.code << obj.encode_i64(0) + g.fixups << Fixup{ name: true_l, off: u32(g.code.len) - 8 } + g.gen_expr(*e.right)! + g.code << op_jnz + g.code << obj.encode_i64(0) + g.fixups << Fixup{ name: true_l, off: u32(g.code.len) - 8 } + g.code << op_push_i + g.code << obj.encode_i64(0) + g.code << op_jmp + g.code << obj.encode_i64(0) + g.fixups << Fixup{ name: end_l, off: u32(g.code.len) - 8 } + g.emit_label(true_l) + g.code << op_push_i + g.code << obj.encode_i64(1) + g.emit_label(end_l) + } + else { + g.gen_expr(*e.left)! + g.gen_expr(*e.right)! + op := match e.op { + .plus { op_add } + .minus { op_sub } + .star { op_mul } + .slash { op_div } + .percent { op_mod } + .eq_eq { op_eq } + .not_eq { op_ne } + .lt { op_lt } + .le { op_le } + .gt { op_gt } + .ge { op_ge } + else { + return error('unsupported binary operator at line ${e.line}') + } + } + g.code << op + } + } +} + +fn (mut g Gen) intern(s string) int { + if s in g.str_map { + return g.str_map[s] + } + idx := g.strings.len + g.strings << s + g.str_map[s] = idx + return idx +} + +fn (mut g Gen) new_label() string { + g.next_lbl++ + return 'L${g.next_lbl}' +} + +fn (mut g Gen) emit_label(name string) { + g.labels[name] = g.code.len +} diff --git a/compiler/lexer.v b/compiler/lexer.v new file mode 100644 index 0000000..c7a27d5 --- /dev/null +++ b/compiler/lexer.v @@ -0,0 +1,275 @@ +// lexer.v — tokenizer for the VuurRaaf source language (.vr). +module compiler + +pub enum TokKind { + eof + ident + int_lit + str_lit + lparen + rparen + lbrace + rbrace + comma + plus + minus + star + slash + percent + eq_eq + not_eq + lt + le + gt + ge + assign + kw_fn + kw_let + kw_if + kw_else + kw_while + kw_return + kw_true + kw_false + kw_and + kw_or + kw_not + kw_print + kw_println + kw_assert +} + +pub struct Tok { +pub: + kind TokKind + lit string + line int +} + +pub fn tokenize(src string) ![]Tok { + mut l := Lexer{ src: src } + mut toks := []Tok{} + for { + t := l.next()! + toks << t + if t.kind == .eof { + break + } + } + return toks +} + +struct Lexer { +mut: + src string + pos int + line int +} + +fn (mut l Lexer) peek() u8 { + if l.pos >= l.src.len { + return 0 + } + return l.src[l.pos] +} + +fn (mut l Lexer) peek2() u8 { + if l.pos + 1 >= l.src.len { + return 0 + } + return l.src[l.pos + 1] +} + +fn (mut l Lexer) advance() u8 { + c := l.src[l.pos] + l.pos++ + if c == `\n` { + l.line++ + } + return c +} + +fn (mut l Lexer) next() !Tok { + // skip whitespace and // comments + for l.pos < l.src.len { + c := l.peek() + if c == ` ` || c == `\t` || c == `\r` || c == `\n` { + l.advance() + continue + } + if c == `/` && l.peek2() == `/` { + for l.pos < l.src.len && l.peek() != `\n` { + l.advance() + } + continue + } + break + } + line := l.line + if l.pos >= l.src.len { + return Tok{ kind: .eof, lit: '', line: line } + } + c := l.peek() + match c { + `(` { + l.advance() + return Tok{ kind: .lparen, lit: '(', line: line } + } + `)` { + l.advance() + return Tok{ kind: .rparen, lit: ')', line: line } + } + `{` { + l.advance() + return Tok{ kind: .lbrace, lit: '{', line: line } + } + `}` { + l.advance() + return Tok{ kind: .rbrace, lit: '}', line: line } + } + `,` { + l.advance() + return Tok{ kind: .comma, lit: ',', line: line } + } + `+` { + l.advance() + return Tok{ kind: .plus, lit: '+', line: line } + } + `-` { + l.advance() + return Tok{ kind: .minus, lit: '-', line: line } + } + `*` { + l.advance() + return Tok{ kind: .star, lit: '*', line: line } + } + `/` { + l.advance() + return Tok{ kind: .slash, lit: '/', line: line } + } + `%` { + l.advance() + return Tok{ kind: .percent, lit: '%', line: line } + } + `=` { + l.advance() + if l.peek() == `=` { + l.advance() + return Tok{ kind: .eq_eq, lit: '==', line: line } + } + return Tok{ kind: .assign, lit: '=', line: line } + } + `!` { + l.advance() + if l.peek() == `=` { + l.advance() + return Tok{ kind: .not_eq, lit: '!=', line: line } + } + return error('unexpected character "!" at line ${line} (did you mean "not"?)') + } + `<` { + l.advance() + if l.peek() == `=` { + l.advance() + return Tok{ kind: .le, lit: '<=', line: line } + } + return Tok{ kind: .lt, lit: '<', line: line } + } + `>` { + l.advance() + if l.peek() == `=` { + l.advance() + return Tok{ kind: .ge, lit: '>=', line: line } + } + return Tok{ kind: .gt, lit: '>', line: line } + } + `"` { + return l.lex_string(line)! + } + `0`...`9` { + return l.lex_number(line) + } + else { + if (c >= `a` && c <= `z`) || (c >= `A` && c <= `Z`) || c == `_` { + return l.lex_ident(line) + } + return error('unexpected character "${c.ascii_str()}" at line ${line}') + } + } +} + +fn (mut l Lexer) lex_number(line int) Tok { + start := l.pos + for l.pos < l.src.len && l.peek() >= `0` && l.peek() <= `9` { + l.advance() + } + return Tok{ kind: .int_lit, lit: l.src[start..l.pos], line: line } +} + +fn (mut l Lexer) lex_ident(line int) Tok { + start := l.pos + for l.pos < l.src.len { + c := l.peek() + if (c >= `a` && c <= `z`) || (c >= `A` && c <= `Z`) || (c >= `0` && c <= `9`) || c == `_` { + l.advance() + } else { + break + } + } + lit := l.src[start..l.pos] + kind := match lit { + 'fn' { TokKind.kw_fn } + 'let' { TokKind.kw_let } + 'if' { TokKind.kw_if } + 'else' { TokKind.kw_else } + 'while' { TokKind.kw_while } + 'return' { TokKind.kw_return } + 'true' { TokKind.kw_true } + 'false' { TokKind.kw_false } + 'and' { TokKind.kw_and } + 'or' { TokKind.kw_or } + 'not' { TokKind.kw_not } + 'print' { TokKind.kw_print } + 'println' { TokKind.kw_println } + 'assert' { TokKind.kw_assert } + else { TokKind.ident } + } + return Tok{ kind: kind, lit: lit, line: line } +} + +fn (mut l Lexer) lex_string(line int) !Tok { + l.advance() // opening quote + mut s := '' + for l.pos < l.src.len { + c := l.advance() + if c == `"` { + return Tok{ kind: .str_lit, lit: s, line: line } + } + if c == `\\` { + if l.pos >= l.src.len { + break + } + e := l.advance() + match e { + `n` { + s += '\n' + } + `t` { + s += '\t' + } + `"` { + s += '"' + } + `\\` { + s += '\\' + } + else { + return error('invalid escape \\${e.ascii_str()} at line ${line}') + } + } + continue + } + s += c.ascii_str() + } + return error('unterminated string at line ${line}') +} diff --git a/compiler/parser.v b/compiler/parser.v new file mode 100644 index 0000000..830880a --- /dev/null +++ b/compiler/parser.v @@ -0,0 +1,388 @@ +// parser.v — recursive-descent parser for the VuurRaaf language. +// +// Grammar (informal): +// program := fn* +// fn := 'fn' IDENT '(' [IDENT (',' IDENT)*] ')' block +// block := '{' stmt* '}' +// stmt := 'let' IDENT '=' expr +// | IDENT '=' expr +// | 'if' cond block ['else' block] +// | 'while' cond block +// | 'return' [expr] +// | 'assert' expr +// | expr +// cond := ['('] expr [')'] // parens optional +// expr := or ('or' or)* +// or := and ('and' and)* +// and := eq (('=='|'!=') eq)* +// eq := rel (('<'|'<='|'>'|'>=') rel)* +// rel := add (('+'|'-') add)* +// add := mul (('*'|'/'|'%') mul)* +// mul := ('not'|'-') mul | primary +// primary := INT | STR | 'true' | 'false' | IDENT ['(' args ')'] | '(' expr ')' +module compiler + +pub enum ExprKind { + int_lit + str_lit + bool_lit + ident + unary + binary + call +} + +pub struct Expr { +pub mut: + kind ExprKind + int_v i64 + str_v string + name string + op TokKind + left &Expr = unsafe { nil } + right &Expr = unsafe { nil } + args []Expr + line int +} + +pub enum StmtKind { + expr_stmt + let_stmt + assign_stmt + if_stmt + while_stmt + ret_stmt + assert_stmt +} + +pub struct Stmt { +pub mut: + kind StmtKind + target string + expr Expr + cond Expr + body []Stmt + els []Stmt + has_val bool + line int +} + +pub struct FnDecl { +pub mut: + name string + params []string + body []Stmt + line int +} + +pub struct Program { +pub mut: + fns []FnDecl +} + +pub fn parse(toks []Tok) !Program { + mut p := Parser{ toks: toks } + return p.parse_program() +} + +struct Parser { +mut: + toks []Tok + pos int +} + +fn (mut p Parser) cur() Tok { + if p.pos < p.toks.len { + return p.toks[p.pos] + } + return p.toks[p.toks.len - 1] +} + +fn (mut p Parser) advance() Tok { + t := p.cur() + if p.pos < p.toks.len - 1 { + p.pos++ + } + return t +} + +fn (mut p Parser) expect(k TokKind, what string) !Tok { + t := p.cur() + if t.kind != k { + return error('expected ${what}, got "${t.lit}" at line ${t.line}') + } + return p.advance() +} + +// parse_cond parses a condition, accepting either `if cond {` or `if (cond) {`. +fn (mut p Parser) parse_cond() !Expr { + if p.cur().kind == .lparen { + p.advance() + e := p.parse_expr()! + p.expect(.rparen, "')'")! + return e + } + return p.parse_expr()! +} + +fn (mut p Parser) parse_program() !Program { + mut prog := Program{} + for p.cur().kind != .eof { + prog.fns << p.parse_fn()! + } + if prog.fns.len == 0 { + return error('no functions found in source') + } + return prog +} + +fn (mut p Parser) parse_fn() !FnDecl { + fn_tok := p.expect(.kw_fn, "'fn'")! + name := p.expect(.ident, 'function name')! + p.expect(.lparen, "'('")! + mut params := []string{} + if p.cur().kind != .rparen { + for { + params << p.expect(.ident, 'parameter name')!.lit + if p.cur().kind == .comma { + p.advance() + continue + } + break + } + } + p.expect(.rparen, "')'")! + body := p.parse_block()! + return FnDecl{ name: name.lit, params: params, body: body, line: fn_tok.line } +} + +fn (mut p Parser) parse_block() ![]Stmt { + p.expect(.lbrace, "'{'")! + mut stmts := []Stmt{} + for p.cur().kind != .rbrace { + if p.cur().kind == .eof { + return error('unexpected end of file inside block (missing "}")') + } + stmts << p.parse_stmt()! + } + p.expect(.rbrace, "'}'")! + return stmts +} + +fn (mut p Parser) parse_stmt() !Stmt { + t := p.cur() + match t.kind { + .kw_let { + p.advance() + name := p.expect(.ident, 'variable name')! + p.expect(.assign, "'='")! + e := p.parse_expr()! + return Stmt{ kind: .let_stmt, target: name.lit, expr: e, line: t.line } + } .kw_if { + p.advance() + cond := p.parse_cond()! + body := p.parse_block()! + mut els := []Stmt{} + if p.cur().kind == .kw_else { + p.advance() + els = p.parse_block()! + } + return Stmt{ kind: .if_stmt, cond: cond, body: body, els: els, line: t.line } + } + .kw_while { + p.advance() + cond := p.parse_cond()! + body := p.parse_block()! + return Stmt{ kind: .while_stmt, cond: cond, body: body, line: t.line } + } + .kw_return { + p.advance() + mut e := Expr{} + has_val := p.cur().kind != .rbrace + if has_val { + e = p.parse_expr()! + } + return Stmt{ kind: .ret_stmt, expr: e, has_val: has_val, line: t.line } + } .kw_assert { + p.advance() + mut e := Expr{} + if p.cur().kind == .lparen { + p.advance() + e = p.parse_expr()! + p.expect(.rparen, "')'")! + } else { + e = p.parse_expr()! + } + return Stmt{ kind: .assert_stmt, expr: e, line: t.line } + } + .ident { + p.advance() + if p.cur().kind == .assign { + p.advance() + e := p.parse_expr()! + return Stmt{ kind: .assign_stmt, target: t.lit, expr: e, line: t.line } + } + e := p.parse_call_or_ident(t)! + return Stmt{ kind: .expr_stmt, expr: e, line: t.line } + } + .kw_print, .kw_println { + p.advance() + e := p.parse_call(t)! + return Stmt{ kind: .expr_stmt, expr: e, line: t.line } + } + else { + return error('unexpected token "${t.lit}" at line ${t.line}') + } + } +} + +// bin_node allocates a binary-operator node. It takes copies of the operands +// so that `&l`/`&r` target fresh heap objects (taking the address of a local +// that is later reassigned would create a self-referential node). +fn bin_node(op TokKind, left Expr, right Expr, line int) Expr { + mut l := left + mut r := right + return Expr{ kind: .binary, op: op, left: &l, right: &r, line: line } +} + +fn unary_node(op TokKind, operand Expr, line int) Expr { + mut o := operand + return Expr{ kind: .unary, op: op, right: &o, line: line } +} + +fn (mut p Parser) parse_expr() !Expr { + return p.parse_or()! +} + +fn (mut p Parser) parse_or() !Expr { + mut e := p.parse_and()! + for p.cur().kind == .kw_or { + op := p.advance() + rhs := p.parse_and()! + e = bin_node(op.kind, e, rhs, op.line) + } + return e +} + +fn (mut p Parser) parse_and() !Expr { + mut e := p.parse_eq()! + for p.cur().kind == .kw_and { + op := p.advance() + rhs := p.parse_eq()! + e = bin_node(op.kind, e, rhs, op.line) + } + return e +} + +fn (mut p Parser) parse_eq() !Expr { + mut e := p.parse_rel()! + for p.cur().kind == .eq_eq || p.cur().kind == .not_eq { + op := p.advance() + rhs := p.parse_rel()! + e = bin_node(op.kind, e, rhs, op.line) + } + return e +} + +fn (mut p Parser) parse_rel() !Expr { + mut e := p.parse_add()! + for p.cur().kind == .lt || p.cur().kind == .le || p.cur().kind == .gt || p.cur().kind == .ge { + op := p.advance() + rhs := p.parse_add()! + e = bin_node(op.kind, e, rhs, op.line) + } + return e +} + +fn (mut p Parser) parse_add() !Expr { + mut e := p.parse_mul()! + for p.cur().kind == .plus || p.cur().kind == .minus { + op := p.advance() + rhs := p.parse_mul()! + e = bin_node(op.kind, e, rhs, op.line) + } + return e +} + +fn (mut p Parser) parse_mul() !Expr { + mut e := p.parse_unary()! + for p.cur().kind == .star || p.cur().kind == .slash || p.cur().kind == .percent { + op := p.advance() + rhs := p.parse_unary()! + e = bin_node(op.kind, e, rhs, op.line) + } + return e +} + +fn (mut p Parser) parse_unary() !Expr { + t := p.cur() + if t.kind == .kw_not || t.kind == .minus { + p.advance() + e := p.parse_unary()! + return unary_node(t.kind, e, t.line) + } + return p.parse_primary()! +} + +fn (mut p Parser) parse_primary() !Expr { + t := p.cur() + match t.kind { + .int_lit { + p.advance() + return Expr{ kind: .int_lit, int_v: t.lit.i64(), line: t.line } + } + .str_lit { + p.advance() + return Expr{ kind: .str_lit, str_v: t.lit, line: t.line } + } + .kw_true { + p.advance() + return Expr{ kind: .bool_lit, int_v: 1, line: t.line } + } + .kw_false { + p.advance() + return Expr{ kind: .bool_lit, int_v: 0, line: t.line } + } + .lparen { + p.advance() + e := p.parse_expr()! + p.expect(.rparen, "')'")! + return e + } + .ident { + p.advance() + return p.parse_call_or_ident(t)! + } + .kw_print, .kw_println { + p.advance() + return p.parse_call(t)! + } + else { + return error('unexpected token "${t.lit}" at line ${t.line}') + } + } +} + +fn (mut p Parser) parse_call_or_ident(t Tok) !Expr { + if p.cur().kind == .lparen { + return p.parse_call(t)! + } + return Expr{ kind: .ident, name: t.lit, line: t.line } +} + +fn (mut p Parser) parse_call(name Tok) !Expr { + p.expect(.lparen, "'('")! + mut args := []Expr{} + if p.cur().kind != .rparen { + for { + args << p.parse_expr()! + if p.cur().kind == .comma { + p.advance() + continue + } + break + } + } + p.expect(.rparen, "')'")! + return Expr{ kind: .call, name: name.lit, args: args, line: name.line } +} diff --git a/examples/fib.vr b/examples/fib.vr new file mode 100644 index 0000000..23af640 --- /dev/null +++ b/examples/fib.vr @@ -0,0 +1,12 @@ +// fib.vr — recursive fibonacci, small enough to benchmark with `vr bench`. + +fn fib(n) { + if n < 2 { + return n + } + return fib(n - 1) + fib(n - 2) +} + +fn main() { + println(fib(20)) +} diff --git a/examples/hello.vr b/examples/hello.vr new file mode 100644 index 0000000..78ad3f9 --- /dev/null +++ b/examples/hello.vr @@ -0,0 +1,74 @@ +// hello.vr — a tour of the VuurRaaf language. +// Run with: vr run examples/hello.vr + +fn greet(name) { + println("hello, " + name + "!") +} + +// recursive fibonacci +fn fib(n) { + if n < 2 { + return n + } + return fib(n - 1) + fib(n - 2) +} + +// returns n! via a loop +fn fact(n) { + let result = 1 + let i = 1 + while i <= n { + result = result * i + i = i + 1 + } + return result +} + +fn main() { + greet("vuurraaf") + + let x = 6 * 7 + println("6 * 7 = ") + println(x) + + let name = "world" + let msg = "hello " + name + println(msg) + println(msg == "hello world") + + // booleans and short-circuiting + let big = x > 40 and x < 50 + let small = x < 10 or x > 100 + println("x > 40 and x < 50: ") + println(big) + println("x < 10 or x > 100: ") + println(small) + println("not big: ") + println(not big) + + // if / else + if x > 40 { + println("x is big") + } else { + println("x is small") + } + + // while loop + let i = 0 + while i < 3 { + println("counting " + i) + i = i + 1 + } + + println("fib(10) = ") + println(fib(10)) + + println("fact(5) = ") + println(fact(5)) + + // asserts are checked at runtime by the vm + assert x == 42 + assert fact(5) == 120 + assert fib(10) == 55 + println("all asserts passed") +} diff --git a/examples/lib.vr b/examples/lib.vr new file mode 100644 index 0000000..26966c4 --- /dev/null +++ b/examples/lib.vr @@ -0,0 +1,17 @@ +// lib.vr — a small library compiled to its own object file. +// vr compile examples/lib.vr -o /tmp/lib.vobj + +fn double(n) { + return n * 2 +} + +fn square(n) { + return n * n +} + +fn describe(n) { + if n > 100 { + return "big" + } + return "small" +} diff --git a/examples/math.vasm b/examples/math.vasm new file mode 100644 index 0000000..9d53c9a --- /dev/null +++ b/examples/math.vasm @@ -0,0 +1,29 @@ +; math.vasm — raw bytecode, hand-written in VuurRaaf assembly. +; Assemble + link + run with: +; vr assemble examples/math.vasm +; vr link math.vobj -o math.vbin +; vr run math.vbin + +.global main + +main: + push_int 6 + push_int 7 + mul + println + push_str "computed 6 * 7 = 42" + println + push_int 40 + push_int 2 + call add 2 ; calls the exported `add` symbol below + println + halt + +; a callable function: takes two args, returns their sum +.global add +add: + enter 0 ; no extra locals (args were copied in by `call`) + load 0 + load 1 + add + retv diff --git a/examples/tests.vr b/examples/tests.vr new file mode 100644 index 0000000..a7f8c98 --- /dev/null +++ b/examples/tests.vr @@ -0,0 +1,41 @@ +// tests.vr — functions named test_* are picked up by `vr test`. +// vr test examples/tests.vr + +fn add(a, b) { + return a + b +} + +fn test_add() { + assert add(2, 3) == 5 + assert add(-1, 1) == 0 +} + +fn test_strings() { + let a = "foo" + let b = "bar" + assert a + b == "foobar" + assert a != b + assert a == "foo" +} + +fn test_loops() { + let total = 0 + let i = 1 + while i <= 10 { + total = total + i + i = i + 1 + } + assert total == 55 +} + +fn test_booleans() { + let x = 7 + assert x > 3 and x < 10 + assert x == 7 or x == 8 + assert not (x == 0) +} + +fn test_failing() { + // this one is meant to fail — shows up in `vr test` output + assert 1 == 2 +} diff --git a/examples/use_lib.vr b/examples/use_lib.vr new file mode 100644 index 0000000..df0246e --- /dev/null +++ b/examples/use_lib.vr @@ -0,0 +1,23 @@ +// use_lib.vr — calls functions defined in lib.vr; the two object files are +// linked together by the linker: +// vr compile examples/lib.vr -o /tmp/lib.vobj +// vr compile examples/use_lib.vr -o /tmp/use_lib.vobj +// vr link /tmp/lib.vobj /tmp/use_lib.vobj -o /tmp/use_lib.vbin +// vr run /tmp/use_lib.vbin + +fn main() { + let d = double(21) + println("double(21) = ") + println(d) + + let s = square(9) + println("square(9) = ") + println(s) + + println("describe(500) = ") + println(describe(500)) + + assert double(21) == 42 + assert square(9) == 81 + println("cross-file calls ok") +} diff --git a/linker/linker.v b/linker/linker.v new file mode 100644 index 0000000..d543add --- /dev/null +++ b/linker/linker.v @@ -0,0 +1,61 @@ +// linker.v — links one or more VROBJ object files into a VRBIN executable. +// +// Code sections are concatenated, symbol entries are rebased, and call-site +// relocations are patched with the final code offsets. Duplicate or unresolved +// symbols are link errors. +module linker + +import obj + +pub fn link(paths []string, out string) ! { + if paths.len == 0 { + return error('no object files to link') + } + mut code := []u8{} + mut strings := []string{} + mut symbols := map[string]int{} + mut relocs := []obj.Reloc{} + for p in paths { + o := obj.read(p)! + base := code.len + for s in o.symbols { + if s.name in symbols { + return error('duplicate symbol "${s.name}" (in ${p})') + } + symbols[s.name] = base + s.entry + } + code << o.code + for r in o.relocs { + relocs << obj.Reloc{ offset: r.offset + u32(base), name: r.name, kind: r.kind } + } + } + // resolve relocations + for r in relocs { + if r.kind == 1 { + // string constant: rebase to its index in the merged table + idx := intern_str(mut strings, r.name) + obj.patch_i64(mut code, r.offset, i64(idx)) + continue + } + entry := symbols[r.name] or { + return error('unresolved symbol "${r.name}" (no such function)') + } + obj.patch_i64(mut code, r.offset, i64(entry)) + } + // build the executable + mut fns := []obj.BinFn{} + for name, entry in symbols { + fns << obj.BinFn{ name: name, entry: entry } + } + obj.write_bin(out, obj.Bin{ fns: fns, strings: strings, code: code })! +} + +fn intern_str(mut table []string, s string) int { + for i, t in table { + if t == s { + return i + } + } + table << s + return table.len - 1 +} diff --git a/main.v b/main.v index a0becec..61b209d 100644 --- a/main.v +++ b/main.v @@ -1,46 +1,487 @@ // vuurraaf/v // A complete toolchain for VuurRaaf, in V. This toolchain is made from zero, -// and includes custom compiler, assembler, linker, and runtime. -// It is designed to be simple and easy to understand, while still being powerful +// and includes a custom compiler, assembler, linker, and runtime. +// It is designed to be simple and easy to understand, while still being powerful // enough to compile and run VuurRaaf programs. +// +// vr run hello.vr compile + link + run +// vr compile hello.vr source -> object (.vobj) +// vr assemble math.vasm assembly -> object (.vobj) +// vr link a.vobj b.vobj objects -> executable (.vbin) +// vr debug hello.vr run with an instruction trace +// vr test tests.vr run every test_* function +// vr bench fib.vr 1000 benchmark main() +// vr help everything else module main import os -import v.vmod +import json2 +import time +import obj +import compiler +import assembler +import linker +import vm -fn toolchain_loader() {} +const name = 'vuurraaf/v' +const version = '0.1.0' +const project_root = @VMODROOT -fn toolchain_unloader() {} +fn main() { + args := os.args[1..] + if args.len == 0 { + toolchain_help() + return + } + cmd := args[0] + rest := args[1..] + match cmd { + 'help', '-h', '--help' { + toolchain_help() + } + 'version', '-v', '--version' { + toolchain_version() + } + 'info' { + toolchain_info() + } + 'config' { + toolchain_config(rest) or { die('config', err) } + } + 'compile', 'c' { + toolchain_compile(rest) or { die('compile', err) } + } + 'assemble', 'a' { + toolchain_assemble(rest) or { die('assemble', err) } + } + 'link', 'l' { + toolchain_link(rest) or { die('link', err) } + } + 'run', 'r' { + toolchain_run(rest) or { die('run', err) } + } + 'debug', 'd' { + toolchain_debug(rest) or { die('debug', err) } + } + 'test', 't' { + toolchain_test(rest) or { die('test', err) } + } + 'bench', 'b' { + toolchain_bench(rest) or { die('bench', err) } + } + 'clean' { + toolchain_clean() + } + 'up' { + toolchain_up() or { die('up', err) } + } + 'symlink' { + toolchain_symlink() or { die('symlink', err) } + } + 'loader' { + toolchain_loader() + } + 'alloc' { + toolchain_alloc() + } + 'free' { + toolchain_free() + } + 'unloader' { + toolchain_unloader() + } + else { + eprintln('vr: unknown command "${cmd}"') + eprintln("run 'vr help' for usage") + exit(1) + } + } +} -fn toolchain_alloc() {} +fn die(cmd string, err IError) { + eprintln('vr ${cmd}: ${err.msg()}') + exit(1) +} -fn toolchain_free() {} +// --------------------------------------------------------------------------- +// commands -fn toolchain_compile() {} +fn toolchain_loader() { + components := [ + 'compiler (lexer, parser, bytecode codegen)', + 'assembler (vasm -> vobj)', + 'linker (vobj set -> vbin)', + 'vm (stack-based runtime)', + 'obj (VROBJ/VRBIN formats)', + ] + println('VuurRaaf toolchain components:') + for c in components { + println(' [loaded] ${c}') + } + println('') + println('all components are statically linked into this binary (${os.executable()})') +} -fn toolchain_assemble() {} +fn toolchain_unloader() { + println('unloader: nothing to unload — the toolchain is a single static binary') +} -fn toolchain_link() {} +fn toolchain_alloc() { + // the toolchain keeps one pre-sized arena for scratch work + arena := 1 * 1024 * 1024 + vm_stack := 65536 * 8 + println('toolchain memory arena:') + println(' byte arena: ${arena} bytes (1 MiB, reserved)') + println(' vm stack: ${vm_stack} bytes (64k slots x 8)') + println(' strings: grow-on-demand heap inside the vm') +} -fn toolchain_run() {} +fn toolchain_free() { + println('free: no persistent allocations to release') +} -fn toolchain_debug() {} +fn toolchain_help() { + println('VuurRaaf toolchain (vuurraaf/v) v${version}') + println('') + println('usage: vr [args]') + println('') + println(' compile [-o out.vobj] compile source to an object file') + println(' assemble [-o out.vobj] assemble raw bytecode to an object file') + println(' link [more.vobj ...] [-o out] link objects into an executable') + println(' run compile, link and run (or run a binary)') + println(' debug run with an instruction trace') + println(' test run every test_* function') + println(' bench [iterations] benchmark main()') + println(' clean remove build artifacts') + println(' up rebuild the vr binary into bin/') + println(' symlink link bin/vr into your PATH') + println(' config [set ] show or change toolchain config') + println(' info show toolchain information') + println(' loader list toolchain components') + println(' alloc show toolchain memory arena') + println(' free | unloader release toolchain resources') + println(' version print version') + println(' help this help') +} -fn toolchain_help() {} +fn toolchain_version() { + println('VuurRaaf toolchain v${version}') +} -fn toolchain_version() {} +fn toolchain_info() { + v_version := os.execute('v version').output.trim_space() + println('${name} v${version}') + println(' root: ${project_root}') + println(' v compiler: ${v_version}') + println(' platform: ${os.user_os()}') + println(' components: compiler, assembler, linker, vm, obj') + println(' config: ${config_path()}') +} -fn toolchain_info() {} +// --------------------------------------------------------------------------- +// config -fn toolchain_config() {} +struct Config { +mut: + outdir string = '.' + verbose bool +} -fn toolchain_test() {} +fn config_path() string { + return os.join_path(os.home_dir(), '.config', 'vuurraaf', 'config.json') +} -fn toolchain_bench() {} +fn load_config() Config { + p := config_path() + if !os.exists(p) { + return Config{} + } + text := os.read_file(p) or { return Config{} } + return json2.decode[Config](text) or { Config{} } +} -fn toolchain_clean() {} +fn save_config(c Config) ! { + dir := os.dir(config_path()) + os.mkdir_all(dir) or { return error('cannot create config dir: ${dir}') } + os.write_file(config_path(), json2.encode(c))! +} -fn toolchain_up() {} +fn toolchain_config(args []string) ! { + if args.len == 0 { + c := load_config() + println('config: ${config_path()}') + println(' outdir: ${c.outdir}') + println(' verbose: ${c.verbose}') + return + } + if args[0] == 'set' && args.len == 3 { + mut c := load_config() + match args[1] { + 'outdir' { + c.outdir = args[2] + } + 'verbose' { + c.verbose = args[2] == 'true' + } + else { + return error('unknown config key "${args[1]}" (known: outdir, verbose)') + } + } + save_config(c)! + println('config updated: ${args[1]} = ${args[2]}') + return + } + return error('usage: vr config [set ]') +} -fn toolchain_symlink() {} \ No newline at end of file +// --------------------------------------------------------------------------- +// compile / assemble / link / run / debug / test / bench + +fn toolchain_compile(args []string) ! { + mut src := '' + mut out := '' + mut i := 0 + for i < args.len { + if args[i] == '-o' && i + 1 < args.len { + out = args[i + 1] + i += 2 + } else { + src = args[i] + i++ + } + } + if src == '' { + return error('usage: vr compile [-o out.vobj]') + } + o := compiler.compile_file(src)! + if out == '' { + out = src.all_before_last('.') + '.vobj' + } + obj.write(out, o)! + println('compiled ${src} -> ${out} (${o.code.len} bytes code, ${o.symbols.len} symbols, ${o.relocs.len} relocations)') +} + +fn toolchain_assemble(args []string) ! { + mut src := '' + mut out := '' + mut i := 0 + for i < args.len { + if args[i] == '-o' && i + 1 < args.len { + out = args[i + 1] + i += 2 + } else { + src = args[i] + i++ + } + } + if src == '' { + return error('usage: vr assemble [-o out.vobj]') + } + o := assembler.assemble_file(src)! + if out == '' { + out = src.all_before_last('.') + '.vobj' + } + obj.write(out, o)! + println('assembled ${src} -> ${out} (${o.code.len} bytes code, ${o.symbols.len} symbols, ${o.relocs.len} relocations)') +} + +fn toolchain_link(args []string) ! { + mut objs := []string{} + mut out := '' + mut i := 0 + for i < args.len { + if args[i] == '-o' && i + 1 < args.len { + out = args[i + 1] + i += 2 + } else { + objs << args[i] + i++ + } + } + if objs.len == 0 { + return error('usage: vr link [more.vobj ...] [-o out.vbin]') + } + if out == '' { + out = os.base(objs[0]).all_before_last('.') + '.vbin' + } + linker.link(objs, out)! + println('linked ${objs.len} object file(s) -> ${out}') +} + +fn toolchain_run(args []string) ! { + if args.len == 0 { + return error('usage: vr run ') + } + f := args[0] + if f.ends_with('.vbin') { + bin := obj.read_bin(f)! + vm.run(bin, 'main', false)! + return + } + if f.ends_with('.vr') { + run_src(f, 'main', false)! + return + } + return error('unsupported file type: ${f} (expected .vr or .vbin)') +} + +fn toolchain_debug(args []string) ! { + if args.len == 0 { + return error('usage: vr debug ') + } + f := args[0] + println('debug: tracing execution of ${f}') + if f.ends_with('.vbin') { + bin := obj.read_bin(f)! + vm.run(bin, 'main', true)! + return + } + if f.ends_with('.vr') { + run_src(f, 'main', true)! + return + } + return error('unsupported file type: ${f} (expected .vr or .vbin)') +} + +fn toolchain_test(args []string) ! { + if args.len == 0 { + return error('usage: vr test ') + } + src := args[0] + o := compiler.compile_file(src)! + mut tests := []string{} + for s in o.symbols { + if s.name.starts_with('test_') { + tests << s.name + } + } + if tests.len == 0 { + println('no test_* functions found in ${src}') + return + } + tmp_obj := os.join_path(os.temp_dir(), 'vr_${os.getpid()}.vobj') + tmp_bin := os.join_path(os.temp_dir(), 'vr_${os.getpid()}.vbin') + defer { + os.rm(tmp_obj) or {} + os.rm(tmp_bin) or {} + } + obj.write(tmp_obj, o)! + linker.link([tmp_obj], tmp_bin)! + bin := obj.read_bin(tmp_bin)! + mut passes := 0 + mut fails := 0 + for t in tests { + if run_test(bin, t) { + println(' PASS ${t}') + passes++ + } else { + fails++ + } + } + println('') + println('${passes} passed, ${fails} failed (${tests.len} total)') + if fails > 0 { + exit(1) + } +} + +fn run_test(bin obj.Bin, name string) bool { + vm.run(bin, name, false) or { + eprintln(' FAIL ${name} — ${err}') + return false + } + return true +} + +fn toolchain_bench(args []string) ! { + if args.len == 0 { + return error('usage: vr bench [iterations]') + } + src := args[0] + mut n := 1000 + if args.len > 1 { + n = args[1].int() + } + tmp_obj := os.join_path(os.temp_dir(), 'vr_${os.getpid()}.vobj') + tmp_bin := os.join_path(os.temp_dir(), 'vr_${os.getpid()}.vbin') + defer { + os.rm(tmp_obj) or {} + os.rm(tmp_bin) or {} + } + o := compiler.compile_file(src)! + obj.write(tmp_obj, o)! + linker.link([tmp_obj], tmp_bin)! + bin := obj.read_bin(tmp_bin)! + start := time.now().unix_milli() + for _ in 0..n { + vm.run(bin, 'main', false)! + } + ms := time.now().unix_milli() - start + rate := if ms > 0 { f64(n) / (f64(ms) / 1000.0) } else { f64(0) } + println('bench: ${n} runs of main() in ${ms}ms (${rate:.0} runs/s)') +} + +fn run_src(src string, entry string, trace bool) ! { + tmp_obj := os.join_path(os.temp_dir(), 'vr_${os.getpid()}.vobj') + tmp_bin := os.join_path(os.temp_dir(), 'vr_${os.getpid()}.vbin') + defer { + os.rm(tmp_obj) or {} + os.rm(tmp_bin) or {} + } + o := compiler.compile_file(src)! + obj.write(tmp_obj, o)! + linker.link([tmp_obj], tmp_bin)! + bin := obj.read_bin(tmp_bin)! + vm.run(bin, entry, trace)! +} + +// --------------------------------------------------------------------------- +// housekeeping + +fn toolchain_clean() { + mut n := 0 + if files := os.ls('.') { + for f in files { + if f.ends_with('.vobj') || f.ends_with('.vbin') { + os.rm(f) or {} + n++ + } + } + } + println('cleaned ${n} artifact(s) (the vr binary in bin/ is left alone; use "vr up" to rebuild it)') +} + +fn toolchain_up() ! { + vcmd := os.find_abs_path_of_executable('v') or { 'v' } + out := os.join_path(project_root, 'bin', 'vr') + // build from the project root with `.`, not an explicit file/path: + // the latter makes V auto-select tcc without the boehm GC, a combination + // that miscompiles this codebase (verified: `v -o out .` is the safe one) + res := os.execute('cd "${project_root}" && ${vcmd} -o "${out}" .') + if res.exit_code != 0 { + return error('build failed:\n${res.output}') + } + println('rebuilt: ${out}') +} + +fn toolchain_symlink() ! { + src := os.join_path(project_root, 'bin', 'vr') + if !os.exists(src) { + return error('bin/vr does not exist — run "vr up" first') + } + candidates := [os.join_path(os.home_dir(), '.local', 'bin'), '/usr/local/bin'] + for dir in candidates { + if !os.exists(dir) { + continue + } + link_path := os.join_path(dir, 'vr') + if os.exists(link_path) { + os.rm(link_path) or {} + } + os.symlink(src, link_path) or { continue } + println('linked ${src} -> ${link_path}') + return + } + return error('no writable bin dir found; symlink ${src} into your PATH manually') +} diff --git a/obj/obj.v b/obj/obj.v new file mode 100644 index 0000000..f8a4e1c --- /dev/null +++ b/obj/obj.v @@ -0,0 +1,233 @@ +// obj.v — the VuurRaaf object file format (VROBJ) and executable format (VRBIN). +// +// VROBJ is the linker input: code, exported symbols, string constants, and +// relocations for call sites that the linker must resolve. +// VRBIN is the final executable consumed by the runtime VM. +// +// Both formats are little-endian and self-describing: +// +// VROBJ: "VROBJ" ver u32 nsym { u32 name_len name i64 entry }* +// u32 nstr { u32 len bytes }* u32 ncode code u32 nreloc { u32 off u32 name_len name u8 kind }* +// VRBIN: "VRBIN" ver u32 nfn { u32 name_len name i64 entry }* +// u32 nstr { u32 len bytes }* u32 ncode code +module obj + +import os + +pub const magic = 'VROBJ' +pub const bin_magic = 'VRBIN' + +pub struct Symbol { +pub mut: + name string + entry int // code offset of the function entry +} + +pub struct Reloc { +pub mut: + offset u32 // byte offset of the 8-byte operand inside `code` + name string // kind 0: symbol name; kind 1: the string constant itself + kind u8 // 0 = call site, 1 = string constant reference +} + +pub struct Obj { +pub mut: + symbols []Symbol + strings []string + code []u8 + relocs []Reloc +} + +pub struct BinFn { +pub mut: + name string + entry int +} + +pub struct Bin { +pub mut: + fns []BinFn + strings []string + code []u8 +} + +// --------------------------------------------------------------------------- +// encoding helpers + +pub fn encode_u32(v u32) []u8 { + return [u8(v & 0xff), u8((v >> 8) & 0xff), u8((v >> 16) & 0xff), u8((v >> 24) & 0xff)] +} + +pub fn encode_i64(v i64) []u8 { + mut b := []u8{} + for i in 0..8 { + b << u8((v >> (8 * i)) & 0xff) + } + return b +} + +// patch_i64 writes a little-endian i64 over `code[off..off+8]`. +pub fn patch_i64(mut code []u8, off u32, v i64) { + for i in 0..8 { + code[off + u32(i)] = u8((v >> (8 * i)) & 0xff) + } +} + +// --------------------------------------------------------------------------- +// reading + +struct Reader { +mut: + b []u8 + pos int +} + +fn (mut r Reader) u8_() !u8 { + if r.pos >= r.b.len { + return error('object file truncated') + } + b := r.b[r.pos] + r.pos++ + return b +} + +fn (mut r Reader) u32_() !u32 { + mut v := u32(0) + for i in 0..4 { + b := r.u8_()! + v |= u32(b) << u32(8 * i) + } + return v +} + +fn (mut r Reader) i64_() !i64 { + mut v := i64(0) + for i in 0..8 { + b := r.u8_()! + v |= i64(u64(b) << u32(8 * i)) + } + return v +} + +fn (mut r Reader) read_str() !string { + n := int(r.u32_()!) + if r.pos + n > r.b.len { + return error('object file truncated') + } + s := r.b[r.pos..r.pos + n].bytestr() + r.pos += n + return s +} + +// --------------------------------------------------------------------------- +// VROBJ + +pub fn write(path string, o Obj) ! { + mut b := []u8{} + b << magic.bytes() + b << u8(1) // format version + b << encode_u32(u32(o.symbols.len)) + for s in o.symbols { + b << encode_u32(u32(s.name.len)) + b << s.name.bytes() + b << encode_i64(i64(s.entry)) + } + b << encode_u32(u32(o.strings.len)) + for s in o.strings { + b << encode_u32(u32(s.len)) + b << s.bytes() + } + b << encode_u32(u32(o.code.len)) + b << o.code + b << encode_u32(u32(o.relocs.len)) + for r in o.relocs { + b << encode_u32(r.offset) + b << encode_u32(u32(r.name.len)) + b << r.name.bytes() + b << r.kind + } + os.write_bytes(path, b)! +} + +pub fn read(path string) !Obj { + b := os.read_bytes(path)! + if b.len < magic.len || b[0..magic.len].bytestr() != magic { + return error('not a VROBJ file: ${path}') + } + mut r := Reader{ b: b, pos: magic.len } + _ := r.u8_()! // version + mut o := Obj{} + nsym := int(r.u32_()!) + for _ in 0..nsym { + name := r.read_str()! + entry := int(r.i64_()!) + o.symbols << Symbol{ name: name, entry: entry } + } + nstr := int(r.u32_()!) + for _ in 0..nstr { + o.strings << r.read_str()! + } + ncode := int(r.u32_()!) + if r.pos + ncode > b.len { + return error('object file truncated') + } + o.code = b[r.pos..r.pos + ncode] + r.pos += ncode + nrel := int(r.u32_()!) + for _ in 0..nrel { + off := r.u32_()! + name := r.read_str()! + kind := r.u8_()! + o.relocs << Reloc{ offset: off, name: name, kind: kind } + } + return o +} + +// --------------------------------------------------------------------------- +// VRBIN + +pub fn write_bin(path string, bin Bin) ! { + mut b := []u8{} + b << bin_magic.bytes() + b << u8(1) // format version + b << encode_u32(u32(bin.fns.len)) + for f in bin.fns { + b << encode_u32(u32(f.name.len)) + b << f.name.bytes() + b << encode_i64(i64(f.entry)) + } + b << encode_u32(u32(bin.strings.len)) + for s in bin.strings { + b << encode_u32(u32(s.len)) + b << s.bytes() + } + b << encode_u32(u32(bin.code.len)) + b << bin.code + os.write_bytes(path, b)! +} + +pub fn read_bin(path string) !Bin { + b := os.read_bytes(path)! + if b.len < bin_magic.len || b[0..bin_magic.len].bytestr() != bin_magic { + return error('not a VRBIN file: ${path}') + } + mut r := Reader{ b: b, pos: bin_magic.len } + _ := r.u8_()! // version + mut bin := Bin{} + nfn := int(r.u32_()!) + for _ in 0..nfn { + name := r.read_str()! + entry := int(r.i64_()!) + bin.fns << BinFn{ name: name, entry: entry } + } + nstr := int(r.u32_()!) + for _ in 0..nstr { + bin.strings << r.read_str()! + } + ncode := int(r.u32_()!) + if r.pos + ncode > b.len { + return error('executable file truncated') + } + bin.code = b[r.pos..r.pos + ncode] + return bin +} diff --git a/v.mod b/v.mod new file mode 100644 index 0000000..f1ac3e3 --- /dev/null +++ b/v.mod @@ -0,0 +1,7 @@ +Module { + name: 'vuurraaf' + description: 'A complete toolchain for VuurRaaf, written in V: compiler, assembler, linker, and runtime.' + version: '0.1.0' + license: 'MIT' + author: 'allexanderbergmns' +} diff --git a/vm/vm.v b/vm/vm.v new file mode 100644 index 0000000..a8d10ec --- /dev/null +++ b/vm/vm.v @@ -0,0 +1,523 @@ +// vm.v — the VuurRaaf runtime: a small stack-based virtual machine. +// +// Stack values are 64-bit tagged integers: odd values are string handles into +// the runtime string heap (handle = value >> 1); even values are encoded +// numbers (value = raw << 1). Encoding numbers as even values means no +// integer ever collides with a string handle. +// +// Call convention: CALL pushes a frame (retaddr, old bp, argc) and copies the +// arguments into the callee's local slots; the callee reserves extra locals +// with `enter n` and cleans up with `ret`/`retv`. +module vm + +import obj + +// opcodes — keep in sync with the compiler, assembler, and this interpreter +const op_halt = u8(0) +const op_push_i = u8(1) +const op_push_s = u8(2) +const op_load = u8(3) +const op_store = u8(4) +const op_pop = u8(5) +const op_dup = u8(6) +const op_add = u8(7) +const op_sub = u8(8) +const op_mul = u8(9) +const op_div = u8(10) +const op_mod = u8(11) +const op_neg = u8(12) +const op_eq = u8(13) +const op_ne = u8(14) +const op_lt = u8(15) +const op_le = u8(16) +const op_gt = u8(17) +const op_ge = u8(18) +const op_and = u8(19) +const op_or = u8(20) +const op_not = u8(21) +const op_jmp = u8(22) +const op_jz = u8(23) +const op_jnz = u8(24) +const op_call = u8(25) +const op_ret = u8(26) +const op_retv = u8(27) +const op_print = u8(28) +const op_println = u8(29) +const op_assert = u8(30) +const op_enter = u8(31) + +const stack_cap = 65536 + +struct Vm { +mut: + code []u8 + strings []string + stack []i64 + sp int + bp int + ip int + trace bool + halted bool +} + +// run executes the function named `entry` from the executable `bin` and +// returns its return value (0 if it never returns one). +pub fn run(bin obj.Bin, entry string, trace bool) !i64 { + mut v := Vm{ + code: bin.code + strings: bin.strings.clone() + stack: []i64{len: stack_cap} + trace: trace + } + mut entry_ip := -1 + for f in bin.fns { + if f.name == entry { + entry_ip = f.entry + break + } + } + if entry_ip < 0 { + names := bin.fns.map(fn (f obj.BinFn) string { + return f.name + }) + return error('no function "${entry}" in program (available: ${names.join(', ')})') + } + // synthetic frame: retaddr = -1 (halt sentinel), old bp = 0, argc = 0 + v.stack[v.sp] = v.enc_int(-1) + v.sp++ + v.stack[v.sp] = v.enc_int(0) + v.sp++ + v.stack[v.sp] = v.enc_int(0) + v.sp++ + v.bp = v.sp + v.ip = entry_ip + v.exec()! + if v.sp > 0 { + return v.dec_int(v.stack[0]) + } + return 0 +} + +fn (mut v Vm) exec() ! { + for !v.halted { + op := v.code[v.ip] + if v.trace { + v.trace_op(op) + } + match op { + op_halt { + v.halted = true + } + op_push_i { + v.ip++ + v.push(v.enc_int(v.read_i64()))! + } + op_push_s { + v.ip++ + idx := int(v.read_i64()) + v.push(v.mkstr(idx))! + } + op_load { + v.ip++ + idx := int(v.read_i64()) + v.push(v.stack[v.bp + idx])! + } + op_store { + v.ip++ + idx := int(v.read_i64()) + v.stack[v.bp + idx] = v.pop()! + } + op_pop { + v.ip++ + v.pop()! + } + op_dup { + v.ip++ + a := v.pop()! + v.push(a)! + v.push(a)! + } + op_add { + v.ip++ + b := v.pop()! + a := v.pop()! + v.push(v.add(a, b)!)! + } + op_sub { + v.ip++ + b := v.pop()! + a := v.pop()! + v.push(v.arith(a, b, '-')!)! + } + op_mul { + v.ip++ + b := v.pop()! + a := v.pop()! + v.push(v.arith(a, b, '*')!)! + } + op_div { + v.ip++ + b := v.pop()! + a := v.pop()! + v.push(v.arith(a, b, '/')!)! + } + op_mod { + v.ip++ + b := v.pop()! + a := v.pop()! + v.push(v.arith(a, b, '%')!)! + } + op_neg { + v.ip++ + a := v.pop()! + if v.is_str(a) { + return error('cannot negate a string') + } + v.push(v.enc_int(-v.dec_int(a)))! + } + op_eq { + v.ip++ + b := v.pop()! + a := v.pop()! + v.push(v.enc_int(v.cmp(a, b, '==')!))! + } + op_ne { + v.ip++ + b := v.pop()! + a := v.pop()! + v.push(v.enc_int(v.cmp(a, b, '!=')!))! + } + op_lt { + v.ip++ + b := v.pop()! + a := v.pop()! + v.push(v.enc_int(v.cmp(a, b, '<')!))! + } + op_le { + v.ip++ + b := v.pop()! + a := v.pop()! + v.push(v.enc_int(v.cmp(a, b, '<=')!))! + } + op_gt { + v.ip++ + b := v.pop()! + a := v.pop()! + v.push(v.enc_int(v.cmp(a, b, '>')!))! + } + op_ge { + v.ip++ + b := v.pop()! + a := v.pop()! + v.push(v.enc_int(v.cmp(a, b, '>=')!))! + } + op_and { + v.ip++ + b := v.pop()! + a := v.pop()! + v.push(v.enc_int(bool_i64(v.truthy(a) && v.truthy(b))))! + } + op_or { + v.ip++ + b := v.pop()! + a := v.pop()! + v.push(v.enc_int(bool_i64(v.truthy(a) || v.truthy(b))))! + } + op_not { + v.ip++ + a := v.pop()! + v.push(v.enc_int(bool_i64(!v.truthy(a))))! + } + op_jmp { + v.ip++ + v.ip = int(v.read_i64()) + } + op_jz { + v.ip++ + target := int(v.read_i64()) + if !v.truthy(v.pop()!) { + v.ip = target + } + } + op_jnz { + v.ip++ + target := int(v.read_i64()) + if v.truthy(v.pop()!) { + v.ip = target + } + } + op_call { + v.ip++ + target := int(v.read_i64()) + argc := int(v.read_i64()) + v.call(target, argc) + } + op_ret { + v.ret(false)! + } + op_retv { + v.ret(true)! + } + op_print { + v.ip++ + v.print_val(v.pop()!) + } + op_println { + v.ip++ + v.print_val(v.pop()!) + println('') + } + op_assert { + v.ip++ + if !v.truthy(v.pop()!) { + return error('assertion failed (ip ${v.ip})') + } + } + op_enter { + v.ip++ + n := int(v.read_i64()) + for _ in 0..n { + v.push(0)! + } + } + else { + return error('unknown opcode ${op} at ip ${v.ip}') + } + } + } +} + +fn (mut v Vm) read_i64() i64 { + mut val := u64(0) + for i in 0..8 { + val |= u64(v.code[v.ip + i]) << u32(8 * i) + } + v.ip += 8 + return i64(val) +} + +fn (mut v Vm) push(x i64) ! { + if v.sp >= v.stack.len { + return error('stack overflow') + } + v.stack[v.sp] = x + v.sp++ +} + +fn (mut v Vm) pop() !i64 { + if v.sp <= 0 { + return error('stack underflow') + } + v.sp-- + return v.stack[v.sp] +} + +fn (mut v Vm) call(target int, argc int) { + v.stack[v.sp] = v.enc_int(i64(v.ip)) // return address (ip already past both operands) + v.sp++ + v.stack[v.sp] = v.enc_int(i64(v.bp)) + v.sp++ + v.stack[v.sp] = v.enc_int(i64(argc)) + v.sp++ + v.bp = v.sp + // copy the arguments below the frame into local slots 0..argc-1 + for i in 0..argc { + v.stack[v.bp + i] = v.stack[v.bp - 3 - argc + i] + } + v.sp = v.bp + argc + v.ip = target +} + +fn (mut v Vm) ret(with_val bool) ! { + retval := if with_val { v.pop()! } else { v.enc_int(0) } + v.sp = v.bp - 1 + argc := int(v.dec_int(v.stack[v.sp])) + v.sp = v.bp - 2 + old_bp := int(v.dec_int(v.stack[v.sp])) + v.sp = v.bp - 3 + ip := int(v.dec_int(v.stack[v.sp])) + v.sp -= argc + v.bp = old_bp + if ip == -1 { + // returned to the synthetic frame: we are done + v.halted = true + v.push(retval)! + return + } + v.ip = ip + v.push(retval)! +} + +fn (mut v Vm) is_str(x i64) bool { + return x & 1 == 1 +} + +fn (mut v Vm) enc_int(x i64) i64 { + return x << 1 +} + +fn (mut v Vm) dec_int(x i64) i64 { + return x >> 1 +} + +fn (mut v Vm) hand(x i64) int { + return int(x >> 1) +} + +fn (mut v Vm) mkstr(idx int) i64 { + return (i64(idx) << 1) | 1 +} + +fn (mut v Vm) truthy(x i64) bool { + return x != 0 +} + +fn bool_i64(b bool) i64 { + return if b { i64(1) } else { i64(0) } +} + +fn (mut v Vm) add(a i64, b i64) !i64 { + if v.is_str(a) && v.is_str(b) { + return v.alloc_str(v.strings[v.hand(a)] + v.strings[v.hand(b)]) + } + if v.is_str(a) { + return v.alloc_str(v.strings[v.hand(a)] + v.num_str(b)) + } + if v.is_str(b) { + return v.alloc_str(v.num_str(a) + v.strings[v.hand(b)]) + } + return v.enc_int(v.dec_int(a) + v.dec_int(b)) +} + +fn (mut v Vm) alloc_str(s string) i64 { + v.strings << s + return v.mkstr(v.strings.len - 1) +} + +fn (mut v Vm) num_str(x i64) string { + return v.dec_int(x).str() +} + +fn (mut v Vm) arith(a i64, b i64, op string) !i64 { + if v.is_str(a) || v.is_str(b) { + return error('cannot use strings with "${op}"') + } + x := v.dec_int(a) + y := v.dec_int(b) + match op { + '-' { + return v.enc_int(x - y) + } + '*' { + return v.enc_int(x * y) + } + '/' { + if y == 0 { + return error('division by zero') + } + return v.enc_int(x / y) + } + '%' { + if y == 0 { + return error('division by zero') + } + return v.enc_int(x % y) + } + else { + return error('internal: bad arith op "${op}"') + } + } +} + +fn (mut v Vm) cmp(a i64, b i64, op string) !i64 { + if v.is_str(a) && v.is_str(b) { + sa := v.strings[v.hand(a)] + sb := v.strings[v.hand(b)] + return bool_i64(match op { + '==' { sa == sb } + '!=' { sa != sb } + '<' { sa < sb } + '<=' { sa <= sb } + '>' { sa > sb } + '>=' { sa >= sb } + else { return error('internal: bad cmp op "${op}"') } + }) + } + if v.is_str(a) || v.is_str(b) { + return error('cannot compare a string and a number') + } + x := v.dec_int(a) + y := v.dec_int(b) + return bool_i64(match op { + '==' { x == y } + '!=' { x != y } + '<' { x < y } + '<=' { x <= y } + '>' { x > y } + '>=' { x >= y } + else { return error('internal: bad cmp op "${op}"') } + }) +} + +fn (mut v Vm) print_val(x i64) { + if v.is_str(x) && v.valid_handle(x) { + print(v.strings[v.hand(x)]) + } else { + print(v.dec_int(x)) + } +} + +fn (mut v Vm) valid_handle(x i64) bool { + h := v.hand(x) + return h >= 0 && h < v.strings.len +} + +fn (mut v Vm) trace_op(op u8) { + name := match op { + op_halt { 'halt' } + op_push_i { 'push_int' } + op_push_s { 'push_str' } + op_load { 'load' } + op_store { 'store' } + op_pop { 'pop' } + op_dup { 'dup' } + op_add { 'add' } + op_sub { 'sub' } + op_mul { 'mul' } + op_div { 'div' } + op_mod { 'mod' } + op_neg { 'neg' } + op_eq { 'eq' } + op_ne { 'ne' } + op_lt { 'lt' } + op_le { 'le' } + op_gt { 'gt' } + op_ge { 'ge' } + op_and { 'and' } + op_or { 'or' } + op_not { 'not' } + op_jmp { 'jmp' } + op_jz { 'jz' } + op_jnz { 'jnz' } + op_call { 'call' } + op_ret { 'ret' } + op_retv { 'retv' } + op_print { 'print' } + op_println { 'println' } + op_assert { 'assert' } + op_enter { 'enter' } + else { '??' } + } + mut s := '' + for i in 0..v.sp { + if i > 0 { + s += ' ' + } + if v.is_str(v.stack[i]) && v.valid_handle(v.stack[i]) { + s += '"${v.strings[v.hand(v.stack[i])]}"' + } else { + s += '${v.dec_int(v.stack[i])}' + } + } + println(' [ip=${v.ip:4}] ${name:-9} stack: [${s}]') +}