This commit is contained in:
allexanderbergmns
2026-08-24 15:55:23 +02:00
parent fc0ff47459
commit 265e667912
8 changed files with 414 additions and 48 deletions
+18 -4
View File
@@ -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
+25 -1
View File
@@ -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' {
+33
View File
@@ -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]
+7 -1
View File
@@ -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 }
+98 -41
View File
@@ -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)!
+59
View File
@@ -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
}
+41
View File
@@ -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
+133 -1
View File
@@ -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 := ''