This commit is contained in:
allexanderbergmns
2026-08-25 18:03:03 +02:00
parent 3e1e9bd08a
commit 68b3a3673c
15 changed files with 402 additions and 8 deletions
+11 -1
View File
@@ -230,7 +230,7 @@ Module functions are namespaced: `os.exists()`, `json.encode()`, ...
| `crypto` | `sha256(s)`, `md5(s)`, `base64_encode(s)`, `base64_decode(s)` |
| `cli` | getopt-style flag parsing over `args()`: `flag(name, default)`, `has(name)`, `positional()` — supports `--name value`, `--name=value`, boolean `--flag`, and short aliases `-n=value` |
Examples: `examples/imports.vr`, `examples/http.vr`, `examples/time.vr`, `examples/cli_tool.vr` (CLI flags + interactive input + typed errors), `examples/stdlib.vr` (tests for the regex/crypto/csv/os/time additions).
Examples: `examples/imports.vr`, `examples/http.vr`, `examples/time.vr`, `examples/cli_tool.vr` (CLI flags + interactive input + typed errors), `examples/defer_reflection.vr` (`defer`, `in`, `type_info`, `range`), `examples/stdlib.vr` (tests for the regex/crypto/csv/os/time additions).
## The VuurRaaf language
@@ -382,6 +382,16 @@ fn main() {
`try { } catch e { }` like explicit `throw`s
- 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
- `defer <stmt>` runs the statement when the function exits (LIFO, on every
return path), so file/resource cleanup reads naturally: `defer remove(tmp)`
- `x in col` membership operator works on arrays (`2 in [1,2,3]`), maps
(`"key" in m`), and strings (`"sub" in s`), and with `not` (`not (x in a)`)
- `type_info(v)` reflects any value into a `{kind, is_number, is_int, is_float,
is_string, is_array, is_struct, is_none, is_closure, len, fields, contents}`
struct for duck-typing and generic utilities
- `range(a, b)` builds `[a, a+1, ..., b)` (descends when `a > b`) as an array
- reflection: `type(x)` returns "int"/"float"/"string"/"array"/"struct"/
"none"/"closure"; `keys(m)`, `has(m, k)`, `len(m)` inspect records
- `break` / `continue` inside `while` and `for` loops (in `for` loops
`continue` advances the loop variable / iterator first)
- else-if chains: `if a { } else if b { } else { }`
BIN
View File
Binary file not shown.
+1
View File
@@ -68,6 +68,7 @@ pub enum StmtKind {
assert_stmt
try_stmt
throw_stmt
defer_stmt
}
// MatchArm is a single `value { body }` arm of a match statement.
+8 -1
View File
@@ -292,6 +292,11 @@ fn (mut c Checker) check_stmt(st Stmt) ! {
.throw_stmt {
_ = c.check_expr(st.expr)!
}
.defer_stmt {
for s in st.body {
c.check_stmt(s)!
}
}
}
}
@@ -491,7 +496,7 @@ fn (mut c Checker) check_binary(e Expr) !TypeInfo {
}
TypeInfo{ kind: .bool_t }
}
.kw_and, .kw_or {
.kw_and, .kw_or, .kw_in {
TypeInfo{ kind: .bool_t }
}
.amp, .pipe, .caret, .lt_lt, .gt_gt {
@@ -635,6 +640,8 @@ fn builtin_result_type(name string) TypeInfo {
'flag_val' { TypeInfo{ kind: .string_t } }
'flag_has' { TypeInfo{ kind: .int_t } }
'flag_positional' { TypeInfo{ kind: .array_t } }
'type_info' { TypeInfo{ kind: .struct_t } }
'range' { TypeInfo{ kind: .array_t } }
'build_is_dir' { TypeInfo{ kind: .int_t } }
// build-module builtins (.vrmm)
'build_compile', 'build_assemble', 'build_link', 'build_exec', 'build_base',
+110 -5
View File
@@ -47,6 +47,12 @@ mut:
modules map[string]bool // imported module names (bare `import os`)
fn_names map[string]bool // top-level function names usable as closure values
captures []string // enclosing locals captured by the closure being compiled
defers []Stmt // deferred statements of the current function (in order)
has_defers bool // current function registers deferred cleanup
defer_ret_slot int // hidden local holding the return value while defers run
defer_value_seen bool // a `return expr` (value) appeared, so restore+retv after defers
cur_defer_label string // label of the current function's deferred-cleanup block
namer_id int // unique-id source for generated label names
}
fn gen(prog Program) !obj.Obj {
@@ -226,10 +232,45 @@ fn (mut g Gen) gen_fn(fd FnDecl) ! {
g.code << obj.encode_i64(i64(g.argc))
g.code << obj.encode_i64(i64(vidx))
}
// one-pass defer handling: collect every `defer` statement in the body first
// (so all returns can be redirected to the deferred-cleanup block), then
// reserve a hidden slot to hold the return value across that block.
g.defers = []Stmt{}
g.has_defers = false
g.defer_value_seen = false
g.collect_defers(fd.body, mut g.defers)
if g.defers.len > 0 {
g.has_defers = true
g.defer_ret_slot = g.local_cnt
g.local_cnt++
}
g.cur_defer_label = g.new_label()
for st in fd.body {
g.gen_stmt(st)!
}
g.code << op_ret // trailing return for fall-through
if g.has_defers {
g.code << op_jmp
g.code << obj.encode_i64(0)
g.fixups << Fixup{ name: g.cur_defer_label, off: u32(g.code.len) - 8 }
} else {
g.code << op_ret // trailing return for fall-through
}
// emit the deferred-cleanup block (LIFO) then the real return here, so a
// `return` in the body jumps into this block and lands back on the return
if g.has_defers {
g.emit_label(g.cur_defer_label)
for i := g.defers.len - 1; i >= 0; i-- {
g.gen_stmt(g.defers[i])!
}
if g.defer_value_seen {
// restore the saved return value and return it
g.emit_load(g.defer_ret_slot)
g.code << op_retv
} else {
g.code << op_ret
}
}
// reserve all local slots: the callee may be called with fewer arguments
// than declared (default parameters) or more (variadic), so the frame must
// always cover slots 0..local_cnt-1
@@ -245,6 +286,8 @@ fn (mut g Gen) gen_fn(fd FnDecl) ! {
}
g.fixups.clear()
g.labels.clear()
g.defers = []Stmt{} // reset for the next (possibly enclosing) function
g.has_defers = false
// snapshot the live locals as debug info for the debugger: name -> slot
for name, slot in g.locals {
g.dbg_locals << obj.DbgLocal{ fn: sym, name: name, slot: slot }
@@ -519,13 +562,34 @@ fn (mut g Gen) gen_stmt(st Stmt) ! {
g.emit_label(end_l)
}
.ret_stmt {
if st.has_val {
g.gen_expr(st.expr)!
g.code << op_retv
if g.has_defers {
// save any return value in the hidden slot, jump into the
// deferred-cleanup block, which restores and returns it
if st.has_val {
g.defer_value_seen = true
g.gen_expr(st.expr)!
g.emit_store(g.defer_ret_slot)
} else {
g.code << op_push_i
g.code << obj.encode_i64(0)
g.emit_store(g.defer_ret_slot)
}
g.code << op_jmp
g.code << obj.encode_i64(0)
g.fixups << Fixup{ name: g.cur_defer_label, off: u32(g.code.len) - 8 }
} else {
g.code << op_ret
if st.has_val {
g.gen_expr(st.expr)!
g.code << op_retv
} else {
g.code << op_ret
}
}
}
.defer_stmt {
// collected in gen_fn's pass and emitted in the cleanup block;
// nothing to place at the declaration site
}
.assert_stmt {
g.gen_expr(st.expr)!
g.code << op_assert
@@ -1047,6 +1111,9 @@ fn builtin_spec(name string) (int, int) {
'flag_val' { native_flag_val, 1 }
'flag_has' { native_flag_has, 1 }
'flag_positional' { native_flag_positional, 0 }
// structured reflection + sequence helper
'type_info' { native_type_info, 1 }
'range' { native_range, 2 }
'build_is_dir' { native_build_is_dir, 1 }
'cwd' { native_cwd, 0 }
'json_pretty' { native_json_pretty, 1 }
@@ -1257,6 +1324,7 @@ fn (mut g Gen) gen_binary(e Expr) ! {
.le { op_le }
.gt { op_gt }
.ge { op_ge }
.kw_in { op_in }
.amp { op_and_b }
.pipe { op_or_b }
.caret { op_xor }
@@ -1510,6 +1578,38 @@ fn (mut g Gen) maybe_capture(name string, bound map[string]bool, mut caps []stri
}
}
// collect_defers walks a statement list, gathering the inner statements of
// every `defer` into `out` in source order. Called once per function before
// code generation so all return sites can be redirected to the cleanup block.
fn (mut g Gen) collect_defers(stmts []Stmt, mut out []Stmt) {
for st in stmts {
match st.kind {
.defer_stmt {
for d in st.body {
out << d
}
}
.if_stmt {
g.collect_defers(st.body, mut out)
g.collect_defers(st.els, mut out)
}
.match_stmt {
for a in st.arms {
g.collect_defers(a.body, mut out)
}
g.collect_defers(st.els_body, mut out)
}
.while_stmt, .for_range_stmt, .for_in_stmt, .try_stmt {
g.collect_defers(st.body, mut out)
if st.kind == .try_stmt {
g.collect_defers(st.els, mut out)
}
}
else {}
}
}
}
fn (mut g Gen) scan_stmt(st Stmt, mut bound map[string]bool, mut caps []string, mut seen map[string]bool) {
match st.kind {
.expr_stmt {
@@ -1623,6 +1723,11 @@ fn (mut g Gen) scan_stmt(st Stmt, mut bound map[string]bool, mut caps []string,
.throw_stmt {
g.scan_expr(st.expr, mut bound, mut caps, mut seen)
}
.defer_stmt {
for s in st.body {
g.scan_stmt(s, mut bound, mut caps, mut seen)
}
}
.break_stmt, .continue_stmt {}
}
}
+1
View File
@@ -310,6 +310,7 @@ fn (mut l Lexer) lex_ident(line int, col int) Tok {
'interface' { TokKind.kw_interface }
'try' { TokKind.kw_try }
'catch' { TokKind.kw_catch }
'defer' { TokKind.kw_defer }
'throw' { TokKind.kw_throw }
else { TokKind.ident }
}
+5
View File
@@ -66,6 +66,7 @@ const op_load_dyn = u8(59) // pop idx, push stack[bp + idx]
const op_varargs = u8(60) // <named:i64> <dst:i64> — collect args[named..argc-1] into an array at local dst
const op_str_method = u8(61) // <name:str> <argc:i64> — call a string method (s.len(), s.contains(x), ...)
const op_push_none = u8(62) // push the `none` sentinel
const op_in = u8(63) // membership: x in col -> 0 or 1
// native builtin ids (keep in sync with vm/opcodes.v)
const native_abs = 100
@@ -191,3 +192,7 @@ const native_input = 196
const native_flag_val = 197
const native_flag_has = 198
const native_flag_positional = 199
// structured reflection + sequence helper
const native_type_info = 200
const native_range = 201
+6 -1
View File
@@ -403,6 +403,11 @@ fn (mut p Parser) parse_stmt() !Stmt {
body := p.parse_block()!
return Stmt{ kind: .for_in_stmt, target: val_name, idx_target: idx_name, expr: first, body: body, line: t.line }
}
.kw_defer {
p.advance()
inner := p.parse_stmt()!
return Stmt{ kind: .defer_stmt, body: [inner], line: t.line }
}
.kw_return {
p.advance()
mut e := Expr{}
@@ -664,7 +669,7 @@ fn (mut p Parser) parse_eq() !Expr {
fn (mut p Parser) parse_rel() !Expr {
mut e := p.parse_shift()!
for p.cur().kind == .lt || p.cur().kind == .le || p.cur().kind == .gt || p.cur().kind == .ge {
for p.cur().kind == .lt || p.cur().kind == .le || p.cur().kind == .gt || p.cur().kind == .ge || p.cur().kind == .kw_in {
op := p.advance()
rhs := p.parse_shift()!
e = bin_node(op.kind, e, rhs, op.line)
+1
View File
@@ -69,6 +69,7 @@ pub enum TokKind {
kw_interface
kw_try
kw_catch
kw_defer
kw_throw
}
+53
View File
@@ -0,0 +1,53 @@
// defer_reflection.vr — demonstrates the Batch-A features: `defer` cleanup,
// the `in` membership operator, `type_info()` reflection, and `range()`.
//
// vr run examples/defer_reflection.vr
fn summarize(v) {
// reflection: inspect any value and build a description
let ti = type_info(v)
let kind = ti.kind
if ti.is_number {
return "a number " + str(v)
} else if ti.is_string {
return "a string of length " + str(ti.len) + ": \"" + v + "\""
} else if ti.is_array {
return "an array with " + str(ti.len) + " elements"
} else if ti.is_struct {
return "a record with fields " + str(ti.fields)
} else if ti.is_none {
return "nothing"
}
return "a " + kind
}
fn grep_lines(docs, term) {
// demonstrate `in` on strings + arrays, with deferred cleanup
let matches = []
defer println(" -> searched " + str(len(docs)) + " docs for \"" + term + "\"")
for d in docs {
if term in d { // substring membership
push(matches, d)
}
}
return matches
}
fn main() {
// range + in + type_info
let nums = range(1, 6) // [1, 2, 3, 4, 5]
println("nums = " + str(nums))
println("3 present? " + str(3 in nums))
println("7 present? " + str(7 in nums))
println(summarize(42))
println(summarize("hello"))
println(summarize([1, 2, 3]))
println(summarize({x: 1, y: 2}))
println(summarize(none))
println("---")
let docs = ["Introducing VuurRaaf", "A tiny language", "VuurRaaf: daily driver"]
let hits = grep_lines(docs, "VuurRaaf")
println("hits: " + str(hits))
}
+77
View File
@@ -658,6 +658,83 @@ fn test_flag_parsing() {
assert len(flag_positional()) == 0
}
fn test_defer_cleanup() {
// defer runs on every exit path; its effect is visible to the caller via
// the shared mutable array (arrays are references).
let log = []
let a = defer_probe(log, 1)
assert a == 0
assert len(log) == 2
assert log[0] == "open"
assert log[1] == "close"
}
fn defer_probe(log, mode) {
push(log, "open")
defer push(log, "close")
if mode == 0 { // early return path also runs defer
return 9
}
return 0
}
fn test_defer_lifo() {
// defers run in reverse declaration order (LIFO), all before returning
let logA = []
let r = defer_lifo(logA)
assert r == 0
assert len(logA) == 2
assert logA[0] == "2nd-registered"
assert logA[1] == "1st-registered"
}
fn defer_lifo(log) {
defer push(log, "1st-registered")
defer push(log, "2nd-registered")
return 0
}
fn test_in_operator() {
let arr = [1, 2, 3]
assert 2 in arr
assert 9 in arr == false
let m = {x: 1}
assert "x" in m
assert "zzz" in m == false
let s = "hello world"
assert "world" in s
assert "nope" in s == false
assert not (9 in arr)
}
fn test_range_helper() {
let up = range(0, 4)
assert len(up) == 4
assert up[0] == 0
assert up[3] == 3
let down = range(3, 0)
assert down[0] == 3
assert down[2] == 1
assert len(down) == 3
}
fn test_type_info() {
let ti = type_info(42)
assert ti.kind == "int"
assert ti.is_int == 1
assert ti.is_number == 1
let ts = type_info("xyz")
assert ts.is_string == 1
assert ts.len == 3
let tn = type_info(none)
assert tn.kind == "none"
assert tn.is_none == 1
let to = type_info({a: 1})
assert to.is_struct == 1
assert len(to.fields) == 1
assert to.fields[0] == "a"
}
fn test_failing() {
// this one is meant to fail — shows up in `vr test` output
assert 1 == 2
+83
View File
@@ -118,6 +118,15 @@ fn (mut v Vm) native(id int, _argc int) ! {
x := v.pop()!
v.push(v.alloc_str(v.type_name(x)))!
}
native_type_info {
x := v.pop()!
v.push(v.type_info_value(x))!
}
native_range {
b := v.pop()!
a := v.pop()!
v.push(v.range_value(a, b))!
}
native_split {
delim := v.pop_str()!
s := v.pop_str()!
@@ -1239,6 +1248,12 @@ fn (mut v Vm) type_name(x i64) string {
if v.is_float(x) {
return 'float'
}
if v.is_none(x) {
return 'none'
}
if v.is_closure(x) {
return 'closure'
}
if v.is_arr(x) {
return 'array'
}
@@ -1248,6 +1263,74 @@ fn (mut v Vm) type_name(x i64) string {
return 'int'
}
// type_info_value reflects a value into a struct for generic/duck-typed code:
// { kind, is_number, is_int, is_float, is_string, is_array, is_struct,
// is_none, is_closure, len, fields } where `fields` is the array of field
// names (structs) and `len` is the element/character count for arrays/strings.
fn (mut v Vm) type_info_value(x i64) i64 {
kind := v.type_name(x)
is_int := v.is_int(x)
is_float := v.is_float(x)
is_string := v.is_str(x) && v.valid_handle(x)
is_array := v.is_arr(x) && v.valid_arr_handle(x)
is_struct := v.is_struct(x) && v.valid_struct_handle(x)
is_none := v.is_none(x)
is_closure := v.is_closure(x) && v.valid_closure_handle(x)
is_number := is_int || is_float
// len: array -> element count; string -> character count; struct -> field count
mut length := 0
if is_array {
length = v.arrays[v.hand(x)].len
} else if is_string {
length = v.strings[v.hand(x)].runes().len
} else if is_struct {
length = v.structs[v.hand(x)].fields.len
}
// fields: array of struct field names (empty otherwise)
mut names := []i64{}
if is_struct {
for f in v.structs[v.hand(x)].fields {
v.strings << f.name
names << v.mkstr(v.strings.len - 1)
}
}
v.arrays << names
field_h := v.mkarr(v.arrays.len - 1)
mut fields := []Field{}
fields << Field{ name: 'kind', val: v.alloc_str(kind) }
fields << Field{ name: 'is_number', val: v.enc_int(if is_number { 1 } else { 0 }) }
fields << Field{ name: 'is_int', val: v.enc_int(if is_int { 1 } else { 0 }) }
fields << Field{ name: 'is_float', val: v.enc_int(if is_float { 1 } else { 0 }) }
fields << Field{ name: 'is_string', val: v.enc_int(if is_string { 1 } else { 0 }) }
fields << Field{ name: 'is_array', val: v.enc_int(if is_array { 1 } else { 0 }) }
fields << Field{ name: 'is_struct', val: v.enc_int(if is_struct { 1 } else { 0 }) }
fields << Field{ name: 'is_none', val: v.enc_int(if is_none { 1 } else { 0 }) }
fields << Field{ name: 'is_closure', val: v.enc_int(if is_closure { 1 } else { 0 }) }
fields << Field{ name: 'len', val: v.enc_int(i64(length)) }
fields << Field{ name: 'fields', val: field_h }
fields << Field{ name: 'contents', val: x }
v.structs << StructVal{ fields: fields, by_name: v.index_fields(fields) }
return v.mkstruct_handle(v.structs.len - 1)
}
// range_value builds the array [a, a+1, ..., b) as ints.
fn (mut v Vm) range_value(a i64, b i64) i64 {
start := v.dec_int(a)
end := v.dec_int(b)
mut arr := []i64{}
if start <= end {
for i := start; i < end; i++ {
arr << v.enc_int(i)
}
} else {
for i := start; i > end; i-- {
arr << v.enc_int(i)
}
}
v.arrays << arr
return v.mkarr(v.arrays.len - 1)
}
// str_method dispatches a string method call. The receiver was pushed before
// the arguments, so it is the first argument from the native builtin's point
// of view; delegating keeps the behavior identical to the free-function forms.
+5
View File
@@ -66,6 +66,7 @@ const op_load_dyn = u8(59) // pop idx, push stack[bp + idx]
const op_varargs = u8(60) // <named:i64> <dst:i64> — collect args[named..argc-1] into array at local dst
const op_str_method = u8(61) // <name:str> <argc:i64> — call a string method (s.len(), s.contains(x), ...)
const op_push_none = u8(62) // push the `none` sentinel
const op_in = u8(63) // membership: x in col -> 0 or 1
// native builtin ids (the operand to op_native)
const native_abs = 100
@@ -190,3 +191,7 @@ const native_input = 196
const native_flag_val = 197
const native_flag_has = 198
const native_flag_positional = 199
// structured reflection + sequence helper
const native_type_info = 200
const native_range = 201
+1
View File
@@ -121,6 +121,7 @@ fn (mut v Vm) trace_op(op u8) {
op_varargs { 'varargs' }
op_str_method { 'str_method' }
op_push_none { 'push_none' }
op_in { 'in' }
else { '??' }
}
mut s := ''
+40
View File
@@ -632,6 +632,27 @@ fn (mut v Vm) exec() ! {
op_slice {
v.op_slice()!
}
op_in {
v.ip++
col := v.pop()!
needle := v.pop()!
// map/struct: string key membership
if v.is_struct(col) && v.valid_struct_handle(col) {
if v.is_str(needle) && v.valid_handle(needle) {
key := v.strings[v.hand(needle)]
v.push(v.enc_int(if key in v.structs[v.hand(col)].by_name { 1 } else { 0 }))!
} else {
v.push(v.enc_int(0))!
}
} else if v.is_arr(col) && v.valid_arr_handle(col) {
found := v.arr_contains(v.arrays[v.hand(col)], needle)
v.push(v.enc_int(if found { 1 } else { 0 }))!
} else if v.is_str(col) && v.valid_handle(col) && v.is_str(needle) && v.valid_handle(needle) {
v.push(v.enc_int(if v.strings[v.hand(col)].contains(v.strings[v.hand(needle)]) { 1 } else { 0 }))!
} else {
v.push(v.enc_int(0))!
}
}
op_native {
v.ip++
id := int(v.read_i64())
@@ -1005,6 +1026,25 @@ fn (mut v Vm) call(target int, argc int) {
v.ip = target
}
// arr_contains reports whether an array holds a value equal to needle
// (comparing ints/floats numerically and strings by content).
fn (mut v Vm) arr_contains(a []i64, needle i64) bool {
for el in a {
if v.vals_eq(el, needle) {
return true
}
}
return false
}
// vals_eq compares two values for equality, treating int/float numerically.
fn (mut v Vm) vals_eq(x i64, y i64) bool {
if (v.is_float(x) || v.is_float(y)) && !v.is_closure(x) && !v.is_closure(y) && !v.is_str(x) && !v.is_str(y) && !v.is_arr(x) && !v.is_arr(y) && !v.is_struct(x) && !v.is_struct(y) {
return v.to_f64(x) == v.to_f64(y)
}
return x == y
}
fn (mut v Vm) ret(with_val bool) ! {
retval := if with_val { v.pop()! } else { v.enc_int(0) }
v.sp = v.bp - 1