This commit is contained in:
allexanderbergmns
2026-08-24 15:40:16 +02:00
parent f77f9d4dff
commit 18b0b38490
8 changed files with 572 additions and 54 deletions
+43 -29
View File
@@ -61,45 +61,57 @@ Quick start:
## The VuurRaaf language ## The VuurRaaf language
A small, V-flavored language. Everything is a 64-bit integer or a string A small, V-flavored language. Values are 64-bit integers, strings, or arrays
(strings concatenate with `+` and compare with `==`/`!=`; `print`/`println` (strings concatenate with `+` and compare with `==`/`!=`; arrays are mutable
accept both). and compare by identity).
``` ```
fn greet(name) { fn sum(items) {
println("hello, " + name + "!") let total = 0
} for x in items { // iterate an array
total = total + x
fn fib(n) {
if n < 2 {
return n
} }
return fib(n - 1) + fib(n - 2) return total
} }
fn main() { fn main() {
let x = 6 * 7 // let with type inference let x = 6 * 7
assert x == 42 // checked by the vm at runtime assert x == 42
let big = x > 40 and x < 50 // and / or / not, short-circuiting let big = x > 40 and x < 50 // and / or / not, short-circuiting
if big { if big {
println("x is big") println("x is big")
} else { } else {
println("x is small") println("x is small")
} }
let i = 0
while i < 3 { let a = [10, 20, 30]
println("counting " + i) // "counting 0", ... a[1] = 99 // index assignment
i = i + 1 push(a, 40) // grow in place
} println(a) // [10, 99, 30, 40]
println(fib(10)) // 55 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` - functions: `fn name(a, b) { ... }` with `return expr`
- variables: `let name = expr`, reassignment `name = 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 `-` - operators: `+ - * / %`, `== != < <= > >=`, `and or not`, unary `-`
- statements: `let`, assignment, `if/else`, `while`, `return`, `assert`, calls, - statements: `let`, assignment, `if/else`, `while`, `for`, `return`, `assert`,
`print(...)` / `println(...)` calls, `print(...)` / `println(...)`
- comments: `//` - comments: `//`
## Assembly ## Assembly
@@ -125,7 +137,7 @@ helper:
Opcodes: `halt push_int push_str load store pop dup add sub mul div mod neg 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 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 ## Formats
@@ -141,15 +153,17 @@ enter`.
| `compiler/` | lexer, recursive-descent parser, bytecode codegen (VROBJ) | | `compiler/` | lexer, recursive-descent parser, bytecode codegen (VROBJ) |
| `assembler/` | `.vasm` -> VROBJ | | `assembler/` | `.vasm` -> VROBJ |
| `linker/` | resolves relocations, rebases strings, emits VRBIN | | `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 | | `obj/` | VROBJ/VRBIN binary formats |
| `bin/` | small standalone tools: `tl_alloc.v`, `tl_loader.v` | | `bin/` | small standalone tools: `tl_alloc.v`, `tl_loader.v` |
The VM is a stack machine with 64-bit tagged values (numbers are stored The VM is a stack machine with 64-bit tagged values using two tag bits:
shifted left so no integer collides with a string handle). Calls push a frame numbers are stored shifted left by two, string handles end in `01`, array
(return address, base pointer, argc), copy arguments into local slots, and handles in `11` — so no integer ever collides with a heap handle. Arrays live
reserve extra locals with `enter n`. `vr debug` prints every instruction with in a growable heap (`mkarray`/`aget`/`aset`/`alen`/`apush`). Calls push a
the stack contents. 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 ## Repository layout
+19 -2
View File
@@ -173,6 +173,22 @@ pub fn assemble(src string) !obj.Obj {
o.code << u8(31) o.code << u8(31)
o.code << obj.encode_i64(parse_int(arg, 'enter')!) 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 { else {
return error('unknown instruction "${op}"') return error('unknown instruction "${op}"')
} }
@@ -238,10 +254,11 @@ fn instr_len(line string) !int {
op := parts[0] op := parts[0]
match op { match op {
'halt', 'pop', 'dup', 'add', 'sub', 'mul', 'div', 'mod', 'neg', 'eq', 'ne', 'lt', 'le', '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 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 return 9
} }
'call' { 'call' {
+139
View File
@@ -41,6 +41,11 @@ const op_print = u8(28)
const op_println = u8(29) const op_println = u8(29)
const op_assert = u8(30) const op_assert = u8(30)
const op_enter = u8(31) 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. // compile parses and compiles VuurRaaf source into an object file.
pub fn compile(src string) !obj.Obj { 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 << op_store
g.code << obj.encode_i64(i64(idx)) 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 { .if_stmt {
else_l := g.new_label() else_l := g.new_label()
end_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.fixups << Fixup{ name: loop_l, off: u32(g.code.len) - 8 }
g.emit_label(end_l) 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 { .ret_stmt {
if st.has_val { if st.has_val {
g.gen_expr(st.expr)! g.gen_expr(st.expr)!
@@ -214,6 +306,18 @@ fn (mut g Gen) gen_expr(e Expr) ! {
g.code << obj.encode_i64(0) g.code << obj.encode_i64(0)
g.relocs << obj.Reloc{ offset: u32(g.code.len) - 8, name: e.str_v, kind: 1 } 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 { .bool_lit {
g.code << op_push_i g.code << op_push_i
g.code << obj.encode_i64(e.int_v) 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 } g.code << if e.name == 'print' { op_print } else { op_println }
return 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 { for a in e.args {
g.gen_expr(a)! g.gen_expr(a)!
} }
@@ -340,6 +461,24 @@ fn (mut g Gen) intern(s string) int {
return 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 { fn (mut g Gen) new_label() string {
g.next_lbl++ g.next_lbl++
return 'L${g.next_lbl}' return 'L${g.next_lbl}'
+28
View File
@@ -10,6 +10,8 @@ pub enum TokKind {
rparen rparen
lbrace lbrace
rbrace rbrace
lbracket
rbracket
comma comma
plus plus
minus minus
@@ -23,11 +25,15 @@ pub enum TokKind {
gt gt
ge ge
assign assign
dotdot
dotdotdot
kw_fn kw_fn
kw_let kw_let
kw_if kw_if
kw_else kw_else
kw_while kw_while
kw_for
kw_in
kw_return kw_return
kw_true kw_true
kw_false kw_false
@@ -127,6 +133,26 @@ fn (mut l Lexer) next() !Tok {
l.advance() l.advance()
return Tok{ kind: .rbrace, lit: '}', line: line } 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() l.advance()
return Tok{ kind: .comma, lit: ',', line: line } return Tok{ kind: .comma, lit: ',', line: line }
@@ -223,6 +249,8 @@ fn (mut l Lexer) lex_ident(line int) Tok {
'if' { TokKind.kw_if } 'if' { TokKind.kw_if }
'else' { TokKind.kw_else } 'else' { TokKind.kw_else }
'while' { TokKind.kw_while } 'while' { TokKind.kw_while }
'for' { TokKind.kw_for }
'in' { TokKind.kw_in }
'return' { TokKind.kw_return } 'return' { TokKind.kw_return }
'true' { TokKind.kw_true } 'true' { TokKind.kw_true }
'false' { TokKind.kw_false } 'false' { TokKind.kw_false }
+94 -9
View File
@@ -6,8 +6,11 @@
// block := '{' stmt* '}' // block := '{' stmt* '}'
// stmt := 'let' IDENT '=' expr // stmt := 'let' IDENT '=' expr
// | IDENT '=' expr // | IDENT '=' expr
// | postfix '=' expr (a[i] = v)
// | 'if' cond block ['else' block] // | 'if' cond block ['else' block]
// | 'while' cond block // | 'while' cond block
// | 'for' IDENT 'in' range block (range := expr '..' expr | expr '...' expr)
// | 'for' IDENT 'in' expr block (iterate an array)
// | 'return' [expr] // | 'return' [expr]
// | 'assert' expr // | 'assert' expr
// | expr // | expr
@@ -27,6 +30,8 @@ pub enum ExprKind {
str_lit str_lit
bool_lit bool_lit
ident ident
array_lit
index
unary unary
binary binary
call call
@@ -41,6 +46,7 @@ pub mut:
op TokKind op TokKind
left &Expr = unsafe { nil } left &Expr = unsafe { nil }
right &Expr = unsafe { nil } right &Expr = unsafe { nil }
elems []Expr
args []Expr args []Expr
line int line int
} }
@@ -49,22 +55,28 @@ pub enum StmtKind {
expr_stmt expr_stmt
let_stmt let_stmt
assign_stmt assign_stmt
index_assign
if_stmt if_stmt
while_stmt while_stmt
for_range_stmt
for_in_stmt
ret_stmt ret_stmt
assert_stmt assert_stmt
} }
pub struct Stmt { pub struct Stmt {
pub mut: pub mut:
kind StmtKind kind StmtKind
target string target string
expr Expr expr Expr
cond Expr cond Expr
body []Stmt base Expr // index_assign: the indexed expression
els []Stmt idx Expr // index_assign: the index expression
has_val bool body []Stmt
line int els []Stmt
has_val bool
inclusive bool // for_range_stmt: `..` (false) vs `...` (true)
line int
} }
pub struct FnDecl { pub struct FnDecl {
@@ -195,6 +207,21 @@ fn (mut p Parser) parse_stmt() !Stmt {
body := p.parse_block()! body := p.parse_block()!
return Stmt{ kind: .while_stmt, cond: cond, body: body, line: t.line } 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 { .kw_return {
p.advance() p.advance()
mut e := Expr{} mut e := Expr{}
@@ -222,6 +249,16 @@ fn (mut p Parser) parse_stmt() !Stmt {
e := p.parse_expr()! e := p.parse_expr()!
return Stmt{ kind: .assign_stmt, target: t.lit, expr: e, line: t.line } 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)! e := p.parse_call_or_ident(t)!
return Stmt{ kind: .expr_stmt, expr: e, line: t.line } 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 } 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 { fn (mut p Parser) parse_expr() !Expr {
return p.parse_or()! return p.parse_or()!
} }
@@ -321,7 +365,32 @@ fn (mut p Parser) parse_unary() !Expr {
e := p.parse_unary()! e := p.parse_unary()!
return unary_node(t.kind, e, t.line) 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 { fn (mut p Parser) parse_primary() !Expr {
@@ -349,6 +418,22 @@ fn (mut p Parser) parse_primary() !Expr {
p.expect(.rparen, "')'")! p.expect(.rparen, "')'")!
return e 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 { .ident {
p.advance() p.advance()
return p.parse_call_or_ident(t)! return p.parse_call_or_ident(t)!
+73
View File
@@ -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")
}
+43
View File
@@ -35,6 +35,49 @@ fn test_booleans() {
assert not (x == 0) 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() { fn test_failing() {
// this one is meant to fail — shows up in `vr test` output // this one is meant to fail — shows up in `vr test` output
assert 1 == 2 assert 1 == 2
+133 -14
View File
@@ -1,9 +1,11 @@
// vm.v — the VuurRaaf runtime: a small stack-based virtual machine. // vm.v — the VuurRaaf runtime: a small stack-based virtual machine.
// //
// Stack values are 64-bit tagged integers: odd values are string handles into // Stack values are 64-bit tagged integers with two tag bits:
// the runtime string heap (handle = value >> 1); even values are encoded // low bits 00 -> encoded number (value = raw << 2)
// numbers (value = raw << 1). Encoding numbers as even values means no // low bits 01 -> string handle (handle = value >> 2, into v.strings)
// integer ever collides with a string handle. // 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 // 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 // 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_println = u8(29)
const op_assert = u8(30) const op_assert = u8(30)
const op_enter = u8(31) 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 const stack_cap = 65536
@@ -52,6 +59,7 @@ struct Vm {
mut: mut:
code []u8 code []u8
strings []string strings []string
arrays [][]i64
stack []i64 stack []i64
sp int sp int
bp int bp int
@@ -173,6 +181,9 @@ fn (mut v Vm) exec() ! {
if v.is_str(a) { if v.is_str(a) {
return error('cannot negate a string') 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)))! v.push(v.enc_int(-v.dec_int(a)))!
} }
op_eq { op_eq {
@@ -280,6 +291,60 @@ fn (mut v Vm) exec() ! {
v.push(0)! 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 { else {
return error('unknown opcode ${op} at ip ${v.ip}') 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 { 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 { fn (mut v Vm) enc_int(x i64) i64 {
return x << 1 return x << 2
} }
fn (mut v Vm) dec_int(x i64) i64 { fn (mut v Vm) dec_int(x i64) i64 {
return x >> 1 return x >> 2
} }
fn (mut v Vm) hand(x i64) int { fn (mut v Vm) hand(x i64) int {
return int(x >> 1) return int(x >> 2)
} }
fn (mut v Vm) mkstr(idx int) i64 { 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 { 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 { 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) { if v.is_str(a) && v.is_str(b) {
return v.alloc_str(v.strings[v.hand(a)] + v.strings[v.hand(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) { if v.is_str(a) || v.is_str(b) {
return error('cannot use strings with "${op}"') 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) x := v.dec_int(a)
y := v.dec_int(b) y := v.dec_int(b)
match op { 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 { 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) { if v.is_str(a) && v.is_str(b) {
sa := v.strings[v.hand(a)] sa := v.strings[v.hand(a)]
sb := v.strings[v.hand(b)] 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) { fn (mut v Vm) print_val(x i64) {
if v.is_str(x) && v.valid_handle(x) { print(v.val_str(x, 0))
print(v.strings[v.hand(x)]) }
} else {
print(v.dec_int(x)) // 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 { 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 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) { fn (mut v Vm) trace_op(op u8) {
name := match op { name := match op {
op_halt { 'halt' } op_halt { 'halt' }
@@ -506,6 +620,11 @@ fn (mut v Vm) trace_op(op u8) {
op_println { 'println' } op_println { 'println' }
op_assert { 'assert' } op_assert { 'assert' }
op_enter { 'enter' } op_enter { 'enter' }
op_mkarray { 'mkarray' }
op_aget { 'aget' }
op_aset { 'aset' }
op_alen { 'alen' }
op_apush { 'apush' }
else { '??' } else { '??' }
} }
mut s := '' 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]) { if v.is_str(v.stack[i]) && v.valid_handle(v.stack[i]) {
s += '"${v.strings[v.hand(v.stack[i])]}"' s += '"${v.strings[v.hand(v.stack[i])]}"'
} else { } 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}]') println(' [ip=${v.ip:4}] ${name:-9} stack: [${s}]')