Critical fixes

This commit is contained in:
allexanderbergmns
2026-08-25 17:34:47 +02:00
parent 415dec40a2
commit 3e1e9bd08a
12 changed files with 361 additions and 19 deletions
+16 -7
View File
@@ -228,11 +228,9 @@ Module functions are namespaced: `os.exists()`, `json.encode()`, ...
| `time` | `now()` (epoch s), `ms()` (epoch ms), `format(t, "YYYY-MM-DD HH:mm:ss")`, `date(t)`, `clock(t)`, `parse("2026-08-25 13:36:45")`, `add_days/add_hours/add_minutes/add_seconds`, `weekday(t)` — Moment-style tokens | | `time` | `now()` (epoch s), `ms()` (epoch ms), `format(t, "YYYY-MM-DD HH:mm:ss")`, `date(t)`, `clock(t)`, `parse("2026-08-25 13:36:45")`, `add_days/add_hours/add_minutes/add_seconds`, `weekday(t)` — Moment-style tokens |
| `regex` | `is_match(pattern, s)`, `find_all(pattern, s)`, `replace(pattern, s, repl)`, `split(pattern, s)` — RE2 syntax; use raw strings `r"\d+"` for patterns | | `regex` | `is_match(pattern, s)`, `find_all(pattern, s)`, `replace(pattern, s, repl)`, `split(pattern, s)` — RE2 syntax; use raw strings `r"\d+"` for patterns |
| `crypto` | `sha256(s)`, `md5(s)`, `base64_encode(s)`, `base64_decode(s)` | | `crypto` | `sha256(s)`, `md5(s)`, `base64_encode(s)`, `base64_decode(s)` |
| `csv` | `parse(s)` — returns an array of rows (arrays of cell strings) | | `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/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/stdlib.vr` (tests for the regex/crypto/csv/os/time additions).
Examples: `examples/imports.vr`, `examples/http.vr`, `examples/time.vr`.
## The VuurRaaf language ## The VuurRaaf language
@@ -352,8 +350,19 @@ fn main() {
- arrays: `[e1, e2, ...]`, indexing `a[i]` (read and write), `len(a)`, - arrays: `[e1, e2, ...]`, indexing `a[i]` (read and write), `len(a)`,
`push/insert/remove/pop/sort/reverse/clone/index_of/join`; array literals `push/insert/remove/pop/sort/reverse/clone/index_of/join`; array literals
may nest; `sort` is a stable merge sort — guaranteed O(n log n) may nest; `sort` is a stable merge sort — guaranteed O(n log n)
- error handling: `try { ... } catch e { ... }` and `throw "message"` — the - error handling: `try { ... } catch e { ... }` and `throw <any value>` — the
runtime unwinds to the nearest catch runtime unwinds to the nearest catch. Throwing a struct/int/float/string works
and the caught value stringifies safely
- `String + <any value>` stringifies the other operand, so `"caught: " + e`,
`"n=" + count`, and record/array debug messages "just work". Only
`Array + Array` / `struct + struct` remain errors
- interactive input: `read_line()` reads one line from stdin (empty on EOF),
and `input(prompt)` shows a prompt on stderr then reads a line — stdout stays
clean for piping
- flag parsing: `flag_val(name)` / `flag_has(name)` / `flag_positional()` parse
`args()` getopt-style (`--name value`, `--name=value`, `--verbose`, `-n=value`,
`--` stops parsing, `-5` is a value not a flag); use `import cli` for the
namespaced `cli.flag` / `cli.has` / `cli.positional` helpers
- bitwise operators: `& | ^ ~ << >>` - bitwise operators: `& | ^ ~ << >>`
- host builtins: `read_file` / `write_file`, `args()`, `getenv` / `setenv`, - host builtins: `read_file` / `write_file`, `args()`, `getenv` / `setenv`,
`exit`, `sleep`, `time()`, `type(x)`, `str(x)`, `int(x)`, `split` / `join` `exit`, `sleep`, `time()`, `type(x)`, `str(x)`, `int(x)`, `split` / `join`
@@ -387,7 +396,7 @@ fn main() {
- constants: `const NAME = 42` (compile-time integer/bool values) - constants: `const NAME = 42` (compile-time integer/bool values)
- operators: `+ - * / %`, `== != < <= > >=`, `and or not`, `& | ^ ~ << >>`, - operators: `+ - * / %`, `== != < <= > >=`, `and or not`, `& | ^ ~ << >>`,
unary `-`; constant expressions fold at compile time unary `-`; constant expressions fold at compile time
- statements: `let`, assignment, `if/else`, `match`, `while`, `for`, - statements: `let`/`mut`, assignment, `if/else`, `match`, `while`, `for`,
`break`, `continue`, `return`, `assert`, `try/catch`/`throw`, calls, `break`, `continue`, `return`, `assert`, `try/catch`/`throw`, calls,
`print(...)` / `println(...)` `print(...)` / `println(...)`
- comments: `//` - comments: `//`
BIN
View File
Binary file not shown.
+4 -1
View File
@@ -631,7 +631,10 @@ fn builtin_result_type(name string) TypeInfo {
'sb_len' { TypeInfo{ kind: .int_t } } 'sb_len' { TypeInfo{ kind: .int_t } }
'spawn' { TypeInfo{ kind: .int_t } } 'spawn' { TypeInfo{ kind: .int_t } }
'spawn_join' { TypeInfo{ kind: .unknown } } 'spawn_join' { TypeInfo{ kind: .unknown } }
'cwd', 'json_pretty' { TypeInfo{ kind: .string_t } } 'cwd', 'json_pretty', 'read_line', 'input' { TypeInfo{ kind: .string_t } }
'flag_val' { TypeInfo{ kind: .string_t } }
'flag_has' { TypeInfo{ kind: .int_t } }
'flag_positional' { TypeInfo{ kind: .array_t } }
'build_is_dir' { TypeInfo{ kind: .int_t } } 'build_is_dir' { TypeInfo{ kind: .int_t } }
// build-module builtins (.vrmm) // build-module builtins (.vrmm)
'build_compile', 'build_assemble', 'build_link', 'build_exec', 'build_base', 'build_compile', 'build_assemble', 'build_link', 'build_exec', 'build_base',
+8 -1
View File
@@ -1040,6 +1040,13 @@ fn builtin_spec(name string) (int, int) {
// concurrency // concurrency
'spawn' { native_spawn, 1 } 'spawn' { native_spawn, 1 }
'spawn_join' { native_spawn_join, 1 } 'spawn_join' { native_spawn_join, 1 }
// interactive input
'read_line' { native_read_line, 0 }
'input' { native_input, 1 }
// getopt-style flag parsing
'flag_val' { native_flag_val, 1 }
'flag_has' { native_flag_has, 1 }
'flag_positional' { native_flag_positional, 0 }
'build_is_dir' { native_build_is_dir, 1 } 'build_is_dir' { native_build_is_dir, 1 }
'cwd' { native_cwd, 0 } 'cwd' { native_cwd, 0 }
'json_pretty' { native_json_pretty, 1 } 'json_pretty' { native_json_pretty, 1 }
@@ -1301,7 +1308,7 @@ fn (mut g Gen) expr_type(e Expr) string {
'build_dir', 'build_join', 'build_root' { 'string' } 'build_dir', 'build_join', 'build_root' { 'string' }
'json_encode', 'format', 'replace', 'pad', 'pad_left', 'repeat', 'sb_new', 'sb_add', 'sb_str' { 'string' } 'json_encode', 'format', 'replace', 'pad', 'pad_left', 'repeat', 'sb_new', 'sb_add', 'sb_str' { 'string' }
'sb_len' { 'int' } 'sb_len' { 'int' }
'cwd', 'json_pretty' { 'string' } 'cwd', 'json_pretty', 'read_line', 'input' { 'string' }
else { '' } else { '' }
} }
} }
+9
View File
@@ -182,3 +182,12 @@ const native_sb_len = 192
// concurrency: spawn/join threads // concurrency: spawn/join threads
const native_spawn = 193 const native_spawn = 193
const native_spawn_join = 194 const native_spawn_join = 194
// interactive input: read a line from stdin
const native_read_line = 195
const native_input = 196
// getopt-style flag parsing over args()
const native_flag_val = 197
const native_flag_has = 198
const native_flag_positional = 199
+60
View File
@@ -0,0 +1,60 @@
// cli_tool.vr — a miniature daily-driver CLI tool demonstrating the pieces a
// real command-line program needs: flag parsing, interactive input, and typed
// error handling.
//
// import cli
// require_name() # (see below)
//
// vr run examples/cli_tool.vr --name alice --verbose
// vr run examples/cli_tool.vr --name=alice file1 file2
//
// Exit codes: 0 on success, 1 on a runtime (bad input) error, 2 on usage/flag
// errors.
import cli
fn main() {
// --- flag parsing -------------------------------------------------------
let name = cli.flag("name", "world") // match --name/-n and --name=value
let verbose = cli.has("verbose") // match --verbose and -v
let files = cli.positional()
if verbose {
println("files=" + str(files))
}
// --- typed throw / catchable errors -----------------------------------
try {
if len(name) == 0 {
throw { kind: "usage", detail: "name must not be empty" }
}
} catch err {
println("error: " + err)
exit(2)
}
println("hello, " + name + "!")
// --- interactive input -------------------------------------------------
println("-- a quick questionnaire (pipe answers, or run interactively) --")
let age = input("your age? ")
if age == "" {
println("(no answer supplied)")
} else if int_age(age) > 0 {
println("noted (born ~" + str(current_year() - int_age(age)) + ")")
} else {
println("noted")
}
println("done.")
}fn int_age(s) {
try {
return int(s)
} catch e {
return 0
}
}
fn current_year() {
// 2026 — keep in sync; or used as a stand-in for now().year
return 2026
}
+38 -1
View File
@@ -319,7 +319,7 @@ fn test_module_imports() {
} }
assert os.base("a/b/c.vr") == "c.vr" assert os.base("a/b/c.vr") == "c.vr"
assert os.dir("a/b/c.vr") == "a/b" assert os.dir("a/b/c.vr") == "a/b"
assert len(os.glob("lib/*.vr")) == 9 assert len(os.glob("lib/*.vr")) == 10
let tmp = os.join(os.cwd(), "tmp_test.txt") let tmp = os.join(os.cwd(), "tmp_test.txt")
os.write_lines(tmp, ["x", "y"]) os.write_lines(tmp, ["x", "y"])
assert len(os.read_lines(tmp)) == 2 assert len(os.read_lines(tmp)) == 2
@@ -621,6 +621,43 @@ fn worker_boom() {
return x return x
} }
fn test_string_coercion() {
// String + any value stringifies, so error messages "just work"
let e = { code: 403, msg: "forbidden" }
assert "err: " + e == "err: {code: 403, msg: forbidden}"
assert "n=" + 42 == "n=42"
assert "f=" + 3.5 == "f=3.5"
assert "a=" + [1, 2] == "a=[1, 2]"
assert "b=" + none == "b=none"
}
fn test_typed_throw() {
// throw and catch arbitrary values (structs, ints, floats)
mut got = ""
try {
throw { reason: "boom" }
} catch err {
got = "" + err
}
assert got == "{reason: boom}"
mut gotn = -1
try {
throw 99
} catch err {
gotn = int(err)
}
assert gotn == 99
}
fn test_flag_parsing() {
// flags are read from args() at runtime; here we check the lookup helpers
// behave with no flags (defaults) — the actual parse is exercised in CI.
assert flag_val("missing") == ""
assert flag_has("missing") == 0
assert len(flag_positional()) == 0
}
fn test_failing() { fn test_failing() {
// this one is meant to fail — shows up in `vr test` output // this one is meant to fail — shows up in `vr test` output
assert 1 == 2 assert 1 == 2
+43
View File
@@ -0,0 +1,43 @@
// cli.vr — getopt-style command-line flag parsing for command-line tools.
//
// import cli
// fn main() {
// let name = cli.flag("name", "world") // --name bob | --name=bob | -n bob
// let verbose = cli.has("v") // -v (boolean flag)
// let rest = cli.positional() // non-flag args
// println("hi " + name)
// }
//
// Parsing is done by the VM (full string handling); this module exposes it as
// namespaced helpers. Supported forms:
//
// --name value long flag with a space-separated value
// --name=value long flag with an inline value
// --name long boolean flag
// -v short flag, treated as boolean (never swallows a positional)
// -n=value short flag with an inline value
//
// The short alias is the long name's first character (`--verbose` -> `-v`). To
// give a value to a short flag, use the inline form `-n=value` (or the long
// form `--name value`). `--` stops flag parsing; everything after is
// positional. The first occurrence of a flag wins. Negative numbers (-5) are
// values, not flags.
// flag returns the value of the named flag, or `default` when absent.
fn flag(name, default) {
let v = flag_val(name)
if v == "" {
return default
}
return v
}
// has returns 1 when the flag was passed (boolean or with a value), else 0.
fn has(name) {
return flag_has(name)
}
// positional returns the non-flag arguments in order; `--` ends flag parsing.
fn positional() {
return flag_positional()
}
+148
View File
@@ -684,6 +684,36 @@ fn (mut v Vm) native(id int, _argc int) ! {
native_spawn_join { native_spawn_join {
v.push(v.native_spawn_join()!)! v.push(v.native_spawn_join()!)!
} }
native_read_line {
line := os.input_opt('') or { '' } // EOF → empty string
v.push(v.alloc_str(line))!
}
native_input {
prompt := v.pop_str()!
eprint(prompt) // prompt on stderr so stdout stays clean for piping
line := os.input_opt('') or { '' }
v.push(v.alloc_str(line))!
}
native_flag_val {
name := v.pop_str()!
v.ensure_flags()
v.push(v.alloc_str(v.flag_lookup(name)))!
}
native_flag_has {
name := v.pop_str()!
v.ensure_flags()
v.push(v.enc_int(if v.flag_present_key(name) { 1 } else { 0 }))!
}
native_flag_positional {
v.ensure_flags()
mut arr := []i64{}
for s in v.flags.positionals {
v.strings << s
arr << v.mkstr(v.strings.len - 1)
}
v.arrays << arr
v.push(v.mkarr(v.arrays.len - 1))!
}
native_cwd { native_cwd {
v.push(v.alloc_str(os.getwd()))! v.push(v.alloc_str(os.getwd()))!
} }
@@ -1083,6 +1113,124 @@ fn (mut v Vm) pop_str() !string {
return v.strings[v.hand(x)] return v.strings[v.hand(x)]
} }
// --------------------------------------------------------------------------
// getopt-style flag parsing over the program's args.
//
// Flags are parsed once (lazily) into v.flags, and every flag / positional
// query reads from that shared result, so a token consumed as a flag value is
// never also reported as a positional. Supported forms: --name value,
// --name=value, boolean --name, single-letter short aliases (-n) derived from
// the long name's first character, and `--` to stop flag parsing. Negative
// numbers like -5 are treated as values, not flags. The first occurrence of a
// flag wins.
fn (mut v Vm) ensure_flags() {
if v.flags.parsed {
return
}
v.flags = FlagArgs{ parsed: true }
mut consumed_value := false // the current flag took a separate value token
mut i := 0
for i < v.prog_args.len {
a := v.prog_args[i]
if a == '--' {
// everything after -- is positional
v.flags.positionals << v.prog_args[i + 1..]
break
}
if !v.flag_token(a) {
// positional, unless it was already consumed as a flag's value below
v.flags.positionals << a
i++
continue
}
// a flag token. Only long flags (--name) consume a separate value token;
// short flags (-v) are treated as boolean to avoid swallowing a following
// positional (use -n=val or --name val for short aliases needing a value).
name, val, has_inline := v.flag_key_val(a)
long_flag := a.starts_with('--')
if name !in v.flags.vals {
if has_inline {
v.flags.vals[name] = val
i++
continue
}
if long_flag && i + 1 < v.prog_args.len && !v.flag_token(v.prog_args[i + 1]) {
v.flags.vals[name] = v.prog_args[i + 1]
i += 2
} else {
v.flags.vals[name] = 'true'
i++
}
continue
}
// duplicate flag: keep the first value, but still consume its value
if long_flag && !has_inline && i + 1 < v.prog_args.len && !v.flag_token(v.prog_args[i + 1]) {
i += 2
} else {
i++
}
}
}
// flag_token reports whether a is parsed as a flag rather than a value.
// flag_lookup returns the value of flag `name`, accepting its single-letter
// short alias as well. Returns '' when the flag was not passed.
fn (mut v Vm) flag_lookup(name string) string {
if name in v.flags.vals {
return v.flags.vals[name]
}
if name.len > 1 {
short := name[..1]
if short in v.flags.vals {
return v.flags.vals[short]
}
}
return ''
}
// flag_present_key reports whether flag `name` (or its short alias) was passed
// at all, regardless of its value.
fn (mut v Vm) flag_present_key(name string) bool {
if name in v.flags.vals {
return true
}
if name.len > 1 {
return name[..1] in v.flags.vals
}
return false
}
fn (mut v Vm) flag_token(a string) bool {
if a == '-' {
return false
}
if !a.starts_with('-') {
return false
}
// a negative number (-5, -3.14) is a value, not a flag
if a.len > 1 && a[1] >= `0` && a[1] <= `9` {
return false
}
return true
}
// flag_key_val splits a flag token into its canonical (dash-stripped) name and
// any inline value. Returns (name, value, has_inline_value).
fn (mut v Vm) flag_key_val(a string) (string, string, bool) {
raw := a.trim_left('-') // strip leading dashes
mut eq := -1
for i in 0..raw.len {
if raw[i] == `=` {
eq = i
break
}
}
if eq >= 0 {
return raw[..eq], raw[eq + 1..], true
}
return raw, '', false
}
// type_name returns the type label of a tagged value. // type_name returns the type label of a tagged value.
fn (mut v Vm) type_name(x i64) string { fn (mut v Vm) type_name(x i64) string {
if v.is_str(x) { if v.is_str(x) {
+9
View File
@@ -181,3 +181,12 @@ const native_sb_len = 192
// concurrency: spawn/join threads // concurrency: spawn/join threads
const native_spawn = 193 const native_spawn = 193
const native_spawn_join = 194 const native_spawn_join = 194
// interactive input: read a line from stdin
const native_read_line = 195
const native_input = 196
// getopt-style flag parsing over args()
const native_flag_val = 197
const native_flag_has = 198
const native_flag_positional = 199
+12 -9
View File
@@ -4,6 +4,18 @@ module vm
import math import math
fn (mut v Vm) add(a i64, b i64) !i64 { fn (mut v Vm) add(a i64, b i64) !i64 {
// String + any: coerce the other operand to its string form so that
// `"caught: " + e`, `"n=" + count`, and record/array debugging "just
// work" the way users expect when building messages.
if v.is_str(a) && !v.is_str(b) {
return v.alloc_str(v.strings[v.hand(a)] + v.val_str(b, 0))
}
if v.is_str(b) && !v.is_str(a) {
return v.alloc_str(v.val_str(a, 0) + v.strings[v.hand(b)])
}
if v.is_str(a) && v.is_str(b) {
return v.alloc_str(v.strings[v.hand(a)] + v.strings[v.hand(b)])
}
if v.is_none(a) || v.is_none(b) { if v.is_none(a) || v.is_none(b) {
return error('cannot use none with +') return error('cannot use none with +')
} }
@@ -13,15 +25,6 @@ fn (mut v Vm) add(a i64, b i64) !i64 {
if v.is_struct(a) || v.is_struct(b) { if v.is_struct(a) || v.is_struct(b) {
return error('cannot add structs with +') 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)])
}
if v.is_str(a) {
return v.alloc_str(v.strings[v.hand(a)] + v.num_str(b))
}
if v.is_str(b) {
return v.alloc_str(v.num_str(a) + v.strings[v.hand(b)])
}
if v.is_float(a) || v.is_float(b) { if v.is_float(a) || v.is_float(b) {
return v.push_float(v.to_f64(a) + v.to_f64(b)) return v.push_float(v.to_f64(a) + v.to_f64(b))
} }
+14
View File
@@ -99,6 +99,20 @@ mut:
fn_of_ip []int // code offset -> function index (for profiling) fn_of_ip []int // code offset -> function index (for profiling)
max_ops i64 // instruction budget; 0 = unlimited (fuzzing safety) max_ops i64 // instruction budget; 0 = unlimited (fuzzing safety)
ops i64 // instructions executed so far ops i64 // instructions executed so far
flags FlagArgs // cached getopt parse of prog_args (lazy)
}
// FlagArgs is the parsed view of the command-line arguments, computed lazily
// and shared by flag_val / flag_has / flag_positional so that a value consumed
// as a flag value is never also returned as a positional.
struct FlagArgs {
// parsed indicates the parse is only valid when built against the same
// argv length as prog_args currently has (args never change at runtime).
mut:
parsed bool
vals map[string]string // canonical flag name -> value ('true' for boolean w/o inline)
ordered []string // flag names in first-appearance order
positionals []string // non-flag arguments not consumed as flag values
} }
fn bool_i64(b bool) i64 { fn bool_i64(b bool) i64 {