diff --git a/compiler/compiler.v b/compiler/compiler.v index 7defe45..6a7c330 100644 --- a/compiler/compiler.v +++ b/compiler/compiler.v @@ -49,6 +49,10 @@ 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 { @@ -89,6 +93,7 @@ mut: 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 @@ -111,6 +116,20 @@ fn gen(prog Program) !obj.Obj { 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 { @@ -220,10 +239,18 @@ fn (mut g Gen) gen_stmt(st Stmt) ! { 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 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) @@ -350,6 +377,11 @@ fn (mut g Gen) gen_stmt(st Stmt) ! { 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() @@ -498,6 +530,20 @@ fn (mut g Gen) gen_expr(e Expr) ! { .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)! @@ -508,17 +554,28 @@ fn (mut g Gen) gen_expr(e Expr) ! { g.code << obj.encode_i64(i64(e.args.len + 1)) // receiver + args } .index { - g.gen_expr(*e.left)! - g.gen_expr(*e.right)! - g.code << op_aget + // 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 an enum variant (e.g., Color.red) - if e.name in g.enum_vals { + // 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 { @@ -572,6 +629,32 @@ fn (mut g Gen) gen_call(e Expr) ! { 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)! } @@ -674,7 +757,8 @@ fn (mut g Gen) expr_type(e Expr) string { // 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). +// (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 { @@ -683,9 +767,110 @@ fn (mut g Gen) method_receiver_type(e Expr) !string { 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) { diff --git a/compiler/lexer.v b/compiler/lexer.v index a0834f0..83e9a51 100644 --- a/compiler/lexer.v +++ b/compiler/lexer.v @@ -51,6 +51,7 @@ pub enum TokKind { kw_assert kw_import kw_enum + kw_const } pub struct Tok { @@ -278,6 +279,7 @@ fn (mut l Lexer) lex_ident(line int) Tok { 'assert' { TokKind.kw_assert } 'import' { TokKind.kw_import } 'enum' { TokKind.kw_enum } + 'const' { TokKind.kw_const } else { TokKind.ident } } return Tok{ kind: kind, lit: lit, line: line } diff --git a/compiler/parser.v b/compiler/parser.v index b423c6b..ab73caf 100644 --- a/compiler/parser.v +++ b/compiler/parser.v @@ -1,10 +1,11 @@ // parser.v — recursive-descent parser for the VuurRaaf language. // // Grammar (informal): -// program := import* (struct | enum | fn)* +// program := import* (struct | enum | const | fn)* // import := 'import' STRING // struct := 'struct' IDENT '{' [IDENT (',' IDENT)*] '}' // enum := 'enum' IDENT '{' [IDENT (',' IDENT)*] '}' +// const := 'const' IDENT '=' expr // fn := 'fn' [ '(' IDENT IDENT ')' ] IDENT '(' [IDENT (',' IDENT)*] ')' block // block := '{' stmt* '}' // stmt := 'let' IDENT '=' expr @@ -145,12 +146,21 @@ pub mut: 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 + fns []FnDecl + structs []StructDecl + enums []EnumDecl + imports []ImportDecl + consts []ConstDecl } pub fn parse(toks []Tok) !Program { @@ -209,6 +219,7 @@ fn (mut p Parser) parse_program() !Program { match p.cur().kind { .kw_struct { prog.structs << p.parse_struct_decl()! } .kw_enum { prog.enums << p.parse_enum_decl()! } + .kw_const { prog.consts << p.parse_const_decl()! } .kw_fn { prog.fns << p.parse_fn()! } else { return error('unexpected token "${p.cur().lit}" at line ${p.cur().line}') } } @@ -271,6 +282,15 @@ fn (mut p Parser) parse_enum_decl() !EnumDecl { return EnumDecl{ name: name.lit, variants: variants, line: t.line } } +// parse_const_decl parses `const NAME = expr`. +fn (mut p Parser) parse_const_decl() !ConstDecl { + t := p.expect(.kw_const, "'const'")! + name := p.expect(.ident, 'constant name')! + p.expect(.assign, "'='")! + value := p.parse_expr()! + return ConstDecl{ name: name.lit, value: value, line: t.line } +} + // parse_fn parses `fn name(params) { }` or a method `fn (p Type) name(params) { }`. fn (mut p Parser) parse_fn() !FnDecl { fn_tok := p.expect(.kw_fn, "'fn'")! @@ -710,16 +730,22 @@ fn (mut p Parser) parse_args() ![]Expr { return args } -// parse_struct_fields parses `{ name: expr, ... }` and returns the fields. +// parse_struct_fields parses `{ name: expr, ... }` or `{ "key": expr, ... }` and returns the fields. fn (mut p Parser) parse_struct_fields() ![]StructField { p.expect(.lbrace, "'{'")! mut fields := []StructField{} if p.cur().kind != .rbrace { for { - name := p.expect(.ident, 'field name')! + // field name can be an identifier or a string literal (for maps) + mut fname := '' + if p.cur().kind == .str_lit { + fname = p.advance().lit + } else { + fname = p.expect(.ident, 'field name')!.lit + } p.expect(.colon, "':'")! val := p.parse_expr()! - fields << StructField{ name: name.lit, val: val } + fields << StructField{ name: fname, val: val } if p.cur().kind == .comma { p.advance() continue @@ -737,5 +763,6 @@ fn (mut p Parser) looks_like_struct_lit() bool { if p.pos + 2 >= p.toks.len { return false } - return p.toks[p.pos + 1].kind == .ident && p.toks[p.pos + 2].kind == .colon + // typed struct literal: `{ ident :` or map literal: `{ "key" :` + return (p.toks[p.pos + 1].kind == .ident || p.toks[p.pos + 1].kind == .str_lit) && p.toks[p.pos + 2].kind == .colon } diff --git a/examples/constants.vr b/examples/constants.vr new file mode 100644 index 0000000..0bccdb4 --- /dev/null +++ b/examples/constants.vr @@ -0,0 +1,50 @@ +// constants.vr — demonstrates const declarations + +const MAX_SIZE = 100 +const MIN_VALUE = 0 +const IS_DEBUG = 1 +const VERSION = 42 + +fn calculate(x) { + // constants work inside functions + if x > MAX_SIZE { + return MAX_SIZE + } + if x < MIN_VALUE { + return MIN_VALUE + } + return x +} + +fn main() { + // constants at top level + assert MAX_SIZE == 100 + assert MIN_VALUE == 0 + assert IS_DEBUG == 1 + assert VERSION == 42 + println("Constants: 100, 0, 1, 42") + + // constants in expressions + let limit = MAX_SIZE * 2 + assert limit == 200 + println("MAX_SIZE * 2 = 200") + + // constants in conditions + if IS_DEBUG == 1 { + println("Debug mode is ON") + } + + // constants in functions + let val = calculate(150) + assert val == 100 + println("calculate(150) = 100") + + let val2 = calculate(-5) + assert val2 == 0 + println("calculate(-5) = 0") + + // constants with boolean operations + assert MAX_SIZE > MIN_VALUE + assert VERSION == 42 + println("All constant assertions passed!") +} diff --git a/examples/enum_count.vr b/examples/enum_count.vr new file mode 100644 index 0000000..00e56a1 --- /dev/null +++ b/examples/enum_count.vr @@ -0,0 +1,46 @@ +// enum_count.vr — demonstrates built-in enum.count() + +enum Color { + red + green + blue +} + +enum Day { + monday + tuesday + wednesday + thursday + friday + saturday + sunday +} + +enum Coin { + penny + nickel + dime + quarter +} + +fn main() { + // Color.count() = 3 + let c = Color.red + assert c.count() == 3 + println("Color has 3 variants") + + // Day.count() = 7 + assert Day.monday.count() == 7 + println("Day has 7 variants") + + // Coin.count() = 4 + let coin = Coin.dime + assert coin.count() == 4 + println("Coin has 4 variants") + + // on a variant literal directly + assert Color.blue.count() == 3 + println("Color.blue.count() = 3") + + println("All count assertions passed!") +} diff --git a/examples/enum_iter.vr b/examples/enum_iter.vr new file mode 100644 index 0000000..7558673 --- /dev/null +++ b/examples/enum_iter.vr @@ -0,0 +1,64 @@ +// enum_iter.vr — demonstrates for-in over enum types + +enum Color { + red + green + blue +} + +enum Day { + monday + tuesday + wednesday + thursday + friday + saturday + sunday +} + +fn main() { + // iterate over Color variants + println("Colors:") + for c in Color { + print(" ") + println(c.to_string()) + } + + // iterate and use count + let total = 0 + for c in Color { + total = total + c.count() + } + assert total == 9 // 3 + 3 + 3 + println("Sum of counts: 9") + + // iterate and use to_string in a match + for d in Day { + if d == Day.saturday or d == Day.sunday { + println(d.to_string() + " is weekend") + } + } + + // break and continue work + let weekdays = 0 + for d in Day { + if d == Day.saturday { + break + } + weekdays = weekdays + 1 + } + assert weekdays == 5 + println("Weekday count: 5") + + // collect variant indices + let indices = [] + for c in Color { + push(indices, c) + } + assert indices[0] == 0 + assert indices[1] == 1 + assert indices[2] == 2 + println("Indices: 0, 1, 2") + + println("All enum iteration assertions passed!") +} diff --git a/examples/enum_tostring.vr b/examples/enum_tostring.vr new file mode 100644 index 0000000..ff6df78 --- /dev/null +++ b/examples/enum_tostring.vr @@ -0,0 +1,50 @@ +// enum_tostring.vr — demonstrates built-in enum.to_string() + +enum Color { + red + green + blue +} + +enum Day { + monday + tuesday + wednesday + thursday + friday + saturday + sunday +} + +fn main() { + // Color.to_string() + let c = Color.red + println(c.to_string()) + assert c.to_string() == "red" + + let g = Color.green + println(g.to_string()) + assert g.to_string() == "green" + + let b = Color.blue + println(b.to_string()) + assert b.to_string() == "blue" + + // Day.to_string() + let d = Day.friday + println(d.to_string()) + assert d.to_string() == "friday" + + let sun = Day.sunday + println(sun.to_string()) + assert sun.to_string() == "sunday" + + // enum variant literal .to_string() + println(Color.green.to_string()) + assert Color.green.to_string() == "green" + + println(Day.monday.to_string()) + assert Day.monday.to_string() == "monday" + + println("All to_string assertions passed!") +} diff --git a/examples/maps.vr b/examples/maps.vr new file mode 100644 index 0000000..4623079 --- /dev/null +++ b/examples/maps.vr @@ -0,0 +1,62 @@ +// maps.vr — demonstrates map/dictionary features + +fn main() { + // create a map using struct literal syntax + let person = { "name": "alice", "age": 30 } + + // access with string index + assert person["name"] == "alice" + assert person["age"] == 30 + println("person[\"name\"] = alice") + println("person[\"age\"] = 30") + + // update with string index + person["age"] = 31 + assert person["age"] == 31 + println("person[\"age\"] after update = 31") + + // add new keys dynamically + person["city"] = "nyc" + assert person["city"] == "nyc" + println("person[\"city\"] = nyc") + + // check key existence with has() + assert has(person, "name") == 1 + assert has(person, "missing") == 0 + println("has(person, \"name\") = 1") + println("has(person, \"missing\") = 0") + + // get all keys with keys() + let k = keys(person) + assert len(k) == 3 + println("keys count = 3") + + // delete a key + delete(person, "city") + assert has(person, "city") == 0 + assert len(person) == 2 + println("after delete: len = 2") + + // len() works on maps + let scores = { "math": 95, "english": 88 } + assert len(scores) == 2 + println("scores len = 2") + + // nested maps + let data = { "user": { "name": "bob" } } + assert data["user"]["name"] == "bob" + println("nested access = bob") + + // maps in arrays + let users = [{ "name": "a" }, { "name": "b" }] + assert users[0]["name"] == "a" + assert users[1]["name"] == "b" + println("maps in arrays OK") + + // empty map + let empty = {} + assert len(empty) == 0 + println("empty map len = 0") + + println("All map assertions passed!") +} diff --git a/examples/maps_debug.vr b/examples/maps_debug.vr new file mode 100644 index 0000000..9c59632 --- /dev/null +++ b/examples/maps_debug.vr @@ -0,0 +1,19 @@ +// maps_debug.vr — debug map keys function + +fn main() { + let person = { "name": "alice", "age": 30 } + + let k = keys(person) + let n = len(k) + println("len(k) = 2") + + if n > 0 { + println("key0") + } + if n > 1 { + println("key1") + } + + assert n == 2 + println("keys OK") +} diff --git a/vm/vm.v b/vm/vm.v index 8c710d7..b94ce58 100644 --- a/vm/vm.v +++ b/vm/vm.v @@ -56,6 +56,10 @@ 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 @@ -350,10 +354,13 @@ fn (mut v Vm) exec() ! { 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') + if v.is_arr(h) && v.valid_arr_handle(h) { + v.push(v.enc_int(i64(v.arrays[v.hand(h)].len)))! + } else if v.is_struct(h) && v.valid_struct_handle(h) { + v.push(v.enc_int(i64(v.structs[v.hand(h)].fields.len)))! + } else { + return error('len() on a non-array, non-struct value') } - v.push(v.enc_int(i64(v.arrays[v.hand(h)].len)))! } op_apush { v.ip++ @@ -433,6 +440,18 @@ fn (mut v Vm) exec() ! { } v.structs[v.hand(h)] = s } + op_shas { + v.op_shas()! + } + op_sdel { + v.op_sdel()! + } + op_slen { + v.op_slen()! + } + op_skeys { + v.op_skeys()! + } else { return error('unknown opcode ${op} at ip ${v.ip}') } @@ -440,6 +459,82 @@ fn (mut v Vm) exec() ! { } } +// op_shas checks if a struct has a field with the given name. +// stack: struct, "key" → pushes 1 if found, 0 if not. +fn (mut v Vm) op_shas() ! { + v.ip++ + name := v.pop()! + h := v.pop()! + if !v.is_struct(h) || !v.valid_struct_handle(h) { + return error('has() on a non-struct value') + } + if !v.is_str(name) || !v.valid_handle(name) { + return error('internal: field name is not a string') + } + fname := v.strings[v.hand(name)] + mut found := false + for f in v.structs[v.hand(h)].fields { + if f.name == fname { + found = true + break + } + } + v.push(v.enc_int(if found { 1 } else { 0 }))! +} + +// op_sdel removes a field from a struct. +// stack: struct, "key" → pushes the struct handle back. +fn (mut v Vm) op_sdel() ! { + v.ip++ + name := v.pop()! + h := v.pop()! + if !v.is_struct(h) || !v.valid_struct_handle(h) { + return error('delete() on a non-struct value') + } + if !v.is_str(name) || !v.valid_handle(name) { + return error('internal: field name is not a string') + } + fname := v.strings[v.hand(name)] + mut s := v.structs[v.hand(h)] + mut new_fields := []Field{} + for f in s.fields { + if f.name != fname { + new_fields << f + } + } + s.fields = new_fields + v.structs[v.hand(h)] = s + v.push(h)! +} + +// op_slen returns the number of fields in a struct. +// stack: struct → pushes field count. +fn (mut v Vm) op_slen() ! { + v.ip++ + h := v.pop()! + if !v.is_struct(h) || !v.valid_struct_handle(h) { + return error('len() on a non-struct value') + } + v.push(v.enc_int(i64(v.structs[v.hand(h)].fields.len)))! +} + +// op_skeys returns an array of field name strings. +// stack: struct → pushes array handle. +fn (mut v Vm) op_skeys() ! { + v.ip++ + h := v.pop()! + if !v.is_struct(h) || !v.valid_struct_handle(h) { + return error('keys() on a non-struct value') + } + mut arr := []i64{} + for f in v.structs[v.hand(h)].fields { + v.strings << f.name + arr << v.mkstr(v.strings.len - 1) + } + v.arrays << arr + v.push(v.mkarr(v.arrays.len - 1))! +} + fn (mut v Vm) read_i64() i64 { mut val := u64(0) for i in 0..8 { @@ -757,6 +852,10 @@ fn (mut v Vm) trace_op(op u8) { op_mkstruct { 'mkstruct' } op_sget { 'sget' } op_sset { 'sset' } + op_shas { 'shas' } + op_sdel { 'sdel' } + op_slen { 'slen' } + op_skeys { 'skeys' } else { '??' } } mut s := ''