diff --git a/README.MD b/README.MD index 95757d0..a886fba 100644 --- a/README.MD +++ b/README.MD @@ -61,45 +61,57 @@ Quick start: ## 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). +A small, V-flavored language. Values are 64-bit integers, strings, or arrays +(strings concatenate with `+` and compare with `==`/`!=`; arrays are mutable +and compare by identity). ``` -fn greet(name) { - println("hello, " + name + "!") -} - -fn fib(n) { - if n < 2 { - return n +fn sum(items) { + let total = 0 + for x in items { // iterate an array + total = total + x } - return fib(n - 1) + fib(n - 2) + return total } 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 + let x = 6 * 7 + assert x == 42 + 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 + + let a = [10, 20, 30] + a[1] = 99 // index assignment + push(a, 40) // grow in place + println(a) // [10, 99, 30, 40] + println(len(a)) // 4 + println(sum(a)) // 179 + + for i in 0..5 { ... } // 0 1 2 3 4 (exclusive ..) + for i in 1...3 { ... } // 1 2 3 (inclusive ...) + + let grid = [[1, 2], [3, 4]] // nested arrays + println(grid[1][0]) // 3 + + let i = 100 + for i in 0..3 { ... } // loop vars are scoped to the loop + println(i) // 100 } ``` - functions: `fn name(a, b) { ... }` with `return expr` - variables: `let name = expr`, reassignment `name = expr` +- arrays: `[e1, e2, ...]`, indexing `a[i]` (read and write), `len(a)`, + `push(a, v)`; array literals may nest +- for loops: `for x in arr { }` and ranges `for i in 0..10 { }` / + `for i in 0...10 { }`; loop variables are scoped to the loop body - operators: `+ - * / %`, `== != < <= > >=`, `and or not`, unary `-` -- statements: `let`, assignment, `if/else`, `while`, `return`, `assert`, calls, - `print(...)` / `println(...)` +- statements: `let`, assignment, `if/else`, `while`, `for`, `return`, `assert`, + calls, `print(...)` / `println(...)` - comments: `//` ## Assembly @@ -125,7 +137,7 @@ helper: 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`. +enter mkarray aget aset alen apush`. ## Formats @@ -141,15 +153,17 @@ enter`. | `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 | +| `vm/` | stack VM: tagged values, call frames, string/array heaps | | `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. +The VM is a stack machine with 64-bit tagged values using two tag bits: +numbers are stored shifted left by two, string handles end in `01`, array +handles in `11` — so no integer ever collides with a heap handle. Arrays live +in a growable heap (`mkarray`/`aget`/`aset`/`alen`/`apush`). 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 (arrays rendered as `[1, 2, ...]`). ## Repository layout diff --git a/assembler/assembler.v b/assembler/assembler.v index 4d75412..667dfa9 100644 --- a/assembler/assembler.v +++ b/assembler/assembler.v @@ -173,6 +173,22 @@ pub fn assemble(src string) !obj.Obj { o.code << u8(31) o.code << obj.encode_i64(parse_int(arg, 'enter')!) } + 'mkarray' { + o.code << u8(32) + o.code << obj.encode_i64(parse_int(arg, 'mkarray')!) + } + 'aget' { + o.code << u8(33) + } + 'aset' { + o.code << u8(34) + } + 'alen' { + o.code << u8(35) + } + 'apush' { + o.code << u8(36) + } else { return error('unknown instruction "${op}"') } @@ -238,10 +254,11 @@ fn instr_len(line string) !int { 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' { + 'gt', 'ge', 'and', 'or', 'not', 'ret', 'retv', 'print', 'println', 'assert', 'aget', + 'aset', 'alen', 'apush' { return 1 } - 'push_int', 'push_str', 'load', 'store', 'jmp', 'jz', 'jnz', 'enter' { + 'push_int', 'push_str', 'load', 'store', 'jmp', 'jz', 'jnz', 'enter', 'mkarray' { return 9 } 'call' { diff --git a/compiler/compiler.v b/compiler/compiler.v index c5d7dbe..1937b57 100644 --- a/compiler/compiler.v +++ b/compiler/compiler.v @@ -41,6 +41,11 @@ 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) // compile parses and compiles VuurRaaf source into an object file. pub fn compile(src string) !obj.Obj { @@ -151,6 +156,12 @@ fn (mut g Gen) gen_stmt(st Stmt) ! { g.code << op_store g.code << obj.encode_i64(i64(idx)) } + .index_assign { + g.gen_expr(st.base)! + g.gen_expr(st.idx)! + g.gen_expr(st.expr)! + g.code << op_aset + } .if_stmt { else_l := g.new_label() end_l := g.new_label() @@ -186,6 +197,87 @@ fn (mut g Gen) gen_stmt(st Stmt) ! { 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() + 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 } + prev := g.locals[st.target] or { -1 } + g.locals[st.target] = var_idx + for s in st.body { + g.gen_stmt(s)! + } + if prev >= 0 { + g.locals[st.target] = prev + } else { + g.locals.delete(st.target) + } + 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 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() + 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.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 } + g.locals[st.target] = elem_idx + for s in st.body { + g.gen_stmt(s)! + } + if prev >= 0 { + g.locals[st.target] = prev + } else { + g.locals.delete(st.target) + } + 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)! @@ -214,6 +306,18 @@ fn (mut g Gen) gen_expr(e Expr) ! { 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)) + } + .index { + 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) @@ -251,6 +355,23 @@ fn (mut g Gen) gen_call(e Expr) ! { 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 + } for a in e.args { g.gen_expr(a)! } @@ -340,6 +461,24 @@ fn (mut g Gen) intern(s string) int { 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}' diff --git a/compiler/lexer.v b/compiler/lexer.v index c7a27d5..c8048c2 100644 --- a/compiler/lexer.v +++ b/compiler/lexer.v @@ -10,6 +10,8 @@ pub enum TokKind { rparen lbrace rbrace + lbracket + rbracket comma plus minus @@ -23,11 +25,15 @@ pub enum TokKind { gt ge assign + dotdot + dotdotdot kw_fn kw_let kw_if kw_else kw_while + kw_for + kw_in kw_return kw_true kw_false @@ -127,6 +133,26 @@ fn (mut l Lexer) next() !Tok { l.advance() return Tok{ kind: .rbrace, lit: '}', line: line } } + `[` { + l.advance() + return Tok{ kind: .lbracket, lit: '[', line: line } + } + `]` { + l.advance() + return Tok{ kind: .rbracket, lit: ']', line: line } + } + `.` { + l.advance() + if l.peek() == `.` { + l.advance() + if l.peek() == `.` { + l.advance() + return Tok{ kind: .dotdotdot, lit: '...', line: line } + } + return Tok{ kind: .dotdot, lit: '..', line: line } + } + return error('unexpected character "." at line ${line}') + } `,` { l.advance() return Tok{ kind: .comma, lit: ',', line: line } @@ -223,6 +249,8 @@ fn (mut l Lexer) lex_ident(line int) Tok { 'if' { TokKind.kw_if } 'else' { TokKind.kw_else } 'while' { TokKind.kw_while } + 'for' { TokKind.kw_for } + 'in' { TokKind.kw_in } 'return' { TokKind.kw_return } 'true' { TokKind.kw_true } 'false' { TokKind.kw_false } diff --git a/compiler/parser.v b/compiler/parser.v index 830880a..4d2b723 100644 --- a/compiler/parser.v +++ b/compiler/parser.v @@ -6,8 +6,11 @@ // block := '{' stmt* '}' // stmt := 'let' IDENT '=' expr // | IDENT '=' expr +// | postfix '=' expr (a[i] = v) // | 'if' cond block ['else' block] // | 'while' cond block +// | 'for' IDENT 'in' range block (range := expr '..' expr | expr '...' expr) +// | 'for' IDENT 'in' expr block (iterate an array) // | 'return' [expr] // | 'assert' expr // | expr @@ -27,6 +30,8 @@ pub enum ExprKind { str_lit bool_lit ident + array_lit + index unary binary call @@ -41,6 +46,7 @@ pub mut: op TokKind left &Expr = unsafe { nil } right &Expr = unsafe { nil } + elems []Expr args []Expr line int } @@ -49,22 +55,28 @@ pub enum StmtKind { expr_stmt let_stmt assign_stmt + index_assign if_stmt while_stmt + for_range_stmt + for_in_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 + 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 + has_val bool + inclusive bool // for_range_stmt: `..` (false) vs `...` (true) + line int } pub struct FnDecl { @@ -195,6 +207,21 @@ fn (mut p Parser) parse_stmt() !Stmt { body := p.parse_block()! return Stmt{ kind: .while_stmt, cond: cond, body: body, line: t.line } } + .kw_for { + p.advance() + name := p.expect(.ident, 'loop variable')! + p.expect(.kw_in, "'in'")! + first := p.parse_expr()! + if p.cur().kind == .dotdot || p.cur().kind == .dotdotdot { + inclusive := p.cur().kind == .dotdotdot + p.advance() + end := p.parse_expr()! + body := p.parse_block()! + return Stmt{ kind: .for_range_stmt, target: name.lit, expr: first, cond: end, inclusive: inclusive, body: body, line: t.line } + } + body := p.parse_block()! + return Stmt{ kind: .for_in_stmt, target: name.lit, expr: first, body: body, line: t.line } + } .kw_return { p.advance() mut e := Expr{} @@ -222,6 +249,16 @@ fn (mut p Parser) parse_stmt() !Stmt { e := p.parse_expr()! return Stmt{ kind: .assign_stmt, target: t.lit, expr: e, line: t.line } } + if p.cur().kind == .lbracket { + // a[i] = v or a[i] (expression statement) + e := p.parse_index_chain(t)! + if p.cur().kind == .assign { + p.advance() + rhs := p.parse_expr()! + return Stmt{ kind: .index_assign, base: *e.left, idx: *e.right, expr: rhs, line: t.line } + } + return Stmt{ kind: .expr_stmt, expr: e, line: t.line } + } e := p.parse_call_or_ident(t)! return Stmt{ kind: .expr_stmt, expr: e, line: t.line } } @@ -250,6 +287,13 @@ fn unary_node(op TokKind, operand Expr, line int) Expr { return Expr{ kind: .unary, op: op, right: &o, line: line } } +// index_node builds `base[idx]`. +fn index_node(base Expr, idx Expr, line int) Expr { + mut b := base + mut i := idx + return Expr{ kind: .index, left: &b, right: &i, line: line } +} + fn (mut p Parser) parse_expr() !Expr { return p.parse_or()! } @@ -321,7 +365,32 @@ fn (mut p Parser) parse_unary() !Expr { e := p.parse_unary()! return unary_node(t.kind, e, t.line) } - return p.parse_primary()! + return p.parse_postfix()! +} + +// parse_postfix handles indexing: `base[expr]`, possibly chained `a[i][j]`. +fn (mut p Parser) parse_postfix() !Expr { + mut e := p.parse_primary()! + for p.cur().kind == .lbracket { + p.advance() + idx := p.parse_expr()! + p.expect(.rbracket, "']'")! + e = index_node(e, idx, e.line) + } + return e +} + +// parse_index_chain is like parse_postfix but starts from an already-consumed +// identifier token (used for statements like `a[i] = v`). +fn (mut p Parser) parse_index_chain(t Tok) !Expr { + mut e := Expr{ kind: .ident, name: t.lit, line: t.line } + for p.cur().kind == .lbracket { + p.advance() + idx := p.parse_expr()! + p.expect(.rbracket, "']'")! + e = index_node(e, idx, t.line) + } + return e } fn (mut p Parser) parse_primary() !Expr { @@ -349,6 +418,22 @@ fn (mut p Parser) parse_primary() !Expr { p.expect(.rparen, "')'")! return e } + .lbracket { + p.advance() + mut elems := []Expr{} + if p.cur().kind != .rbracket { + for { + elems << p.parse_expr()! + if p.cur().kind == .comma { + p.advance() + continue + } + break + } + } + p.expect(.rbracket, "']'")! + return Expr{ kind: .array_lit, elems: elems, line: t.line } + } .ident { p.advance() return p.parse_call_or_ident(t)! diff --git a/examples/arrays.vr b/examples/arrays.vr new file mode 100644 index 0000000..53f327b --- /dev/null +++ b/examples/arrays.vr @@ -0,0 +1,73 @@ +// arrays.vr — arrays and for loops. +// Run with: vr run examples/arrays.vr + +fn sum(items) { + let total = 0 + for x in items { + total = total + x + } + return total +} + +fn main() { + // array literal + let a = [10, 20, 30] + println(a) // [10, 20, 30] + println(len(a)) // 3 + + // indexing: read and write + println(a[0]) // 10 + a[1] = 99 + println(a) // [10, 99, 30] + + // growing with push (mutates in place) + push(a, 40) + println(a) // [10, 99, 30, 40] + println(len(a)) // 4 + + // iterate the array + let total = sum(a) + println("sum = ") + println(total) // 179 + + // range for loops + for i in 0..5 { + println("i = " + i) // 0 1 2 3 4 + } + + for n in 1...3 { + println("n = " + n) // 1 2 3 (inclusive) + } + + // nested loops + let grid = [[1, 2], [3, 4]] + for row in grid { + for cell in row { + print(cell) + print(" ") + } + println("") + } + println(grid[1][0]) // 3 + + // build an array in a loop + let squares = [] + for i in 0..5 { + push(squares, i * i) + } + println(squares) // [0, 1, 4, 9, 16] + + // loop variables are scoped to the loop + let i = 100 + for i in 0..3 { + println(i) + } + println("i is back to ") + println(i) // 100 + + assert sum([1, 2, 3, 4]) == 10 + assert len(squares) == 5 + assert squares[4] == 16 + assert a == a // arrays compare by identity + println("all array asserts passed") +} diff --git a/examples/tests.vr b/examples/tests.vr index a7f8c98..7646e64 100644 --- a/examples/tests.vr +++ b/examples/tests.vr @@ -35,6 +35,49 @@ fn test_booleans() { assert not (x == 0) } +fn test_arrays() { + let a = [1, 2, 3] + assert len(a) == 3 + assert a[0] == 1 + assert a[2] == 3 + a[1] = 42 + assert a[1] == 42 + push(a, 4) + assert len(a) == 4 + assert a[3] == 4 + let m = [[1, 2], [3, 4]] + assert m[1][0] == 3 +} + +fn test_for_range() { + let total = 0 + for i in 0..5 { + total = total + i + } + assert total == 10 // 0+1+2+3+4 + let inc = 0 + for i in 1...3 { + inc = inc + i + } + assert inc == 6 // 1+2+3 (inclusive) +} + +fn test_for_in() { + let a = [5, 6, 7] + let total = 0 + for x in a { + total = total + x + } + assert total == 18 + let squares = [] + for i in 0..4 { + push(squares, i * i) + } + assert squares == squares // identity comparison + assert len(squares) == 4 + assert squares[3] == 9 +} + fn test_failing() { // this one is meant to fail — shows up in `vr test` output assert 1 == 2 diff --git a/vm/vm.v b/vm/vm.v index a8d10ec..a3c3e8a 100644 --- a/vm/vm.v +++ b/vm/vm.v @@ -1,9 +1,11 @@ // 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. +// 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 11 -> array handle (handle = value >> 2, into v.arrays) +// Encoding numbers with a constant shift means no integer ever collides with +// a string 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 @@ -45,6 +47,11 @@ 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 stack_cap = 65536 @@ -52,6 +59,7 @@ struct Vm { mut: code []u8 strings []string + arrays [][]i64 stack []i64 sp int bp int @@ -173,6 +181,9 @@ fn (mut v Vm) exec() ! { if v.is_str(a) { return error('cannot negate a string') } + if v.is_arr(a) { + return error('cannot negate an array') + } v.push(v.enc_int(-v.dec_int(a)))! } op_eq { @@ -280,6 +291,60 @@ fn (mut v Vm) exec() ! { v.push(0)! } } + op_mkarray { + v.ip++ + n := int(v.read_i64()) + mut arr := []i64{len: n} + for i := n - 1; i >= 0; i-- { + arr[i] = v.pop()! + } + v.arrays << arr + v.push(v.mkarr(v.arrays.len - 1))! + } + op_aget { + v.ip++ + idx := int(v.dec_int(v.pop()!)) + h := v.pop()! + if !v.is_arr(h) || !v.valid_arr_handle(h) { + return error('indexing a non-array value') + } + a := v.arrays[v.hand(h)] + if idx < 0 || idx >= a.len { + return error('array index ${idx} out of bounds (len ${a.len})') + } + v.push(a[idx])! + } + op_aset { + v.ip++ + val := v.pop()! + idx := int(v.dec_int(v.pop()!)) + h := v.pop()! + if !v.is_arr(h) || !v.valid_arr_handle(h) { + return error('indexing a non-array value') + } + if idx < 0 || idx >= v.arrays[v.hand(h)].len { + return error('array index ${idx} out of bounds (len ${v.arrays[v.hand(h)].len})') + } + v.arrays[v.hand(h)][idx] = val + } + op_alen { + v.ip++ + h := v.pop()! + if !v.is_arr(h) || !v.valid_arr_handle(h) { + return error('len() on a non-array value') + } + v.push(v.enc_int(i64(v.arrays[v.hand(h)].len)))! + } + op_apush { + v.ip++ + val := v.pop()! + h := v.pop()! + if !v.is_arr(h) || !v.valid_arr_handle(h) { + return error('push() on a non-array value') + } + v.arrays[v.hand(h)] << val + v.push(h)! + } else { return error('unknown opcode ${op} at ip ${v.ip}') } @@ -349,23 +414,31 @@ fn (mut v Vm) ret(with_val bool) ! { } fn (mut v Vm) is_str(x i64) bool { - return x & 1 == 1 + return x & 3 == 1 +} + +fn (mut v Vm) is_arr(x i64) bool { + return x & 3 == 3 } fn (mut v Vm) enc_int(x i64) i64 { - return x << 1 + return x << 2 } fn (mut v Vm) dec_int(x i64) i64 { - return x >> 1 + return x >> 2 } fn (mut v Vm) hand(x i64) int { - return int(x >> 1) + return int(x >> 2) } fn (mut v Vm) mkstr(idx int) i64 { - return (i64(idx) << 1) | 1 + return (i64(idx) << 2) | 1 +} + +fn (mut v Vm) mkarr(idx int) i64 { + return (i64(idx) << 2) | 3 } fn (mut v Vm) truthy(x i64) bool { @@ -377,6 +450,9 @@ fn bool_i64(b bool) i64 { } 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_str(a) && v.is_str(b) { return v.alloc_str(v.strings[v.hand(a)] + v.strings[v.hand(b)]) } @@ -402,6 +478,9 @@ 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}"') + } x := v.dec_int(a) y := v.dec_int(b) match op { @@ -430,6 +509,13 @@ fn (mut v Vm) arith(a i64, b i64, op string) !i64 { } 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_str(a) && v.is_str(b) { sa := v.strings[v.hand(a)] sb := v.strings[v.hand(b)] @@ -460,11 +546,34 @@ fn (mut v Vm) cmp(a i64, b i64, op string) !i64 { } 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)) + 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 + ']' + } + return v.dec_int(x).str() } fn (mut v Vm) valid_handle(x i64) bool { @@ -472,6 +581,11 @@ fn (mut v Vm) valid_handle(x i64) bool { 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) trace_op(op u8) { name := match op { op_halt { 'halt' } @@ -506,6 +620,11 @@ fn (mut v Vm) trace_op(op u8) { op_println { 'println' } op_assert { 'assert' } op_enter { 'enter' } + op_mkarray { 'mkarray' } + op_aget { 'aget' } + op_aset { 'aset' } + op_alen { 'alen' } + op_apush { 'apush' } else { '??' } } mut s := '' @@ -516,7 +635,7 @@ fn (mut v Vm) trace_op(op u8) { 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])}' + s += v.val_str(v.stack[i], 0) } } println(' [ip=${v.ip:4}] ${name:-9} stack: [${s}]')