diff --git a/README.MD b/README.MD index ead4add..f1d740a 100644 --- a/README.MD +++ b/README.MD @@ -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 | | `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)` | -| `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: `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). ## The VuurRaaf language @@ -352,8 +350,19 @@ fn main() { - arrays: `[e1, e2, ...]`, indexing `a[i]` (read and write), `len(a)`, `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) -- error handling: `try { ... } catch e { ... }` and `throw "message"` — the - runtime unwinds to the nearest catch +- error handling: `try { ... } catch e { ... }` and `throw ` — the + runtime unwinds to the nearest catch. Throwing a struct/int/float/string works + and the caught value stringifies safely +- `String + ` 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: `& | ^ ~ << >>` - host builtins: `read_file` / `write_file`, `args()`, `getenv` / `setenv`, `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) - operators: `+ - * / %`, `== != < <= > >=`, `and or not`, `& | ^ ~ << >>`, 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, `print(...)` / `println(...)` - comments: `//` diff --git a/bin/vc b/bin/vc index f241fb6..f7a6b02 100755 Binary files a/bin/vc and b/bin/vc differ diff --git a/compiler/check.v b/compiler/check.v index 5dcbc97..00f5054 100644 --- a/compiler/check.v +++ b/compiler/check.v @@ -631,7 +631,10 @@ fn builtin_result_type(name string) TypeInfo { 'sb_len' { TypeInfo{ kind: .int_t } } 'spawn' { TypeInfo{ kind: .int_t } } '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-module builtins (.vrmm) 'build_compile', 'build_assemble', 'build_link', 'build_exec', 'build_base', diff --git a/compiler/codegen.v b/compiler/codegen.v index 139ffc0..d3cae12 100644 --- a/compiler/codegen.v +++ b/compiler/codegen.v @@ -1040,6 +1040,13 @@ fn builtin_spec(name string) (int, int) { // concurrency 'spawn' { native_spawn, 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 } 'cwd' { native_cwd, 0 } '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' } 'json_encode', 'format', 'replace', 'pad', 'pad_left', 'repeat', 'sb_new', 'sb_add', 'sb_str' { 'string' } 'sb_len' { 'int' } - 'cwd', 'json_pretty' { 'string' } + 'cwd', 'json_pretty', 'read_line', 'input' { 'string' } else { '' } } } diff --git a/compiler/opcodes.v b/compiler/opcodes.v index 32ab24e..ffb74ea 100644 --- a/compiler/opcodes.v +++ b/compiler/opcodes.v @@ -182,3 +182,12 @@ const native_sb_len = 192 // concurrency: spawn/join threads const native_spawn = 193 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 diff --git a/examples/cli_tool.vr b/examples/cli_tool.vr new file mode 100644 index 0000000..adec2d4 --- /dev/null +++ b/examples/cli_tool.vr @@ -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 +} \ No newline at end of file diff --git a/examples/tests.vr b/examples/tests.vr index 1a1199c..d18a61a 100644 --- a/examples/tests.vr +++ b/examples/tests.vr @@ -319,7 +319,7 @@ fn test_module_imports() { } assert os.base("a/b/c.vr") == "c.vr" 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") os.write_lines(tmp, ["x", "y"]) assert len(os.read_lines(tmp)) == 2 @@ -621,6 +621,43 @@ fn worker_boom() { 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() { // this one is meant to fail — shows up in `vr test` output assert 1 == 2 diff --git a/lib/cli.vr b/lib/cli.vr new file mode 100644 index 0000000..96ab923 --- /dev/null +++ b/lib/cli.vr @@ -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() +} \ No newline at end of file diff --git a/vm/native.v b/vm/native.v index a0cca38..4e2c906 100644 --- a/vm/native.v +++ b/vm/native.v @@ -684,6 +684,36 @@ fn (mut v Vm) native(id int, _argc int) ! { 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 { v.push(v.alloc_str(os.getwd()))! } @@ -1083,6 +1113,124 @@ fn (mut v Vm) pop_str() !string { 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. fn (mut v Vm) type_name(x i64) string { if v.is_str(x) { diff --git a/vm/opcodes.v b/vm/opcodes.v index 208737c..565ab76 100644 --- a/vm/opcodes.v +++ b/vm/opcodes.v @@ -181,3 +181,12 @@ const native_sb_len = 192 // concurrency: spawn/join threads const native_spawn = 193 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 diff --git a/vm/ops.v b/vm/ops.v index 082b1c2..ba6e601 100644 --- a/vm/ops.v +++ b/vm/ops.v @@ -4,6 +4,18 @@ module vm import math 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) { 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) { 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) { return v.push_float(v.to_f64(a) + v.to_f64(b)) } diff --git a/vm/types.v b/vm/types.v index 5e5b843..207869d 100644 --- a/vm/types.v +++ b/vm/types.v @@ -99,6 +99,20 @@ mut: fn_of_ip []int // code offset -> function index (for profiling) max_ops i64 // instruction budget; 0 = unlimited (fuzzing safety) 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 {