diff --git a/README.MD b/README.MD index 9bcb202..21df361 100644 --- a/README.MD +++ b/README.MD @@ -61,9 +61,9 @@ Quick start: ## The VuurRaaf language -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). +A small, V-flavored language. Values are 64-bit integers, strings, arrays, or +structs (strings concatenate with `+` and compare with `==`/`!=`; arrays and +structs are mutable references that compare by identity). ``` fn sum(items) { @@ -129,6 +129,12 @@ fn main() { println("workday") } } + + let pt = { x: 3, y: 4 } // struct literal: { name: value, ... } + println(pt.x) // 3 — field access + pt.y = 5 // field assignment + let p = { name: "amy", addr: { city: "nyc" } } // nested structs + println(p.addr.city) // nyc } ``` @@ -143,6 +149,10 @@ fn main() { - else-if chains: `if a { } else if b { } else { }` - `match`: `match expr { v1 { } v2 { } else { } }` — arms test equality on any comparable value (ints, strings, ...); the `else` arm is optional +- structs: literals `{ name: value, ... }` (may nest and may be empty `{}`), + field access `a.b` and assignment `a.b = v` (chained: `a[i].b`, `a.b[i]`); + structs are mutable references (identity `==`/`!=`), and setting a missing + field adds it, so records can be built incrementally - operators: `+ - * / %`, `== != < <= > >=`, `and or not`, unary `-` - statements: `let`, assignment, `if/else`, `match`, `while`, `for`, `break`, `continue`, `return`, `assert`, calls, `print(...)` / `println(...)` @@ -171,7 +181,11 @@ 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 mkarray aget aset alen apush`. +enter mkarray aget aset alen apush mkstruct sget sset`. + +Struct opcodes: `mkstruct n` pops `n` (name, value) pairs and pushes a struct +handle; `sget "field"` / `sset "field"` read/write a named field (pushing the +field name as a string first, exactly like the compiler does). ## Formats diff --git a/assembler/assembler.v b/assembler/assembler.v index 667dfa9..06979c3 100644 --- a/assembler/assembler.v +++ b/assembler/assembler.v @@ -189,6 +189,26 @@ pub fn assemble(src string) !obj.Obj { 'apush' { o.code << u8(36) } + 'mkstruct' { + o.code << u8(37) + o.code << obj.encode_i64(parse_int(arg, 'mkstruct')!) + } + 'sget' { + // sget "name" → push_str "name"; sget + s := unquote(arg)! + o.code << u8(2) + o.code << obj.encode_i64(0) // placeholder — rebased by the linker + o.relocs << obj.Reloc{ offset: u32(o.code.len) - 8, name: s, kind: 1 } + o.code << u8(38) + } + 'sset' { + // sset "name" → push_str "name"; sset + s := unquote(arg)! + o.code << u8(2) + o.code << obj.encode_i64(0) // placeholder — rebased by the linker + o.relocs << obj.Reloc{ offset: u32(o.code.len) - 8, name: s, kind: 1 } + o.code << u8(39) + } else { return error('unknown instruction "${op}"') } @@ -258,7 +278,11 @@ fn instr_len(line string) !int { 'aset', 'alen', 'apush' { return 1 } - 'push_int', 'push_str', 'load', 'store', 'jmp', 'jz', 'jnz', 'enter', 'mkarray' { + 'sget', 'sset' { + return 10 // push_str (9) + the opcode (1) + } + 'push_int', 'push_str', 'load', 'store', 'jmp', 'jz', 'jnz', 'enter', 'mkarray', + 'mkstruct' { return 9 } 'call' { diff --git a/compiler/compiler.v b/compiler/compiler.v index 49ee4f0..629c32f 100644 --- a/compiler/compiler.v +++ b/compiler/compiler.v @@ -46,6 +46,9 @@ 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) // compile parses and compiles VuurRaaf source into an object file. pub fn compile(src string) !obj.Obj { @@ -171,6 +174,13 @@ fn (mut g Gen) gen_stmt(st Stmt) ! { 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() @@ -384,6 +394,21 @@ fn (mut g Gen) gen_expr(e Expr) ! { g.code << op_mkarray g.code << obj.encode_i64(i64(e.elems.len)) } + .struct_lit { + // 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 { + g.gen_expr(*e.left)! + g.emit_field_name(e.name) + g.code << op_sget + } .index { g.gen_expr(*e.left)! g.gen_expr(*e.right)! @@ -522,6 +547,14 @@ fn (mut g Gen) gen_binary(e Expr) ! { } } +// 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] diff --git a/compiler/lexer.v b/compiler/lexer.v index abf52ee..da667bc 100644 --- a/compiler/lexer.v +++ b/compiler/lexer.v @@ -13,6 +13,8 @@ pub enum TokKind { lbracket rbracket comma + dot + colon plus minus star @@ -154,12 +156,16 @@ fn (mut l Lexer) next() !Tok { } return Tok{ kind: .dotdot, lit: '..', line: line } } - return error('unexpected character "." at line ${line}') + return Tok{ kind: .dot, lit: '.', line: line } } `,` { l.advance() return Tok{ kind: .comma, lit: ',', line: line } } + `:` { + l.advance() + return Tok{ kind: .colon, lit: ':', line: line } + } `+` { l.advance() return Tok{ kind: .plus, lit: '+', line: line } diff --git a/compiler/parser.v b/compiler/parser.v index e741b52..ef88c7a 100644 --- a/compiler/parser.v +++ b/compiler/parser.v @@ -6,7 +6,7 @@ // block := '{' stmt* '}' // stmt := 'let' IDENT '=' expr // | IDENT '=' expr -// | postfix '=' expr (a[i] = v) +// | postfix '=' expr (a[i] = v, a.b = v) // | 'if' cond block ['else' ('if' ... | block)] // | 'match' expr '{' (expr block | 'else' block)* '}' // | 'while' cond block @@ -25,6 +25,8 @@ // add := mul (('*'|'/'|'%') mul)* // mul := ('not'|'-') mul | primary // primary := INT | STR | 'true' | 'false' | IDENT ['(' args ')'] | '(' expr ')' +// | '{' [IDENT ':' expr (',' IDENT ':' expr)*] '}' (struct literal) +// postfix := primary ('.' IDENT | '[' expr ']')* (field access, indexing) module compiler pub enum ExprKind { @@ -33,24 +35,34 @@ pub enum ExprKind { bool_lit ident array_lit + struct_lit index + field 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 - op TokKind - left &Expr = unsafe { nil } - right &Expr = unsafe { nil } - elems []Expr - args []Expr - line int + 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 { @@ -58,6 +70,7 @@ pub enum StmtKind { let_stmt assign_stmt index_assign + field_assign if_stmt match_stmt while_stmt @@ -283,22 +296,33 @@ fn (mut p Parser) parse_stmt() !Stmt { } .ident { p.advance() + mut e := Expr{} + if p.cur().kind == .lparen { + // a call statement: foo(args), optionally chained foo().x + e = p.parse_call(t)! + e = p.parse_postfix_tail(e)! + } else { + e = p.parse_postfix_tail(Expr{ kind: .ident, name: t.lit, line: t.line })! + } if p.cur().kind == .assign { + // assignment to an ident, an index, or a field p.advance() - 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 } + rhs := p.parse_expr()! + match e.kind { + .ident { + return Stmt{ kind: .assign_stmt, target: e.name, expr: rhs, line: t.line } + } + .index { + return Stmt{ kind: .index_assign, base: *e.left, idx: *e.right, expr: rhs, line: t.line } + } + .field { + return Stmt{ kind: .field_assign, base: *e.left, target: e.name, expr: rhs, line: t.line } + } + else { + return error('cannot assign to this expression (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 } } .kw_print, .kw_println { @@ -352,6 +376,12 @@ fn index_node(base Expr, idx Expr, line int) Expr { return Expr{ kind: .index, left: &b, right: &i, line: line } } +// field_node builds `base.name`. +fn field_node(base Expr, name string, line int) Expr { + mut b := base + return Expr{ kind: .field, left: &b, name: name, line: line } +} + fn (mut p Parser) parse_expr() !Expr { return p.parse_or()! } @@ -426,29 +456,36 @@ fn (mut p Parser) parse_unary() !Expr { return p.parse_postfix()! } -// parse_postfix handles indexing: `base[expr]`, possibly chained `a[i][j]`. +// parse_postfix handles postfix operators after a primary: indexing +// `a[i]` (chainable `a[i][j]`) and field access `a.b` (chainable `a.b.c`), +// in any mix: `a[i].b`, `a.b[i]`, ... 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 + return p.parse_postfix_tail(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) +// parse_postfix_tail continues a postfix chain from an already-parsed base. +// It takes the base by value (Expr only holds pointers to heap-allocated +// child nodes) and returns the extended chain. +fn (mut p Parser) parse_postfix_tail(e Expr) !Expr { + mut cur := e + for { + if p.cur().kind == .lbracket { + p.advance() + idx := p.parse_expr()! + p.expect(.rbracket, "']'")! + cur = index_node(cur, idx, cur.line) + continue + } + if p.cur().kind == .dot { + p.advance() + name := p.expect(.ident, 'field name')! + cur = field_node(cur, name.lit, cur.line) + continue + } + break } - return e + return cur } fn (mut p Parser) parse_primary() !Expr { @@ -492,6 +529,26 @@ fn (mut p Parser) parse_primary() !Expr { p.expect(.rbracket, "']'")! return Expr{ kind: .array_lit, elems: elems, line: t.line } } + .lbrace { + // struct literal: { name: expr, ... } + p.advance() + mut fields := []StructField{} + if p.cur().kind != .rbrace { + for { + name := p.expect(.ident, 'field name')! + p.expect(.colon, "':'")! + val := p.parse_expr()! + fields << StructField{ name: name.lit, val: val } + if p.cur().kind == .comma { + p.advance() + continue + } + break + } + } + p.expect(.rbrace, "'}'")! + return Expr{ kind: .struct_lit, fields: fields, line: t.line } + } .ident { p.advance() return p.parse_call_or_ident(t)! diff --git a/examples/structs.vr b/examples/structs.vr new file mode 100644 index 0000000..7fdf683 --- /dev/null +++ b/examples/structs.vr @@ -0,0 +1,59 @@ +// structs.vr — struct/record values with field access. +// vr run examples/structs.vr + +fn make_point(x, y) { + return { x: x, y: y } +} + +fn dist_sq(p) { + return p.x * p.x + p.y * p.y +} + +fn main() { + // struct literal: { name: value, ... } + let p = { name: "bob", age: 30 } + println(p) // {name: bob, age: 30} + println(p.name) // bob + println(p.age) // 30 + + // field assignment mutates the record + p.age = 31 + println(p.age) // 31 + + // structs are reference values, like arrays + let q = p + q.age = 40 + println(p.age) // 40 — p and q share the same record + + // structs flow through functions + let pt = make_point(3, 4) + println(dist_sq(pt)) // 25 + + // structs compose with arrays, in both directions + let pts = [{ x: 1, y: 2 }, { x: 10, y: 20 }] + println(pts[1].x) // 10 + pts[0].x = 99 + println(pts[0].x) // 99 + + let person = { name: "amy", addr: { city: "nyc", zip: 10001 } } + println(person.addr.city) // nyc + person.addr.zip = 90001 + println(person.addr.zip) // 90001 + + let holder = { items: [1, 2, 3] } + holder.items[1] = 42 + println(holder.items) // [1, 42, 3] + + // records can be built incrementally from an empty struct + let s = {} + s.kind = "point" + s.value = 7 + println(s) // {kind: point, value: 7} + + // structs compare by identity with == / != + let a = { n: 1 } + let b = a + let c = { n: 1 } + assert a == b + assert a != c +} diff --git a/examples/tests.vr b/examples/tests.vr index 1715089..1491693 100644 --- a/examples/tests.vr +++ b/examples/tests.vr @@ -185,6 +185,47 @@ fn test_match() { assert hit == 0 } +fn test_structs() { + let p = { name: "bob", age: 30 } + assert p.name == "bob" + assert p.age == 30 + p.age = 31 + assert p.age == 31 + + // structs are reference values + let q = p + q.age = 40 + assert p.age == 40 + + // nested structs + let person = { name: "amy", addr: { city: "nyc", zip: 10001 } } + assert person.addr.city == "nyc" + person.addr.zip = 90001 + assert person.addr.zip == 90001 + + // structs in arrays + let pts = [{ x: 1, y: 2 }, { x: 10, y: 20 }] + assert pts[1].x == 10 + pts[0].x = 99 + assert pts[0].x == 99 + + // arrays in structs + let holder = { items: [1, 2, 3] } + holder.items[1] = 42 + assert holder.items[1] == 42 + + // structs passed to and returned from functions + let pt = { x: 3, y: 4 } + assert pt.x * pt.x + pt.y * pt.y == 25 + + // identity comparison + let a = { n: 1 } + let b = a + let c = { n: 1 } + assert a == b + assert a != c +} + 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 a3c3e8a..3393077 100644 --- a/vm/vm.v +++ b/vm/vm.v @@ -3,9 +3,10 @@ // 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 or array handle. +// 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 @@ -52,14 +53,30 @@ 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 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 @@ -184,6 +201,9 @@ fn (mut v Vm) exec() ! { if v.is_arr(a) { return error('cannot negate an array') } + if v.is_struct(a) { + return error('cannot negate a struct') + } v.push(v.enc_int(-v.dec_int(a)))! } op_eq { @@ -345,6 +365,74 @@ fn (mut v Vm) exec() ! { v.arrays[v.hand(h)] << val v.push(h)! } + op_mkstruct { + v.ip++ + n := int(v.read_i64()) + mut fields := []Field{len: n} + // stack holds (name, value) pairs; pop from the last field back + for i := n - 1; i >= 0; i-- { + val := v.pop()! + name := v.pop()! + if !v.is_str(name) || !v.valid_handle(name) { + return error('internal: struct field name is not a string') + } + fields[i] = Field{ name: v.strings[v.hand(name)], val: val } + } + v.structs << StructVal{ fields: fields } + v.push(v.mkstruct_handle(v.structs.len - 1))! + } + op_sget { + v.ip++ + name := v.pop()! + h := v.pop()! + if !v.is_struct(h) || !v.valid_struct_handle(h) { + return error('field access 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 { + v.push(f.val)! + found = true + break + } + } + if !found { + return error('no field "${fname}" on struct') + } + } + op_sset { + v.ip++ + // stack: [struct, value, "name"] — the name is on top + name := v.pop()! + val := v.pop()! + h := v.pop()! + if !v.is_struct(h) || !v.valid_struct_handle(h) { + return error('field assignment 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 found := false + for i, f in s.fields { + if f.name == fname { + s.fields[i].val = val + found = true + break + } + } + if !found { + // setting a missing field adds it, so records can be built + // incrementally from an empty `{}` + s.fields << Field{ name: fname, val: val } + } + v.structs[v.hand(h)] = s + } else { return error('unknown opcode ${op} at ip ${v.ip}') } @@ -421,6 +509,10 @@ 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 x << 2 } @@ -441,6 +533,10 @@ fn (mut v Vm) mkarr(idx int) i64 { return (i64(idx) << 2) | 3 } +fn (mut v Vm) mkstruct_handle(idx int) i64 { + return (i64(idx) << 2) | 2 +} + fn (mut v Vm) truthy(x i64) bool { return x != 0 } @@ -453,6 +549,9 @@ 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)]) } @@ -481,6 +580,9 @@ fn (mut v Vm) arith(a i64, b i64, op string) !i64 { 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 { @@ -516,6 +618,13 @@ fn (mut v Vm) cmp(a i64, b i64, op string) !i64 { } 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)] @@ -573,6 +682,21 @@ fn (mut v Vm) val_str(x i64, depth int) string { } 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() } @@ -586,6 +710,11 @@ fn (mut v Vm) valid_arr_handle(x i64) bool { 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' } @@ -625,6 +754,9 @@ fn (mut v Vm) trace_op(op u8) { op_aset { 'aset' } op_alen { 'alen' } op_apush { 'apush' } + op_mkstruct { 'mkstruct' } + op_sget { 'sget' } + op_sset { 'sset' } else { '??' } } mut s := ''