diff --git a/README.MD b/README.MD index f1d740a..017b50d 100644 --- a/README.MD +++ b/README.MD @@ -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 ` 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 { }` diff --git a/bin/vc b/bin/vc index f7a6b02..d13e171 100755 Binary files a/bin/vc and b/bin/vc differ diff --git a/compiler/ast.v b/compiler/ast.v index bc23670..a6880a3 100644 --- a/compiler/ast.v +++ b/compiler/ast.v @@ -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. diff --git a/compiler/check.v b/compiler/check.v index 00f5054..de9f3be 100644 --- a/compiler/check.v +++ b/compiler/check.v @@ -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', diff --git a/compiler/codegen.v b/compiler/codegen.v index d3cae12..6817583 100644 --- a/compiler/codegen.v +++ b/compiler/codegen.v @@ -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 {} } } diff --git a/compiler/lexer.v b/compiler/lexer.v index a476061..d272a47 100644 --- a/compiler/lexer.v +++ b/compiler/lexer.v @@ -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 } } diff --git a/compiler/opcodes.v b/compiler/opcodes.v index ffb74ea..27ac7f7 100644 --- a/compiler/opcodes.v +++ b/compiler/opcodes.v @@ -66,6 +66,7 @@ const op_load_dyn = u8(59) // pop idx, push stack[bp + idx] const op_varargs = u8(60) // — collect args[named..argc-1] into an array at local dst const op_str_method = u8(61) // — 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 diff --git a/compiler/parser.v b/compiler/parser.v index b8960e3..472c14b 100644 --- a/compiler/parser.v +++ b/compiler/parser.v @@ -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) diff --git a/compiler/tokens.v b/compiler/tokens.v index 40938b3..4e0ddf2 100644 --- a/compiler/tokens.v +++ b/compiler/tokens.v @@ -69,6 +69,7 @@ pub enum TokKind { kw_interface kw_try kw_catch + kw_defer kw_throw } diff --git a/examples/defer_reflection.vr b/examples/defer_reflection.vr new file mode 100644 index 0000000..a81efcc --- /dev/null +++ b/examples/defer_reflection.vr @@ -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)) +} \ No newline at end of file diff --git a/examples/tests.vr b/examples/tests.vr index d18a61a..9538a9c 100644 --- a/examples/tests.vr +++ b/examples/tests.vr @@ -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 diff --git a/vm/native.v b/vm/native.v index 4e2c906..dd732de 100644 --- a/vm/native.v +++ b/vm/native.v @@ -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. diff --git a/vm/opcodes.v b/vm/opcodes.v index 565ab76..48f608b 100644 --- a/vm/opcodes.v +++ b/vm/opcodes.v @@ -66,6 +66,7 @@ const op_load_dyn = u8(59) // pop idx, push stack[bp + idx] const op_varargs = u8(60) // — collect args[named..argc-1] into array at local dst const op_str_method = u8(61) // — 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 diff --git a/vm/print.v b/vm/print.v index 874728d..3767644 100644 --- a/vm/print.v +++ b/vm/print.v @@ -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 := '' diff --git a/vm/vm.v b/vm/vm.v index 6517395..ea4d828 100644 --- a/vm/vm.v +++ b/vm/vm.v @@ -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