From b0a4da7e2fb74f91a56c3ccbea0870ed83c90395 Mon Sep 17 00:00:00 2001 From: allexanderbergmns Date: Mon, 24 Aug 2026 20:14:59 +0200 Subject: [PATCH] Code split --- compiler/ast.v | 130 +++++++ compiler/codegen.v | 854 +++++++++++++++++++++++++++++++++++++++++ compiler/compiler.v | 902 +------------------------------------------- compiler/lexer.v | 70 +--- compiler/opcodes.v | 49 +++ compiler/parser.v | 128 ------- compiler/tokens.v | 66 ++++ vm/opcodes.v | 49 +++ vm/ops.v | 111 ++++++ vm/print.v | 110 ++++++ vm/types.v | 34 ++ vm/value.v | 63 ++++ vm/vm.v | 358 ------------------ 13 files changed, 1470 insertions(+), 1454 deletions(-) create mode 100644 compiler/ast.v create mode 100644 compiler/codegen.v create mode 100644 compiler/opcodes.v create mode 100644 compiler/tokens.v create mode 100644 vm/opcodes.v create mode 100644 vm/ops.v create mode 100644 vm/print.v create mode 100644 vm/types.v create mode 100644 vm/value.v diff --git a/compiler/ast.v b/compiler/ast.v new file mode 100644 index 0000000..085080b --- /dev/null +++ b/compiler/ast.v @@ -0,0 +1,130 @@ +// ast.v — abstract syntax tree types for the VuurRaaf language. +module compiler + +pub enum ExprKind { + int_lit + str_lit + bool_lit + ident + array_lit + struct_lit + index + field + method_call + unary + binary + call +} + +// StructField is one `name: value` entry of a struct literal. +pub struct StructField { +pub mut: + name string + val Expr +} + +pub struct Expr { +pub mut: + kind ExprKind + int_v i64 + str_v string + name string // ident/call name, or the field name of a `.field` access + op TokKind + left &Expr = unsafe { nil } + right &Expr = unsafe { nil } + elems []Expr + fields []StructField // struct_lit: the named fields + args []Expr + line int +} + +pub enum StmtKind { + expr_stmt + let_stmt + assign_stmt + index_assign + field_assign + if_stmt + match_stmt + while_stmt + for_range_stmt + for_in_stmt + break_stmt + continue_stmt + ret_stmt + assert_stmt +} + +// MatchArm is a single `value { body }` arm of a match statement. +pub struct MatchArm { +pub mut: + val Expr + body []Stmt +} + +pub struct Stmt { +pub mut: + kind StmtKind + target string + expr Expr + cond Expr + base Expr // index_assign: the indexed expression + idx Expr // index_assign: the index expression + body []Stmt + els []Stmt + arms []MatchArm // match_stmt: the arms (val + body) + has_else bool // match_stmt: a trailing else arm exists + els_body []Stmt // match_stmt: body of the else arm + has_val bool + inclusive bool // for_range_stmt: `..` (false) vs `...` (true) + line int +} + +// StructDecl is a `struct Name { a, b }` declaration. +pub struct StructDecl { +pub mut: + name string + fields []string + line int +} + +pub struct FnDecl { +pub mut: + name string + recv_name string // method receiver local name ('' for plain functions) + recv_type string // method receiver struct type ('' for plain functions) + params []string + body []Stmt + line int +} + +pub struct ImportDecl { +pub mut: + path string + line int +} + +// EnumDecl is an `enum Name { variant1 variant2 ... }` declaration. +pub struct EnumDecl { +pub mut: + name string + variants []string + line int +} + +// ConstDecl is a `const NAME = value` declaration. +pub struct ConstDecl { +pub mut: + name string + value Expr + line int +} + +pub struct Program { +pub mut: + fns []FnDecl + structs []StructDecl + enums []EnumDecl + imports []ImportDecl + consts []ConstDecl +} diff --git a/compiler/codegen.v b/compiler/codegen.v new file mode 100644 index 0000000..6f33786 --- /dev/null +++ b/compiler/codegen.v @@ -0,0 +1,854 @@ +// codegen.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 obj + +struct Fixup { + name string + off u32 +} + +// LoopCtx records where `break` and `continue` should jump while generating +// the body of a loop. For `for` loops `continue` targets the increment, not +// the condition check, so the loop variable still advances. +struct LoopCtx { + break_l string + continue_l string +} + +struct Gen { +mut: + code []u8 + strings []string + str_map map[string]int + symbols []obj.Symbol + relocs []obj.Reloc + locals map[string]int + types map[string]string // local name -> declared struct type ('' = unknown) + structs map[string][]string // declared struct name -> field list + enums map[string][]string // enum name -> variant list + enum_vals map[string]int // 'Enum.variant' -> integer value + consts map[string]i64 // constant name -> integer value + local_cnt int + argc int + cur_fn string + labels map[string]int + fixups []Fixup + loops []LoopCtx + enter_off u32 + next_lbl int +} + +fn gen(prog Program) !obj.Obj { + mut g := Gen{} + // register enums first so their values are available everywhere + for ed in prog.enums { + if ed.name in g.enums { + return error('duplicate enum declaration "${ed.name}"') + } + g.enums[ed.name] = ed.variants + for i, v in ed.variants { + g.enum_vals['${ed.name}.${v}'] = i + } + } + // register constants + for cd in prog.consts { + if cd.name in g.consts { + return error('duplicate constant declaration "${cd.name}"') + } + // constants must be compile-time integer expressions + if cd.value.kind == .int_lit { + g.consts[cd.name] = cd.value.int_v + } else if cd.value.kind == .bool_lit { + g.consts[cd.name] = cd.value.int_v + } else { + return error('constant "${cd.name}" must be an integer or boolean literal (line ${cd.line})') + } + } + // register struct declarations + for sd in prog.structs { + if sd.name in g.structs { + return error('duplicate struct declaration "${sd.name}"') + } + g.structs[sd.name] = sd.fields + } + // compile imported files and merge their objects + for imp in prog.imports { + imported := compile_file(imp.path)! + // merge symbols from the imported object + for s in imported.symbols { + g.symbols << s + } + // merge strings + for s in imported.strings { + g.strings << s + } + // append imported bytecode and adjust relocations + code_off := g.code.len + g.code << imported.code + for r in imported.relocs { + g.relocs << obj.Reloc{ offset: u32(code_off) + r.offset, name: r.name, kind: r.kind } + } + } + 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) ! { + // methods compile to functions named `Type.method`; the receiver is the + // implicit first argument, so `p.dist(x)` becomes `call Point.dist p, x` + sym := if fd.recv_type.len > 0 { '${fd.recv_type}.${fd.name}' } else { fd.name } + g.cur_fn = sym + g.symbols << obj.Symbol{ name: sym, entry: g.code.len } + g.locals.clear() + g.types.clear() + g.local_cnt = 0 + g.argc = fd.params.len + if fd.recv_type.len > 0 { 1 } else { 0 } + mut next := 0 + if fd.recv_type.len > 0 { + g.locals[fd.recv_name] = 0 + g.types[fd.recv_name] = fd.recv_type + next = 1 + } + for i, p in fd.params { + g.locals[p] = i + next + } + 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.types[st.target] = g.expr_type(st.expr) + 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.types[st.target] = g.expr_type(st.expr) + g.code << op_store + g.code << obj.encode_i64(i64(idx)) + } + .index_assign { + // if the index is a string literal, use struct field set (map style) + if st.idx.kind == .str_lit { + g.gen_expr(st.base)! + g.gen_expr(st.expr)! + g.emit_field_name(st.idx.str_v) + g.code << op_sset + } else { + g.gen_expr(st.base)! + g.gen_expr(st.idx)! + g.gen_expr(st.expr)! + g.code << op_aset + } + } + .field_assign { + // a.b = v → a, v, "b" sset (field name on top of the stack) + g.gen_expr(st.base)! + g.gen_expr(st.expr)! + g.emit_field_name(st.target) + g.code << op_sset + } + .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) + } + .match_stmt { + // match x { v1 {..} v2 {..} else {..} } → subject := x; a chain of + // equality tests jumping to the matching arm; else falls through. + subj_idx := g.new_local() + end_l := g.new_label() + g.gen_expr(st.expr)! + g.emit_store(subj_idx) + for i, arm in st.arms { + next_l := g.new_label() + g.emit_load(subj_idx) + g.gen_expr(arm.val)! + g.code << op_eq + g.code << op_jz + g.code << obj.encode_i64(0) + g.fixups << Fixup{ name: next_l, off: u32(g.code.len) - 8 } + for s in arm.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(next_l) + if i == st.arms.len - 1 && !st.has_else { + // no else: fall through to the end label + g.emit_label(end_l) + } + } + if st.has_else { + for s in st.els_body { + 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 } + g.loops << LoopCtx{ break_l: end_l, continue_l: loop_l } + for s in st.body { + g.gen_stmt(s)! + } + g.loops.delete_last() + 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) + } + .for_range_stmt { + // for i in a..b / for i in a...b → i := a; while i <(<=) b { body; i++ } + var_idx := g.new_local() + bound_idx := g.new_local() + loop_l := g.new_label() + inc_l := g.new_label() + end_l := g.new_label() + g.gen_expr(st.expr)! + g.gen_expr(st.cond)! + g.emit_store(bound_idx) + g.emit_store(var_idx) + g.emit_label(loop_l) + g.emit_load(var_idx) + g.emit_load(bound_idx) + g.code << if st.inclusive { op_le } else { op_lt } + g.code << op_jz + g.code << obj.encode_i64(0) + g.fixups << Fixup{ name: end_l, off: u32(g.code.len) - 8 } + g.loops << LoopCtx{ break_l: end_l, continue_l: inc_l } + prev := g.locals[st.target] or { -1 } + prev_t := g.types[st.target] or { '' } + g.locals[st.target] = var_idx + g.types.delete(st.target) + for s in st.body { + g.gen_stmt(s)! + } + if prev >= 0 { + g.locals[st.target] = prev + } else { + g.locals.delete(st.target) + } + if prev_t.len > 0 { + g.types[st.target] = prev_t + } + g.loops.delete_last() + g.emit_label(inc_l) + g.emit_load(var_idx) + g.code << op_push_i + g.code << obj.encode_i64(1) + g.code << op_add + g.emit_store(var_idx) + 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) + } + .for_in_stmt { + // for x in EnumType { ... } → iterate over enum variants as integers + if st.expr.kind == .ident && st.expr.name in g.enums { + g.gen_for_enum(st.target, st.expr.name, st.body, st.line)! + return + } + // for x in arr → idx := 0; while idx < len(arr) { x := arr[idx]; body; idx++ } + arr_idx := g.new_local() + idx_idx := g.new_local() + elem_idx := g.new_local() + loop_l := g.new_label() + inc_l := g.new_label() + end_l := g.new_label() + g.gen_expr(st.expr)! + g.emit_store(arr_idx) + g.code << op_push_i + g.code << obj.encode_i64(0) + g.emit_store(idx_idx) + g.emit_label(loop_l) + g.emit_load(idx_idx) + g.emit_load(arr_idx) + g.code << op_alen + g.code << op_lt + g.code << op_jz + g.code << obj.encode_i64(0) + g.fixups << Fixup{ name: end_l, off: u32(g.code.len) - 8 } + g.loops << LoopCtx{ break_l: end_l, continue_l: inc_l } + g.emit_load(arr_idx) + g.emit_load(idx_idx) + g.code << op_aget + g.emit_store(elem_idx) + prev := g.locals[st.target] or { -1 } + prev_t := g.types[st.target] or { '' } + g.locals[st.target] = elem_idx + g.types.delete(st.target) + for s in st.body { + g.gen_stmt(s)! + } + if prev >= 0 { + g.locals[st.target] = prev + } else { + g.locals.delete(st.target) + } + if prev_t.len > 0 { + g.types[st.target] = prev_t + } + g.loops.delete_last() + g.emit_label(inc_l) + g.emit_load(idx_idx) + g.code << op_push_i + g.code << obj.encode_i64(1) + g.code << op_add + g.emit_store(idx_idx) + 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 + } + .break_stmt { + if g.loops.len == 0 { + return error('break outside of a loop (line ${st.line})') + } + ctx := g.loops[g.loops.len - 1] + g.code << op_jmp + g.code << obj.encode_i64(0) + g.fixups << Fixup{ name: ctx.break_l, off: u32(g.code.len) - 8 } + } + .continue_stmt { + if g.loops.len == 0 { + return error('continue outside of a loop (line ${st.line})') + } + ctx := g.loops[g.loops.len - 1] + g.code << op_jmp + g.code << obj.encode_i64(0) + g.fixups << Fixup{ name: ctx.continue_l, off: u32(g.code.len) - 8 } + } + } +} + +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 } + } + .array_lit { + for el in e.elems { + g.gen_expr(el)! + } + g.code << op_mkarray + g.code << obj.encode_i64(i64(e.elems.len)) + } + .struct_lit { + // typed literals validate their fields against the declaration + // (an undeclared type name is allowed — it may live in another + // file, where the same validation applies) + if e.name.len > 0 && e.name in g.structs { + decl_fields := g.structs[e.name] + mut seen := map[string]bool{} + for f in e.fields { + if f.name !in decl_fields { + return error('unknown field "${f.name}" for struct ${e.name} (line ${e.line})') + } + if f.name in seen { + return error('duplicate field "${f.name}" in struct literal (line ${e.line})') + } + seen[f.name] = true + } + } + // for each field: push the name string then the value; mkstruct n + // pops the (name, value) pairs and builds the record + for f in e.fields { + g.emit_field_name(f.name) + g.gen_expr(f.val)! + } + g.code << op_mkstruct + g.code << obj.encode_i64(i64(e.fields.len)) + } + .field { + // check if it's an enum variant (e.g., Color.red) + if e.left.kind == .ident { + key := '${e.left.name}.${e.name}' + if key in g.enum_vals { + g.code << op_push_i + g.code << obj.encode_i64(i64(g.enum_vals[key])) + return + } + } + g.gen_expr(*e.left)! + g.emit_field_name(e.name) + g.code << op_sget + } + .method_call { + // p.dist(x) → call .dist p, x + recv_t := g.method_receiver_type(e)! + // built-in: enum.to_string() generates a match on the integer value + if e.name == 'to_string' && recv_t in g.enums && e.args.len == 0 { + g.gen_enum_to_string(recv_t, *e.left, e.line)! + return + } + // built-in: enum.count() returns the number of variants + if e.name == 'count' && recv_t in g.enums && e.args.len == 0 { + g.gen_expr(*e.left)! + g.code << op_pop + variants := g.enums[recv_t] + g.code << op_push_i + g.code << obj.encode_i64(i64(variants.len)) + return + } + g.gen_expr(*e.left)! + 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: '${recv_t}.${e.name}', kind: 0 } + g.code << obj.encode_i64(i64(e.args.len + 1)) // receiver + args + } + .index { + // if the index is a string literal, use struct field access (map style) + if e.right.kind == .str_lit { + g.gen_expr(*e.left)! + g.emit_field_name(e.right.str_v) + g.code << op_sget + } else { + g.gen_expr(*e.left)! + g.gen_expr(*e.right)! + g.code << op_aget + } + } + .bool_lit { + g.code << op_push_i + g.code << obj.encode_i64(e.int_v) + } + .ident { + // check if it's a constant + if e.name in g.consts { + g.code << op_push_i + g.code << obj.encode_i64(g.consts[e.name]) + } else if e.name in g.enum_vals { + // check if it's an enum variant (e.g., Color.red) + g.code << op_push_i + g.code << obj.encode_i64(i64(g.enum_vals[e.name])) + } else { + 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 + } + if e.name == 'len' { + if e.args.len != 1 { + return error('len() takes exactly one argument (line ${e.line})') + } + g.gen_expr(e.args[0])! + g.code << op_alen + return + } + if e.name == 'push' { + if e.args.len != 2 { + return error('push() takes exactly two arguments (line ${e.line})') + } + g.gen_expr(e.args[0])! + g.gen_expr(e.args[1])! + g.code << op_apush + return + } + if e.name == 'has' { + if e.args.len != 2 { + return error('has() takes exactly two arguments (line ${e.line})') + } + g.gen_expr(e.args[0])! + g.gen_expr(e.args[1])! + g.code << op_shas + return + } + if e.name == 'delete' { + if e.args.len != 2 { + return error('delete() takes exactly two arguments (line ${e.line})') + } + g.gen_expr(e.args[0])! + g.gen_expr(e.args[1])! + g.code << op_sdel + return + } + if e.name == 'keys' { + if e.args.len != 1 { + return error('keys() takes exactly one argument (line ${e.line})') + } + g.gen_expr(e.args[0])! + g.code << op_skeys + 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 + } + } +} + +// expr_type returns the declared struct type of an expression when it is +// statically knowable: a typed literal `Point{...}`, a copy of a typed +// variable, or an enum variant `Enum.variant`. Everything else has no +// known type (''). +fn (mut g Gen) expr_type(e Expr) string { + if e.kind == .struct_lit { + return e.name + } + if e.kind == .ident { + return g.types[e.name] or { '' } + } + // enum variant: Color.red → type is "Color" + if e.kind == .field && e.left.kind == .ident { + key := '${e.left.name}.${e.name}' + if key in g.enum_vals { + return e.left.name + } + } + return '' +} + +// method_receiver_type resolves the struct type a method call is made on. +// The receiver must be a plain variable whose type the compiler knows +// (from a typed literal, an assignment, or a method receiver binding) +// or an enum variant expression (e.g. Color.red). +fn (mut g Gen) method_receiver_type(e Expr) !string { + recv := e.left + if recv.kind == .ident { + t := g.types[recv.name] or { '' } + if t.len > 0 { + return t + } + } + // enum variant: Color.red → type is "Color" + if recv.kind == .field && recv.left.kind == .ident { + key := '${recv.left.name}.${recv.name}' + if key in g.enum_vals { + return recv.left.name + } + } + return error('cannot resolve method "${e.name}": receiver type unknown (line ${e.line})') +} + +// gen_enum_to_string generates bytecode for `e.to_string()` on an enum value. +// It emits a match statement that maps each integer variant to its string name. +fn (mut g Gen) gen_enum_to_string(enum_name string, recv Expr, line int) ! { + variants := g.enums[enum_name] or { + return error('unknown enum "${enum_name}" at line ${line}') + } + // store the receiver in a temp local + subj_idx := g.new_local() + g.gen_expr(recv)! + g.emit_store(subj_idx) + // end label for the match + end_l := g.new_label() + for i, v in variants { + next_l := g.new_label() + // load subject, push variant integer, compare + g.emit_load(subj_idx) + g.code << op_push_i + g.code << obj.encode_i64(i64(i)) + g.code << op_eq + g.code << op_jz + g.code << obj.encode_i64(0) + g.fixups << Fixup{ name: next_l, off: u32(g.code.len) - 8 } + // push the variant name as a string + g.code << op_push_s + g.code << obj.encode_i64(0) + g.relocs << obj.Reloc{ offset: u32(g.code.len) - 8, name: v, kind: 1 } + // jump to end + 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(next_l) + } + // else: push "unknown" + g.code << op_push_s + g.code << obj.encode_i64(0) + g.relocs << obj.Reloc{ offset: u32(g.code.len) - 8, name: 'unknown', kind: 1 } + g.emit_label(end_l) +} + +// gen_for_enum generates a for loop that iterates over all variants of an enum. +// for x in Color { ... } → for i in 0..count { x = i; ... } (x typed as Color) +fn (mut g Gen) gen_for_enum(var_name string, enum_name string, body []Stmt, line int) ! { + variants := g.enums[enum_name] or { + return error('unknown enum "${enum_name}" at line ${line}') + } + count := variants.len + // i := 0 + var_idx := g.new_local() + bound_idx := g.new_local() + g.code << op_push_i + g.code << obj.encode_i64(0) + g.emit_store(var_idx) + g.code << op_push_i + g.code << obj.encode_i64(i64(count)) + g.emit_store(bound_idx) + loop_l := g.new_label() + inc_l := g.new_label() + end_l := g.new_label() + g.emit_label(loop_l) + g.emit_load(var_idx) + g.emit_load(bound_idx) + g.code << op_lt + g.code << op_jz + g.code << obj.encode_i64(0) + g.fixups << Fixup{ name: end_l, off: u32(g.code.len) - 8 } + g.loops << LoopCtx{ break_l: end_l, continue_l: inc_l } + prev := g.locals[var_name] or { -1 } + prev_t := g.types[var_name] or { '' } + g.locals[var_name] = var_idx + g.types[var_name] = enum_name // type the loop variable as the enum + for s in body { + g.gen_stmt(s)! + } + if prev >= 0 { + g.locals[var_name] = prev + } else { + g.locals.delete(var_name) + } + if prev_t.len > 0 { + g.types[var_name] = prev_t + } + g.loops.delete_last() + g.emit_label(inc_l) + g.emit_load(var_idx) + g.code << op_push_i + g.code << obj.encode_i64(1) + g.code << op_add + g.emit_store(var_idx) + 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) +} + +// emit_field_name pushes a field name as a string constant. Like string +// literals it goes through a kind-1 relocation so multi-file links rebase it. +fn (mut g Gen) emit_field_name(name string) { + g.code << op_push_s + g.code << obj.encode_i64(0) + g.relocs << obj.Reloc{ offset: u32(g.code.len) - 8, name: name, kind: 1 } +} + +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_local() int { + idx := g.local_cnt + g.local_cnt++ + return idx +} + +fn (mut g Gen) emit_load(idx int) { + g.code << op_load + g.code << obj.encode_i64(i64(idx)) +} + +fn (mut g Gen) emit_store(idx int) { + g.code << op_store + g.code << obj.encode_i64(i64(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/compiler.v b/compiler/compiler.v index 6a7c330..429cd70 100644 --- a/compiler/compiler.v +++ b/compiler/compiler.v @@ -1,59 +1,9 @@ -// 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. +// compiler.v — public API for the VuurRaaf compiler. 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) -const op_mkarray = u8(32) -const op_aget = u8(33) -const op_aset = u8(34) -const op_alen = u8(35) -const op_apush = u8(36) -const op_mkstruct = u8(37) -const op_sget = u8(38) -const op_sset = u8(39) -const op_shas = u8(40) -const op_sdel = u8(41) -const op_slen = u8(42) -const op_skeys = u8(43) - // compile parses and compiles VuurRaaf source into an object file. pub fn compile(src string) !obj.Obj { toks := tokenize(src)! @@ -65,853 +15,3 @@ pub fn compile_file(path string) !obj.Obj { src := os.read_file(path)! return compile(src)! } - -// --------------------------------------------------------------------------- - -struct Fixup { - name string - off u32 -} - -// LoopCtx records where `break` and `continue` should jump while generating -// the body of a loop. For `for` loops `continue` targets the increment, not -// the condition check, so the loop variable still advances. -struct LoopCtx { - break_l string - continue_l string -} - -struct Gen { -mut: - code []u8 - strings []string - str_map map[string]int - symbols []obj.Symbol - relocs []obj.Reloc - locals map[string]int - types map[string]string // local name -> declared struct type ('' = unknown) - structs map[string][]string // declared struct name -> field list - enums map[string][]string // enum name -> variant list - enum_vals map[string]int // 'Enum.variant' -> integer value - consts map[string]i64 // constant name -> integer value - local_cnt int - argc int - cur_fn string - labels map[string]int - fixups []Fixup - loops []LoopCtx - enter_off u32 - next_lbl int -} - -fn gen(prog Program) !obj.Obj { - mut g := Gen{} - // register enums first so their values are available everywhere - for ed in prog.enums { - if ed.name in g.enums { - return error('duplicate enum declaration "${ed.name}"') - } - g.enums[ed.name] = ed.variants - for i, v in ed.variants { - g.enum_vals['${ed.name}.${v}'] = i - } - } - // register constants - for cd in prog.consts { - if cd.name in g.consts { - return error('duplicate constant declaration "${cd.name}"') - } - // constants must be compile-time integer expressions - if cd.value.kind == .int_lit { - g.consts[cd.name] = cd.value.int_v - } else if cd.value.kind == .bool_lit { - g.consts[cd.name] = cd.value.int_v - } else { - return error('constant "${cd.name}" must be an integer or boolean literal (line ${cd.line})') - } - } - // register struct declarations - for sd in prog.structs { - if sd.name in g.structs { - return error('duplicate struct declaration "${sd.name}"') - } - g.structs[sd.name] = sd.fields - } - // compile imported files and merge their objects - for imp in prog.imports { - imported := compile_file(imp.path)! - // merge symbols from the imported object - for s in imported.symbols { - g.symbols << s - } - // merge strings - for s in imported.strings { - g.strings << s - } - // append imported bytecode and adjust relocations - code_off := g.code.len - g.code << imported.code - for r in imported.relocs { - g.relocs << obj.Reloc{ offset: u32(code_off) + r.offset, name: r.name, kind: r.kind } - } - } - 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) ! { - // methods compile to functions named `Type.method`; the receiver is the - // implicit first argument, so `p.dist(x)` becomes `call Point.dist p, x` - sym := if fd.recv_type.len > 0 { '${fd.recv_type}.${fd.name}' } else { fd.name } - g.cur_fn = sym - g.symbols << obj.Symbol{ name: sym, entry: g.code.len } - g.locals.clear() - g.types.clear() - g.local_cnt = 0 - g.argc = fd.params.len + if fd.recv_type.len > 0 { 1 } else { 0 } - mut next := 0 - if fd.recv_type.len > 0 { - g.locals[fd.recv_name] = 0 - g.types[fd.recv_name] = fd.recv_type - next = 1 - } - for i, p in fd.params { - g.locals[p] = i + next - } - 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.types[st.target] = g.expr_type(st.expr) - 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.types[st.target] = g.expr_type(st.expr) - g.code << op_store - g.code << obj.encode_i64(i64(idx)) - } - .index_assign { - // if the index is a string literal, use struct field set (map style) - if st.idx.kind == .str_lit { - g.gen_expr(st.base)! - g.gen_expr(st.expr)! - g.emit_field_name(st.idx.str_v) - g.code << op_sset - } else { - g.gen_expr(st.base)! - g.gen_expr(st.idx)! - g.gen_expr(st.expr)! - g.code << op_aset - } - } - .field_assign { - // a.b = v → a, v, "b" sset (field name on top of the stack) - g.gen_expr(st.base)! - g.gen_expr(st.expr)! - g.emit_field_name(st.target) - g.code << op_sset - } - .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) - } - .match_stmt { - // match x { v1 {..} v2 {..} else {..} } → subject := x; a chain of - // equality tests jumping to the matching arm; else falls through. - subj_idx := g.new_local() - end_l := g.new_label() - g.gen_expr(st.expr)! - g.emit_store(subj_idx) - for i, arm in st.arms { - next_l := g.new_label() - g.emit_load(subj_idx) - g.gen_expr(arm.val)! - g.code << op_eq - g.code << op_jz - g.code << obj.encode_i64(0) - g.fixups << Fixup{ name: next_l, off: u32(g.code.len) - 8 } - for s in arm.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(next_l) - if i == st.arms.len - 1 && !st.has_else { - // no else: fall through to the end label - g.emit_label(end_l) - } - } - if st.has_else { - for s in st.els_body { - 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 } - g.loops << LoopCtx{ break_l: end_l, continue_l: loop_l } - for s in st.body { - g.gen_stmt(s)! - } - g.loops.delete_last() - 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) - } - .for_range_stmt { - // for i in a..b / for i in a...b → i := a; while i <(<=) b { body; i++ } - var_idx := g.new_local() - bound_idx := g.new_local() - loop_l := g.new_label() - inc_l := g.new_label() - end_l := g.new_label() - g.gen_expr(st.expr)! - g.gen_expr(st.cond)! - g.emit_store(bound_idx) - g.emit_store(var_idx) - g.emit_label(loop_l) - g.emit_load(var_idx) - g.emit_load(bound_idx) - g.code << if st.inclusive { op_le } else { op_lt } - g.code << op_jz - g.code << obj.encode_i64(0) - g.fixups << Fixup{ name: end_l, off: u32(g.code.len) - 8 } - g.loops << LoopCtx{ break_l: end_l, continue_l: inc_l } - prev := g.locals[st.target] or { -1 } - prev_t := g.types[st.target] or { '' } - g.locals[st.target] = var_idx - g.types.delete(st.target) - for s in st.body { - g.gen_stmt(s)! - } - if prev >= 0 { - g.locals[st.target] = prev - } else { - g.locals.delete(st.target) - } - if prev_t.len > 0 { - g.types[st.target] = prev_t - } - g.loops.delete_last() - g.emit_label(inc_l) - g.emit_load(var_idx) - g.code << op_push_i - g.code << obj.encode_i64(1) - g.code << op_add - g.emit_store(var_idx) - 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) - } - .for_in_stmt { - // for x in EnumType { ... } → iterate over enum variants as integers - if st.expr.kind == .ident && st.expr.name in g.enums { - g.gen_for_enum(st.target, st.expr.name, st.body, st.line)! - return - } - // for x in arr → idx := 0; while idx < len(arr) { x := arr[idx]; body; idx++ } - arr_idx := g.new_local() - idx_idx := g.new_local() - elem_idx := g.new_local() - loop_l := g.new_label() - inc_l := g.new_label() - end_l := g.new_label() - g.gen_expr(st.expr)! - g.emit_store(arr_idx) - g.code << op_push_i - g.code << obj.encode_i64(0) - g.emit_store(idx_idx) - g.emit_label(loop_l) - g.emit_load(idx_idx) - g.emit_load(arr_idx) - g.code << op_alen - g.code << op_lt - g.code << op_jz - g.code << obj.encode_i64(0) - g.fixups << Fixup{ name: end_l, off: u32(g.code.len) - 8 } - g.loops << LoopCtx{ break_l: end_l, continue_l: inc_l } - g.emit_load(arr_idx) - g.emit_load(idx_idx) - g.code << op_aget - g.emit_store(elem_idx) - prev := g.locals[st.target] or { -1 } - prev_t := g.types[st.target] or { '' } - g.locals[st.target] = elem_idx - g.types.delete(st.target) - for s in st.body { - g.gen_stmt(s)! - } - if prev >= 0 { - g.locals[st.target] = prev - } else { - g.locals.delete(st.target) - } - if prev_t.len > 0 { - g.types[st.target] = prev_t - } - g.loops.delete_last() - g.emit_label(inc_l) - g.emit_load(idx_idx) - g.code << op_push_i - g.code << obj.encode_i64(1) - g.code << op_add - g.emit_store(idx_idx) - 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 - } - .break_stmt { - if g.loops.len == 0 { - return error('break outside of a loop (line ${st.line})') - } - ctx := g.loops[g.loops.len - 1] - g.code << op_jmp - g.code << obj.encode_i64(0) - g.fixups << Fixup{ name: ctx.break_l, off: u32(g.code.len) - 8 } - } - .continue_stmt { - if g.loops.len == 0 { - return error('continue outside of a loop (line ${st.line})') - } - ctx := g.loops[g.loops.len - 1] - g.code << op_jmp - g.code << obj.encode_i64(0) - g.fixups << Fixup{ name: ctx.continue_l, off: u32(g.code.len) - 8 } - } - } -} - -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 } - } - .array_lit { - for el in e.elems { - g.gen_expr(el)! - } - g.code << op_mkarray - g.code << obj.encode_i64(i64(e.elems.len)) - } - .struct_lit { - // typed literals validate their fields against the declaration - // (an undeclared type name is allowed — it may live in another - // file, where the same validation applies) - if e.name.len > 0 && e.name in g.structs { - decl_fields := g.structs[e.name] - mut seen := map[string]bool{} - for f in e.fields { - if f.name !in decl_fields { - return error('unknown field "${f.name}" for struct ${e.name} (line ${e.line})') - } - if f.name in seen { - return error('duplicate field "${f.name}" in struct literal (line ${e.line})') - } - seen[f.name] = true - } - } - // for each field: push the name string then the value; mkstruct n - // pops the (name, value) pairs and builds the record - for f in e.fields { - g.emit_field_name(f.name) - g.gen_expr(f.val)! - } - g.code << op_mkstruct - g.code << obj.encode_i64(i64(e.fields.len)) - } - .field { - // check if it's an enum variant (e.g., Color.red) - if e.left.kind == .ident { - key := '${e.left.name}.${e.name}' - if key in g.enum_vals { - g.code << op_push_i - g.code << obj.encode_i64(i64(g.enum_vals[key])) - return - } - } - g.gen_expr(*e.left)! - g.emit_field_name(e.name) - g.code << op_sget - } - .method_call { - // p.dist(x) → call .dist p, x - recv_t := g.method_receiver_type(e)! - // built-in: enum.to_string() generates a match on the integer value - if e.name == 'to_string' && recv_t in g.enums && e.args.len == 0 { - g.gen_enum_to_string(recv_t, *e.left, e.line)! - return - } - // built-in: enum.count() returns the number of variants - if e.name == 'count' && recv_t in g.enums && e.args.len == 0 { - g.gen_expr(*e.left)! - g.code << op_pop - variants := g.enums[recv_t] - g.code << op_push_i - g.code << obj.encode_i64(i64(variants.len)) - return - } - g.gen_expr(*e.left)! - 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: '${recv_t}.${e.name}', kind: 0 } - g.code << obj.encode_i64(i64(e.args.len + 1)) // receiver + args - } - .index { - // if the index is a string literal, use struct field access (map style) - if e.right.kind == .str_lit { - g.gen_expr(*e.left)! - g.emit_field_name(e.right.str_v) - g.code << op_sget - } else { - g.gen_expr(*e.left)! - g.gen_expr(*e.right)! - g.code << op_aget - } - } - .bool_lit { - g.code << op_push_i - g.code << obj.encode_i64(e.int_v) - } - .ident { - // check if it's a constant - if e.name in g.consts { - g.code << op_push_i - g.code << obj.encode_i64(g.consts[e.name]) - } else if e.name in g.enum_vals { - // check if it's an enum variant (e.g., Color.red) - g.code << op_push_i - g.code << obj.encode_i64(i64(g.enum_vals[e.name])) - } else { - 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 - } - if e.name == 'len' { - if e.args.len != 1 { - return error('len() takes exactly one argument (line ${e.line})') - } - g.gen_expr(e.args[0])! - g.code << op_alen - return - } - if e.name == 'push' { - if e.args.len != 2 { - return error('push() takes exactly two arguments (line ${e.line})') - } - g.gen_expr(e.args[0])! - g.gen_expr(e.args[1])! - g.code << op_apush - return - } - if e.name == 'has' { - if e.args.len != 2 { - return error('has() takes exactly two arguments (line ${e.line})') - } - g.gen_expr(e.args[0])! - g.gen_expr(e.args[1])! - g.code << op_shas - return - } - if e.name == 'delete' { - if e.args.len != 2 { - return error('delete() takes exactly two arguments (line ${e.line})') - } - g.gen_expr(e.args[0])! - g.gen_expr(e.args[1])! - g.code << op_sdel - return - } - if e.name == 'keys' { - if e.args.len != 1 { - return error('keys() takes exactly one argument (line ${e.line})') - } - g.gen_expr(e.args[0])! - g.code << op_skeys - 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 - } - } -} - -// expr_type returns the declared struct type of an expression when it is -// statically knowable: a typed literal `Point{...}`, a copy of a typed -// variable, or an enum variant `Enum.variant`. Everything else has no -// known type (''). -fn (mut g Gen) expr_type(e Expr) string { - if e.kind == .struct_lit { - return e.name - } - if e.kind == .ident { - return g.types[e.name] or { '' } - } - // enum variant: Color.red → type is "Color" - if e.kind == .field && e.left.kind == .ident { - key := '${e.left.name}.${e.name}' - if key in g.enum_vals { - return e.left.name - } - } - return '' -} - -// method_receiver_type resolves the struct type a method call is made on. -// The receiver must be a plain variable whose type the compiler knows -// (from a typed literal, an assignment, or a method receiver binding) -// or an enum variant expression (e.g. Color.red). -fn (mut g Gen) method_receiver_type(e Expr) !string { - recv := e.left - if recv.kind == .ident { - t := g.types[recv.name] or { '' } - if t.len > 0 { - return t - } - } - // enum variant: Color.red → type is "Color" - if recv.kind == .field && recv.left.kind == .ident { - key := '${recv.left.name}.${recv.name}' - if key in g.enum_vals { - return recv.left.name - } - } - return error('cannot resolve method "${e.name}": receiver type unknown (line ${e.line})') -} - -// gen_enum_to_string generates bytecode for `e.to_string()` on an enum value. -// It emits a match statement that maps each integer variant to its string name. -fn (mut g Gen) gen_enum_to_string(enum_name string, recv Expr, line int) ! { - variants := g.enums[enum_name] or { - return error('unknown enum "${enum_name}" at line ${line}') - } - // store the receiver in a temp local - subj_idx := g.new_local() - g.gen_expr(recv)! - g.emit_store(subj_idx) - // end label for the match - end_l := g.new_label() - for i, v in variants { - next_l := g.new_label() - // load subject, push variant integer, compare - g.emit_load(subj_idx) - g.code << op_push_i - g.code << obj.encode_i64(i64(i)) - g.code << op_eq - g.code << op_jz - g.code << obj.encode_i64(0) - g.fixups << Fixup{ name: next_l, off: u32(g.code.len) - 8 } - // push the variant name as a string - g.code << op_push_s - g.code << obj.encode_i64(0) - g.relocs << obj.Reloc{ offset: u32(g.code.len) - 8, name: v, kind: 1 } - // jump to end - 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(next_l) - } - // else: push "unknown" - g.code << op_push_s - g.code << obj.encode_i64(0) - g.relocs << obj.Reloc{ offset: u32(g.code.len) - 8, name: 'unknown', kind: 1 } - g.emit_label(end_l) -} - -// gen_for_enum generates a for loop that iterates over all variants of an enum. -// for x in Color { ... } → for i in 0..count { x = i; ... } (x typed as Color) -fn (mut g Gen) gen_for_enum(var_name string, enum_name string, body []Stmt, line int) ! { - variants := g.enums[enum_name] or { - return error('unknown enum "${enum_name}" at line ${line}') - } - count := variants.len - // i := 0 - var_idx := g.new_local() - bound_idx := g.new_local() - g.code << op_push_i - g.code << obj.encode_i64(0) - g.emit_store(var_idx) - g.code << op_push_i - g.code << obj.encode_i64(i64(count)) - g.emit_store(bound_idx) - loop_l := g.new_label() - inc_l := g.new_label() - end_l := g.new_label() - g.emit_label(loop_l) - g.emit_load(var_idx) - g.emit_load(bound_idx) - g.code << op_lt - g.code << op_jz - g.code << obj.encode_i64(0) - g.fixups << Fixup{ name: end_l, off: u32(g.code.len) - 8 } - g.loops << LoopCtx{ break_l: end_l, continue_l: inc_l } - prev := g.locals[var_name] or { -1 } - prev_t := g.types[var_name] or { '' } - g.locals[var_name] = var_idx - g.types[var_name] = enum_name // type the loop variable as the enum - for s in body { - g.gen_stmt(s)! - } - if prev >= 0 { - g.locals[var_name] = prev - } else { - g.locals.delete(var_name) - } - if prev_t.len > 0 { - g.types[var_name] = prev_t - } - g.loops.delete_last() - g.emit_label(inc_l) - g.emit_load(var_idx) - g.code << op_push_i - g.code << obj.encode_i64(1) - g.code << op_add - g.emit_store(var_idx) - 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) -} - -// emit_field_name pushes a field name as a string constant. Like string -// literals it goes through a kind-1 relocation so multi-file links rebase it. -fn (mut g Gen) emit_field_name(name string) { - g.code << op_push_s - g.code << obj.encode_i64(0) - g.relocs << obj.Reloc{ offset: u32(g.code.len) - 8, name: name, kind: 1 } -} - -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_local() int { - idx := g.local_cnt - g.local_cnt++ - return idx -} - -fn (mut g Gen) emit_load(idx int) { - g.code << op_load - g.code << obj.encode_i64(i64(idx)) -} - -fn (mut g Gen) emit_store(idx int) { - g.code << op_store - g.code << obj.encode_i64(i64(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 index 731c877..9b9820f 100644 --- a/compiler/lexer.v +++ b/compiler/lexer.v @@ -1,70 +1,6 @@ // lexer.v — tokenizer for the VuurRaaf source language (.vr). module compiler -pub enum TokKind { - eof - ident - int_lit - str_lit - lparen - rparen - lbrace - rbrace - lbracket - rbracket - comma - dot - colon - plus - minus - star - slash - percent - eq_eq - not_eq - lt - le - gt - ge - assign - plus_eq - minus_eq - star_eq - slash_eq - dotdot - dotdotdot - kw_fn - kw_struct - kw_let - kw_if - kw_else - kw_while - kw_for - kw_in - kw_match - kw_break - kw_continue - kw_return - kw_true - kw_false - kw_and - kw_or - kw_not - kw_print - kw_println - kw_assert - kw_import - kw_enum - kw_const -} - -pub struct Tok { -pub: - kind TokKind - lit string - line int -} - pub fn tokenize(src string) ![]Tok { mut l := Lexer{ src: src } mut toks := []Tok{} @@ -242,7 +178,7 @@ fn (mut l Lexer) next() !Tok { } return Tok{ kind: .gt, lit: '>', line: line } } - `"` { + `\"` { return l.lex_string(line)! } `0`...`9` { @@ -310,7 +246,7 @@ fn (mut l Lexer) lex_string(line int) !Tok { mut s := '' for l.pos < l.src.len { c := l.advance() - if c == `"` { + if c == `\"` { return Tok{ kind: .str_lit, lit: s, line: line } } if c == `\\` { @@ -325,7 +261,7 @@ fn (mut l Lexer) lex_string(line int) !Tok { `t` { s += '\t' } - `"` { + `\"` { s += '"' } `\\` { diff --git a/compiler/opcodes.v b/compiler/opcodes.v new file mode 100644 index 0000000..73ac572 --- /dev/null +++ b/compiler/opcodes.v @@ -0,0 +1,49 @@ +// opcodes.v — bytecode opcodes for the VuurRaaf compiler. +// +// Keep in sync with vm/vm.v and assembler/assembler.v. +module compiler + +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 op_mkarray = u8(32) +const op_aget = u8(33) +const op_aset = u8(34) +const op_alen = u8(35) +const op_apush = u8(36) +const op_mkstruct = u8(37) +const op_sget = u8(38) +const op_sset = u8(39) +const op_shas = u8(40) +const op_sdel = u8(41) +const op_slen = u8(42) +const op_skeys = u8(43) diff --git a/compiler/parser.v b/compiler/parser.v index ce1085d..29ec861 100644 --- a/compiler/parser.v +++ b/compiler/parser.v @@ -35,134 +35,6 @@ // ('.' IDENT '(' ... ')' is a method call; everything else field access) module compiler -pub enum ExprKind { - int_lit - str_lit - bool_lit - ident - array_lit - struct_lit - index - field - method_call - unary - binary - call -} - -// StructField is one `name: value` entry of a struct literal. -pub struct StructField { -pub mut: - name string - val Expr -} - -pub struct Expr { -pub mut: - kind ExprKind - int_v i64 - str_v string - name string // ident/call name, or the field name of a `.field` access - op TokKind - left &Expr = unsafe { nil } - right &Expr = unsafe { nil } - elems []Expr - fields []StructField // struct_lit: the named fields - args []Expr - line int -} - -pub enum StmtKind { - expr_stmt - let_stmt - assign_stmt - index_assign - field_assign - if_stmt - match_stmt - while_stmt - for_range_stmt - for_in_stmt - break_stmt - continue_stmt - ret_stmt - assert_stmt -} - -// MatchArm is a single `value { body }` arm of a match statement. -pub struct MatchArm { -pub mut: - val Expr - body []Stmt -} - -pub struct Stmt { -pub mut: - kind StmtKind - target string - expr Expr - cond Expr - base Expr // index_assign: the indexed expression - idx Expr // index_assign: the index expression - body []Stmt - els []Stmt - arms []MatchArm // match_stmt: the arms (val + body) - has_else bool // match_stmt: a trailing else arm exists - els_body []Stmt // match_stmt: body of the else arm - has_val bool - inclusive bool // for_range_stmt: `..` (false) vs `...` (true) - line int -} - -// StructDecl is a `struct Name { a, b }` declaration. -pub struct StructDecl { -pub mut: - name string - fields []string - line int -} - -pub struct FnDecl { -pub mut: - name string - recv_name string // method receiver local name ('' for plain functions) - recv_type string // method receiver struct type ('' for plain functions) - params []string - body []Stmt - line int -} - -pub struct ImportDecl { -pub mut: - path string - line int -} - -// EnumDecl is an `enum Name { variant1 variant2 ... }` declaration. -pub struct EnumDecl { -pub mut: - name string - variants []string - line int -} - -// ConstDecl is a `const NAME = value` declaration. -pub struct ConstDecl { -pub mut: - name string - value Expr - line int -} - -pub struct Program { -pub mut: - fns []FnDecl - structs []StructDecl - enums []EnumDecl - imports []ImportDecl - consts []ConstDecl -} - pub fn parse(toks []Tok) !Program { mut p := Parser{ toks: toks } return p.parse_program() diff --git a/compiler/tokens.v b/compiler/tokens.v new file mode 100644 index 0000000..8230085 --- /dev/null +++ b/compiler/tokens.v @@ -0,0 +1,66 @@ +// tokens.v — token types for the VuurRaaf lexer. +module compiler + +pub enum TokKind { + eof + ident + int_lit + str_lit + lparen + rparen + lbrace + rbrace + lbracket + rbracket + comma + dot + colon + plus + minus + star + slash + percent + eq_eq + not_eq + lt + le + gt + ge + assign + plus_eq + minus_eq + star_eq + slash_eq + dotdot + dotdotdot + kw_fn + kw_struct + kw_let + kw_if + kw_else + kw_while + kw_for + kw_in + kw_match + kw_break + kw_continue + kw_return + kw_true + kw_false + kw_and + kw_or + kw_not + kw_print + kw_println + kw_assert + kw_import + kw_enum + kw_const +} + +pub struct Tok { +pub: + kind TokKind + lit string + line int +} diff --git a/vm/opcodes.v b/vm/opcodes.v new file mode 100644 index 0000000..ecbeccd --- /dev/null +++ b/vm/opcodes.v @@ -0,0 +1,49 @@ +// opcodes.v — bytecode opcodes for the VuurRaaf VM. +// +// Keep in sync with the compiler and assembler. +module vm + +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 op_mkarray = u8(32) +const op_aget = u8(33) +const op_aset = u8(34) +const op_alen = u8(35) +const op_apush = u8(36) +const op_mkstruct = u8(37) +const op_sget = u8(38) +const op_sset = u8(39) +const op_shas = u8(40) // has(map, "key") -> 1 if key exists, 0 otherwise +const op_sdel = u8(41) // delete(map, "key") -> removes the key +const op_slen = u8(42) // slen(struct) -> number of fields +const op_skeys = u8(43) // skeys(struct) -> array of field name strings diff --git a/vm/ops.v b/vm/ops.v new file mode 100644 index 0000000..e3e3b61 --- /dev/null +++ b/vm/ops.v @@ -0,0 +1,111 @@ +// ops.v — arithmetic, comparison, and string operations for the VuurRaaf VM. +module vm + +fn (mut v Vm) add(a i64, b i64) !i64 { + if v.is_arr(a) || v.is_arr(b) { + return error('cannot add arrays with +') + } + if v.is_struct(a) || v.is_struct(b) { + return error('cannot add structs with +') + } + 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}"') + } + if v.is_arr(a) || v.is_arr(b) { + return error('cannot use arrays with "${op}"') + } + if v.is_struct(a) || v.is_struct(b) { + return error('cannot use structs 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_arr(a) || v.is_arr(b) { + // arrays compare by identity (handle equality) with ==/!= + if op == '==' || op == '!=' { + return bool_i64(if op == '==' { a == b } else { a != b }) + } + return error('cannot order arrays') + } + if v.is_struct(a) || v.is_struct(b) { + // structs compare by identity (handle equality) with ==/!= + if op == '==' || op == '!=' { + return bool_i64(if op == '==' { a == b } else { a != b }) + } + return error('cannot order structs') + } + 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}"') } + }) +} diff --git a/vm/print.v b/vm/print.v new file mode 100644 index 0000000..1f92f03 --- /dev/null +++ b/vm/print.v @@ -0,0 +1,110 @@ +// print.v — value rendering and instruction tracing for the VuurRaaf VM. +module vm + +fn (mut v Vm) print_val(x i64) { + print(v.val_str(x, 0)) +} + +// val_str renders a value: strings as-is, arrays as [a, b, ...] (with a depth +// guard so self-referential arrays cannot hang the printer), numbers as ints. +fn (mut v Vm) val_str(x i64, depth int) string { + if depth > 16 { + return '...' + } + if v.is_str(x) && v.valid_handle(x) { + return v.strings[v.hand(x)] + } + if v.is_arr(x) && v.valid_arr_handle(x) { + a := v.arrays[v.hand(x)] + mut s := '[' + limit := if a.len > 20 { 20 } else { a.len } + for i in 0..limit { + if i > 0 { + s += ', ' + } + s += v.val_str(a[i], depth + 1) + } + if a.len > limit { + s += ', ...' + } + return s + ']' + } + if v.is_struct(x) && v.valid_struct_handle(x) { + s := v.structs[v.hand(x)] + mut out := '{' + limit := if s.fields.len > 20 { 20 } else { s.fields.len } + for i in 0..limit { + if i > 0 { + out += ', ' + } + out += s.fields[i].name + ': ' + v.val_str(s.fields[i].val, depth + 1) + } + if s.fields.len > limit { + out += ', ...' + } + return out + '}' + } + return v.dec_int(x).str() +} + +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' } + op_mkarray { 'mkarray' } + op_aget { 'aget' } + op_aset { 'aset' } + op_alen { 'alen' } + op_apush { 'apush' } + op_mkstruct { 'mkstruct' } + op_sget { 'sget' } + op_sset { 'sset' } + op_shas { 'shas' } + op_sdel { 'sdel' } + op_slen { 'slen' } + op_skeys { 'skeys' } + 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.val_str(v.stack[i], 0) + } + } + println(' [ip=${v.ip:4}] ${name:-9} stack: [${s}]') +} diff --git a/vm/types.v b/vm/types.v new file mode 100644 index 0000000..1741078 --- /dev/null +++ b/vm/types.v @@ -0,0 +1,34 @@ +// types.v — core types and constants for the VuurRaaf VM. +module vm + +const stack_cap = 65536 + +// Field is one `name: value` entry of a struct value. +struct Field { +mut: + name string + val i64 +} + +struct StructVal { +mut: + fields []Field +} + +struct Vm { +mut: + code []u8 + strings []string + arrays [][]i64 + structs []StructVal + stack []i64 + sp int + bp int + ip int + trace bool + halted bool +} + +fn bool_i64(b bool) i64 { + return if b { i64(1) } else { i64(0) } +} diff --git a/vm/value.v b/vm/value.v new file mode 100644 index 0000000..f09b8c0 --- /dev/null +++ b/vm/value.v @@ -0,0 +1,63 @@ +// value.v — tagged value encoding and helpers for the VuurRaaf VM. +// +// Stack values are 64-bit tagged integers with two tag bits: +// low bits 00 -> encoded number (value = raw << 2) +// low bits 01 -> string handle (handle = value >> 2, into v.strings) +// low bits 10 -> struct handle (handle = value >> 2, into v.structs) +// low bits 11 -> array handle (handle = value >> 2, into v.arrays) +module vm + +fn (mut v Vm) is_str(x i64) bool { + return x & 3 == 1 +} + +fn (mut v Vm) is_arr(x i64) bool { + return x & 3 == 3 +} + +fn (mut v Vm) is_struct(x i64) bool { + return x & 3 == 2 +} + +fn (mut v Vm) enc_int(x i64) i64 { + return u64(x) << 2 +} + +fn (mut v Vm) dec_int(x i64) i64 { + return x >> 2 +} + +fn (mut v Vm) hand(x i64) int { + return int(x >> 2) +} + +fn (mut v Vm) mkstr(idx int) i64 { + return (u64(idx) << 2) | 1 +} + +fn (mut v Vm) mkarr(idx int) i64 { + return (u64(idx) << 2) | 3 +} + +fn (mut v Vm) mkstruct_handle(idx int) i64 { + return (u64(idx) << 2) | 2 +} + +fn (mut v Vm) truthy(x i64) bool { + return x != 0 +} + +fn (mut v Vm) valid_handle(x i64) bool { + h := v.hand(x) + return h >= 0 && h < v.strings.len +} + +fn (mut v Vm) valid_arr_handle(x i64) bool { + h := v.hand(x) + return h >= 0 && h < v.arrays.len +} + +fn (mut v Vm) valid_struct_handle(x i64) bool { + h := v.hand(x) + return h >= 0 && h < v.structs.len +} diff --git a/vm/vm.v b/vm/vm.v index b94ce58..ecfe963 100644 --- a/vm/vm.v +++ b/vm/vm.v @@ -1,13 +1,5 @@ // vm.v — the VuurRaaf runtime: a small stack-based virtual machine. // -// Stack values are 64-bit tagged integers with two tag bits: -// low bits 00 -> encoded number (value = raw << 2) -// low bits 01 -> string handle (handle = value >> 2, into v.strings) -// low bits 10 -> struct handle (handle = value >> 2, into v.structs) -// low bits 11 -> array handle (handle = value >> 2, into v.arrays) -// Encoding numbers with a constant shift means no integer ever collides with -// a string, struct, or array 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`. @@ -15,80 +7,6 @@ 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 op_mkarray = u8(32) -const op_aget = u8(33) -const op_aset = u8(34) -const op_alen = u8(35) -const op_apush = u8(36) -const op_mkstruct = u8(37) -const op_sget = u8(38) -const op_sset = u8(39) -const op_shas = u8(40) // has(map, "key") -> 1 if key exists, 0 otherwise -const op_sdel = u8(41) // delete(map, "key") -> removes the key -const op_slen = u8(42) // slen(struct) -> number of fields -const op_skeys = u8(43) // skeys(struct) -> array of field name strings - -const stack_cap = 65536 - -// Field is one `name: value` entry of a struct value. -struct Field { -mut: - name string - val i64 -} - -struct StructVal { -mut: - fields []Field -} - -struct Vm { -mut: - code []u8 - strings []string - arrays [][]i64 - structs []StructVal - 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 { @@ -595,279 +513,3 @@ fn (mut v Vm) ret(with_val bool) ! { v.ip = ip v.push(retval)! } - -fn (mut v Vm) is_str(x i64) bool { - return x & 3 == 1 -} - -fn (mut v Vm) is_arr(x i64) bool { - return x & 3 == 3 -} - -fn (mut v Vm) is_struct(x i64) bool { - return x & 3 == 2 -} - -fn (mut v Vm) enc_int(x i64) i64 { - return u64(x) << 2 -} - -fn (mut v Vm) dec_int(x i64) i64 { - return x >> 2 -} - -fn (mut v Vm) hand(x i64) int { - return int(x >> 2) -} - -fn (mut v Vm) mkstr(idx int) i64 { - return (u64(idx) << 2) | 1 -} - -fn (mut v Vm) mkarr(idx int) i64 { - return (u64(idx) << 2) | 3 -} - -fn (mut v Vm) mkstruct_handle(idx int) i64 { - return (u64(idx) << 2) | 2 -} - -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_arr(a) || v.is_arr(b) { - return error('cannot add arrays with +') - } - if v.is_struct(a) || v.is_struct(b) { - return error('cannot add structs with +') - } - 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}"') - } - if v.is_arr(a) || v.is_arr(b) { - return error('cannot use arrays with "${op}"') - } - if v.is_struct(a) || v.is_struct(b) { - return error('cannot use structs 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_arr(a) || v.is_arr(b) { - // arrays compare by identity (handle equality) with ==/!= - if op == '==' || op == '!=' { - return bool_i64(if op == '==' { a == b } else { a != b }) - } - return error('cannot order arrays') - } - if v.is_struct(a) || v.is_struct(b) { - // structs compare by identity (handle equality) with ==/!= - if op == '==' || op == '!=' { - return bool_i64(if op == '==' { a == b } else { a != b }) - } - return error('cannot order structs') - } - 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) { - print(v.val_str(x, 0)) -} - -// val_str renders a value: strings as-is, arrays as [a, b, ...] (with a depth -// guard so self-referential arrays cannot hang the printer), numbers as ints. -fn (mut v Vm) val_str(x i64, depth int) string { - if depth > 16 { - return '...' - } - if v.is_str(x) && v.valid_handle(x) { - return v.strings[v.hand(x)] - } - if v.is_arr(x) && v.valid_arr_handle(x) { - a := v.arrays[v.hand(x)] - mut s := '[' - limit := if a.len > 20 { 20 } else { a.len } - for i in 0..limit { - if i > 0 { - s += ', ' - } - s += v.val_str(a[i], depth + 1) - } - if a.len > limit { - s += ', ...' - } - return s + ']' - } - if v.is_struct(x) && v.valid_struct_handle(x) { - s := v.structs[v.hand(x)] - mut out := '{' - limit := if s.fields.len > 20 { 20 } else { s.fields.len } - for i in 0..limit { - if i > 0 { - out += ', ' - } - out += s.fields[i].name + ': ' + v.val_str(s.fields[i].val, depth + 1) - } - if s.fields.len > limit { - out += ', ...' - } - return out + '}' - } - return v.dec_int(x).str() -} - -fn (mut v Vm) valid_handle(x i64) bool { - h := v.hand(x) - return h >= 0 && h < v.strings.len -} - -fn (mut v Vm) valid_arr_handle(x i64) bool { - h := v.hand(x) - return h >= 0 && h < v.arrays.len -} - -fn (mut v Vm) valid_struct_handle(x i64) bool { - h := v.hand(x) - return h >= 0 && h < v.structs.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' } - op_mkarray { 'mkarray' } - op_aget { 'aget' } - op_aset { 'aset' } - op_alen { 'alen' } - op_apush { 'apush' } - op_mkstruct { 'mkstruct' } - op_sget { 'sget' } - op_sset { 'sset' } - op_shas { 'shas' } - op_sdel { 'sdel' } - op_slen { 'slen' } - op_skeys { 'skeys' } - 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.val_str(v.stack[i], 0) - } - } - println(' [ip=${v.ip:4}] ${name:-9} stack: [${s}]') -}