More buildins and vscode exstention <CO-AUthored, ai>

This commit is contained in:
allexanderbergmns
2026-08-25 16:20:11 +02:00
parent 5daadce7a9
commit 7ed774089e
27 changed files with 2080 additions and 122 deletions
+13
View File
@@ -64,3 +64,16 @@ jobs:
grep -n 'fn test_failing' examples/tests.vr || true grep -n 'fn test_failing' examples/tests.vr || true
sed '/fn test_failing/,/^}/d' examples/tests.vr > /tmp/ci_tests.vr sed '/fn test_failing/,/^}/d' examples/tests.vr > /tmp/ci_tests.vr
./bin/vr test /tmp/ci_tests.vr ./bin/vr test /tmp/ci_tests.vr
- name: Run stdlib test suite
run: |
./bin/vr test examples/stdlib.vr
- name: LSP smoke test
run: |
BODY='{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}'
printf 'Content-Length: %d\r\n\r\n%s' "${#BODY}" "$BODY" | ./bin/vr lsp | grep -q '"id":1' && echo "LSP responded"
- name: Watch-mode and directory-test smoke
run: |
./bin/vr test examples | grep -q 'passed'
+19 -3
View File
@@ -19,6 +19,9 @@ jobs:
- os: macos-latest - os: macos-latest
artifact: vr-macos-arm64 artifact: vr-macos-arm64
binary: vr binary: vr
- os: windows-latest
artifact: vr-windows-amd64.exe
binary: vr.exe
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
@@ -26,17 +29,30 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Install V - name: Install V (unix)
if: runner.os != 'Windows'
shell: bash
run: | run: |
git clone --depth 1 https://github.com/vlang/v /tmp/v git clone --depth 1 https://github.com/vlang/v /tmp/v
cd /tmp/v && make cd /tmp/v && make
sudo ln -sf /tmp/v/v /usr/local/bin/v sudo ln -sf /tmp/v/v /usr/local/bin/v
v version v version
- name: Install V (windows)
if: runner.os == 'Windows'
shell: bash
run: |
curl -L -o v_windows.zip https://github.com/vlang/v/releases/latest/download/v_windows.zip
unzip -o v_windows.zip -d /tmp/vwin
echo "/tmp/vwin/v" >> "$GITHUB_PATH"
v version
- name: Build toolchain - name: Build toolchain
shell: bash
run: v -o ${{ matrix.binary }} . run: v -o ${{ matrix.binary }} .
- name: Smoke test - name: Smoke test
shell: bash
run: ./${{ matrix.binary }} run examples/hello.vr run: ./${{ matrix.binary }} run examples/hello.vr
- name: Upload artifact - name: Upload artifact
@@ -57,7 +73,7 @@ jobs:
- name: Verify tag matches v.mod version - name: Verify tag matches v.mod version
run: | run: |
TAG="${GITHUB_REF#refs/tags/v}" TAG="${GITHUB_REF#refs/tags/v}"
MOD_VERSION=$(grep -oP "version: '\K[^"]+" v.mod) MOD_VERSION=$(grep -oP "version: '\\K[^\"]+" v.mod)
if [ "$TAG" != "$MOD_VERSION" ]; then if [ "$TAG" != "$MOD_VERSION" ]; then
echo "::error::Tag version (v${TAG}) does not match v.mod version (${MOD_VERSION})" echo "::error::Tag version (v${TAG}) does not match v.mod version (${MOD_VERSION})"
exit 1 exit 1
@@ -74,7 +90,7 @@ jobs:
mkdir -p release mkdir -p release
for dir in artifacts/*/; do for dir in artifacts/*/; do
name=$(basename "$dir") name=$(basename "$dir")
cp "$dir"/vr release/"$name" cp "$dir"/* release/"$name"
chmod +x release/"$name" chmod +x release/"$name"
done done
ls -lh release/ ls -lh release/
+42 -7
View File
@@ -41,6 +41,7 @@ vr up rebuild bin/vr
vr symlink link bin/vr into your PATH vr symlink link bin/vr into your PATH
vr config [set <key> <value>] toolchain config (outdir, verbose) vr config [set <key> <value>] toolchain config (outdir, verbose)
vr repl interactive session vr repl interactive session
vr lsp language server (JSON-RPC over stdio)
vr fmt [-w] <file.vr> format source (keeps comments) vr fmt [-w] <file.vr> format source (keeps comments)
vr init [name] scaffold a project (vr.mod + main.vr) vr init [name] scaffold a project (vr.mod + main.vr)
vr get <owner/repo | git-url | ./path> fetch a package into vendor/ vr get <owner/repo | git-url | ./path> fetch a package into vendor/
@@ -53,6 +54,9 @@ vr info | loader | alloc | version | help
./bin/vr repl # try expressions and functions interactively ./bin/vr repl # try expressions and functions interactively
./bin/vr fmt -w f.vr # normalize a file's indentation/spacing in place ./bin/vr fmt -w f.vr # normalize a file's indentation/spacing in place
./bin/vr init myproj # start a project; vr get owner/repo fetches packages ./bin/vr init myproj # start a project; vr get owner/repo fetches packages
./bin/vr run -w f.vr # watch f.vr and rerun on every save
./bin/vr test examples # run every test_* fn in every .vr file under examples/
./bin/vr lsp # language server for editors (diagnostics, go-to-def)
``` ```
Quick start: Quick start:
@@ -165,12 +169,17 @@ Module functions are namespaced: `os.exists()`, `json.encode()`, ...
| module | functions | | module | functions |
| --- | --- | | --- | --- |
| `os` | filesystem + process: `exists`, `is_dir`, `is_file`, `mkdir`, `remove`, `copy`, `list_dir`, `glob`, `join`, `base`, `dir`, `read_lines`, `write_lines`, `cwd`, `env`, `exec` | | `os` | filesystem + process: `exists`, `is_dir`, `is_file`, `mkdir`, `remove`, `copy`, `list_dir`, `glob`, `join`, `base`, `dir`, `read_lines`, `write_lines`, `cwd`, `env`, `exec`, `exec_full` (`{code, stdout, stderr}`), `ext`, `abs`, `rel` |
| `json` | `encode`, `decode`, `pretty` — objects become structs, integral numbers decode as ints, `null``none` | | `json` | `encode`, `decode`, `pretty` — objects become structs, integral numbers decode as ints, `null``none` |
| `strings` | `length`, `lines`, `split`, `join`, `replace`, `contains`, `starts_with`, `ends_with`, `upper`, `lower`, `trim`, `pad`, `pad_left`, `repeat`, `format`, `capitalize` | | `strings` | `length`, `lines`, `split`, `join`, `replace`, `contains`, `starts_with`, `ends_with`, `upper`, `lower`, `trim`, `pad`, `pad_left`, `repeat`, `format`, `capitalize` |
| `math` | `abs`, `min`, `max`, `floor`, `ceil`, `round`, `sqrt`, `pow`, `clamp`, `sign`, `pi` | | `math` | `abs`, `min`, `max`, `floor`, `ceil`, `round`, `sqrt`, `pow`, `clamp`, `sign`, `pi` |
| `http` | `get(url)`, `post(url, data)`, `get_text(url)` — return `{status, body}`; network failures throw (catchable), HTTP errors (404) are normal responses | | `http` | `get(url)`, `post(url, data)`, `get_text(url)`, `request(method, url, data, headers, timeout_ms)`, `get_with_headers`, `post_with_headers`, `get_timeout`, `post_timeout` — return `{status, body}`; network failures throw (catchable), HTTP errors (404) are normal responses |
| `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")` — 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 |
| `crypto` | `sha256(s)`, `md5(s)`, `base64_encode(s)`, `base64_decode(s)` |
| `csv` | `parse(s)` — returns an array of rows (arrays of cell strings) |
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`.
@@ -257,7 +266,12 @@ fn main() {
- functions: `fn name(a, b) { ... }` with `return expr`; default parameter - functions: `fn name(a, b) { ... }` with `return expr`; default parameter
values `fn f(a, b = 10)`, variadic params `fn f(nums...)`, destructuring values `fn f(a, b = 10)`, variadic params `fn f(nums...)`, destructuring
`let { a, b } = rec` and `let [x, y] = arr`, and anonymous closures `let { a, b } = rec` and `let [x, y] = arr`, and anonymous closures
`let f = fn(x) { return x * 2 }` stored in variables and arrays `let f = fn(x) { return x * 2 }` stored in variables and arrays; a closure
**captures the enclosing locals it references by value** (nested closures
included), so `let n = 1; let bump = fn() { n = n + 1; return n }` works —
writes hit the closure's own copy, the outer variable is untouched
- raw strings: `r"..."` passes text through verbatim (no escape processing),
ideal for regex patterns like `r"\d+"`
- generics: `fn first[T](arr) { return arr[0] }` with checked call sites - generics: `fn first[T](arr) { return arr[0] }` with checked call sites
`first[int](arr)` — the VM is dynamically typed, so type parameters erase `first[int](arr)` — the VM is dynamically typed, so type parameters erase
to a single function but arity and duplicates are validated to a single function but arity and duplicates are validated
@@ -269,6 +283,9 @@ fn main() {
and slice by character (runes), and methods like `s.to_upper()`, and slice by character (runes), and methods like `s.to_upper()`,
`s.contains(x)`, `s.split(d)`, `s.index_of(x)`, `s.to_int()`, `s.len()` `s.contains(x)`, `s.split(d)`, `s.index_of(x)`, `s.to_int()`, `s.len()`
work on any string-valued expression work on any string-valued expression
- maps: structs serve as string-keyed maps with **hash-indexed O(1)** access;
keys may be computed expressions (`m[key_var] = v`, `m[key_var]`), not just
literals; `has(m, k)`, `delete(m, k)`, `keys(m)`, `len(m)` all work
- 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 may nest
@@ -355,7 +372,7 @@ field name as a string first, exactly like the compiler does).
| `compiler/` | lexer, parser, type checker, bytecode codegen (VROBJ) | | `compiler/` | lexer, parser, type checker, bytecode codegen (VROBJ) |
| `assembler/` | `.vasm` -> VROBJ | | `assembler/` | `.vasm` -> VROBJ |
| `linker/` | resolves relocations, rebases strings, emits VRBIN | | `linker/` | resolves relocations, rebases strings, emits VRBIN |
| `vm/` | stack VM: tagged values, call frames, string/array heaps | | `vm/` | stack VM: tagged values, call frames, string/array heaps, hash-indexed structs, GC-traced closure captures |
| `obj/` | VROBJ/VRBIN binary formats | | `obj/` | VROBJ/VRBIN binary formats |
| `bin/` | small standalone tools: `tl_alloc.v`, `tl_loader.v` | | `bin/` | small standalone tools: `tl_alloc.v`, `tl_loader.v` |
@@ -364,13 +381,31 @@ ints, string/array/struct/float/closure handles — so no integer ever
collides with a heap handle. A mark-and-sweep garbage collector runs between collides with a heap handle. A mark-and-sweep garbage collector runs between
opcodes when the heap grows past a threshold, tracing the stack (which holds opcodes when the heap grows past a threshold, tracing the stack (which holds
every frame's locals) and compacting the pools; string constants baked into every frame's locals) and compacting the pools; string constants baked into
bytecode are never collected. Bytecode carries a line table, so runtime bytecode are never collected. Structs carry a hash index, so map-style field
errors report the source line. A conservative compile-time type checker access is O(1); closure captures live inside the closure and are traced and
remapped by the GC like any other heap value. Bytecode carries a line table,
so runtime errors report the source line **and a full call-stack trace**
(`fn` → line for every frame). A conservative compile-time type checker
(`compiler/check.v`) rejects provably wrong programs (unknown variables, (`compiler/check.v`) rejects provably wrong programs (unknown variables,
field access on numbers, arithmetic on strings, wrong arity) while leaving field access on numbers, arithmetic on strings, wrong arity) while leaving
dynamic programs alone. `vr debug` prints every instruction with the stack dynamic programs alone. `vr debug` prints every instruction with the stack
contents (arrays rendered as `[1, 2, ...]`). contents (arrays rendered as `[1, 2, ...]`).
## Editor support
`vr lsp` speaks LSP over stdio, so any LSP-capable editor gets diagnostics
(the compiler's errors, positioned at the right line), go-to-definition
(functions, structs, enums, constants, locals, and stdlib module functions),
and hover (symbol kind + line). The `extension/` folder contains a VS Code
client (`code --install-extension extension/` after `npm install` in
`extension/`) with syntax highlighting, or point any other LSP client at
`vr lsp`.
## Releases
Tagging `vX.Y.Z` (matching `v.mod`) builds `vr` for Linux, macOS and
Windows and attaches them to a GitHub Release.
## Repository layout ## Repository layout
``` ```
+10 -1
View File
@@ -613,7 +613,16 @@ fn builtin_result_type(name string) TypeInfo {
'http_get', 'http_post' { TypeInfo{ kind: .struct_t } } 'http_get', 'http_post' { TypeInfo{ kind: .struct_t } }
// date/time // date/time
'now', 'time_ms', 'parse_time' { TypeInfo{ kind: .int_t } } 'now', 'time_ms', 'parse_time' { TypeInfo{ kind: .int_t } }
'format_time' { TypeInfo{ kind: .string_t } } 'format_time', 'weekday' { TypeInfo{ kind: .string_t } }
// regex
'regex_match' { TypeInfo{ kind: .int_t } }
'regex_find_all', 'regex_split', 'csv_parse' { TypeInfo{ kind: .array_t } }
'regex_replace' { TypeInfo{ kind: .string_t } }
// crypto/encoding
'base64_encode', 'base64_decode', 'sha256', 'md5' { TypeInfo{ kind: .string_t } }
// extended HTTP + path/process helpers
'http_req', 'exec_full' { TypeInfo{ kind: .struct_t } }
'path_ext', 'path_abs', 'path_rel' { TypeInfo{ kind: .string_t } }
else { TypeInfo{ kind: .unknown } } else { TypeInfo{ kind: .unknown } }
} }
} }
+262 -2
View File
@@ -44,6 +44,7 @@ mut:
enter_off u32 enter_off u32
next_lbl int next_lbl int
modules map[string]bool // imported module names (bare `import os`) modules map[string]bool // imported module names (bare `import os`)
captures []string // enclosing locals captured by the closure being compiled
} }
fn gen(prog Program) !obj.Obj { fn gen(prog Program) !obj.Obj {
@@ -121,6 +122,7 @@ fn gen(prog Program) !obj.Obj {
} }
} }
for fd in prog.fns { for fd in prog.fns {
g.captures = []string{} // top-level functions capture nothing
g.gen_fn(fd)! g.gen_fn(fd)!
} }
return obj.Obj{ return obj.Obj{
@@ -142,9 +144,16 @@ fn (mut g Gen) gen_fn(fd FnDecl) ! {
g.locals.clear() g.locals.clear()
g.types.clear() g.types.clear()
g.local_cnt = 0 g.local_cnt = 0
g.argc = fd.params.len + if fd.recv_type.len > 0 { 1 } else { 0 } // closure captures occupy the leading local slots (filled by the caller's
// op_call_closure), then the receiver (methods), then the parameters
g.argc = fd.params.len + g.captures.len + if fd.recv_type.len > 0 { 1 } else { 0 }
mut next := 0 mut next := 0
if fd.recv_type.len > 0 { if g.captures.len > 0 {
for i, c in g.captures {
g.locals[c] = i
}
next = g.captures.len
} else if fd.recv_type.len > 0 {
g.locals[fd.recv_name] = 0 g.locals[fd.recv_name] = 0
g.types[fd.recv_name] = fd.recv_type g.types[fd.recv_name] = fd.recv_type
next = 1 next = 1
@@ -711,6 +720,9 @@ fn (mut g Gen) gen_expr(e Expr) ! {
.anon_fn { .anon_fn {
g.lam_counter++ g.lam_counter++
name := '__lam_${g.lam_counter}' name := '__lam_${g.lam_counter}'
// find the enclosing locals the body references (its free
// variables); they become this closure's captures
caps := g.scan_captures(e.fn_body, e.fparams)
// jump over the lambda body so callers don't fall through // jump over the lambda body so callers don't fall through
g.code << op_jmp g.code << op_jmp
g.code << obj.encode_i64(0) g.code << obj.encode_i64(0)
@@ -735,8 +747,10 @@ fn (mut g Gen) gen_expr(e Expr) ! {
saved_local_cnt := g.local_cnt saved_local_cnt := g.local_cnt
saved_enter_off := g.enter_off saved_enter_off := g.enter_off
saved_argc := g.argc saved_argc := g.argc
saved_captures := g.captures
g.labels.clear() g.labels.clear()
g.fixups = []Fixup{} g.fixups = []Fixup{}
g.captures = caps
g.gen_fn(fd)! g.gen_fn(fd)!
// Restore the enclosing state. // Restore the enclosing state.
g.fixups = saved_fixups g.fixups = saved_fixups
@@ -746,12 +760,18 @@ fn (mut g Gen) gen_expr(e Expr) ! {
g.local_cnt = saved_local_cnt g.local_cnt = saved_local_cnt
g.enter_off = saved_enter_off g.enter_off = saved_enter_off
g.argc = saved_argc g.argc = saved_argc
g.captures = saved_captures
// Patch the skip jump to land at the closure opcode we emit next // Patch the skip jump to land at the closure opcode we emit next
// (PC-relative, like all other jump targets). // (PC-relative, like all other jump targets).
obj.patch_i64(mut g.code, skip_fix_off, i64(g.code.len - (int(skip_fix_off) + 8))) obj.patch_i64(mut g.code, skip_fix_off, i64(g.code.len - (int(skip_fix_off) + 8)))
// capture the enclosing locals' current values (capture by value)
for cname in caps {
g.emit_load(g.locals[cname])
}
g.code << op_closure g.code << op_closure
g.code << obj.encode_i64(0) g.code << obj.encode_i64(0)
g.relocs << obj.Reloc{ offset: u32(g.code.len) - 8, name: name, kind: 0 } g.relocs << obj.Reloc{ offset: u32(g.code.len) - 8, name: name, kind: 0 }
g.code << obj.encode_i64(i64(caps.len))
} }
.bool_lit { .bool_lit {
g.code << op_push_i g.code << op_push_i
@@ -982,6 +1002,24 @@ fn builtin_spec(name string) (int, int) {
'time_ms' { native_time_ms, 0 } 'time_ms' { native_time_ms, 0 }
'format_time' { native_format_time, 2 } 'format_time' { native_format_time, 2 }
'parse_time' { native_parse_time, 1 } 'parse_time' { native_parse_time, 1 }
'weekday' { native_weekday, 1 }
// regex
'regex_match' { native_regex_match, 2 }
'regex_find_all' { native_regex_find_all, 2 }
'regex_replace' { native_regex_replace, 3 }
'regex_split' { native_regex_split, 2 }
// crypto/encoding
'base64_encode' { native_base64_encode, 1 }
'base64_decode' { native_base64_decode, 1 }
'sha256' { native_sha256, 1 }
'md5' { native_md5, 1 }
'csv_parse' { native_csv_parse, 1 }
// extended HTTP + path/process helpers
'http_req' { native_http_req, 5 }
'path_ext' { native_path_ext, 1 }
'path_abs' { native_path_abs, 1 }
'path_rel' { native_path_rel, 2 }
'exec_full' { native_exec_full, 1 }
else { -1, 0 } else { -1, 0 }
} }
} }
@@ -1377,3 +1415,225 @@ fn (mut g Gen) new_label() string {
fn (mut g Gen) emit_label(name string) { fn (mut g Gen) emit_label(name string) {
g.labels[name] = g.code.len g.labels[name] = g.code.len
} }
// ---------------------------------------------------------------------------
// closure capture analysis
//
// A closure (anonymous `fn`) may reference the enclosing function's locals.
// Since the VM compiles each function with its own frame, those references
// are resolved by capturing the values at closure-creation time: the compiler
// scans the body for free variables, registers them as the lambda's leading
// local slots, and emits loads of their current values before op_closure.
// scan_captures returns the enclosing locals a closure body references, in
// first-reference order (stable and deterministic for codegen).
fn (mut g Gen) scan_captures(body []Stmt, fparams []string) []string {
mut bound := map[string]bool{}
for p in fparams {
bound[p] = true
}
mut caps := []string{}
mut seen := map[string]bool{}
for st in body {
g.scan_stmt(st, mut bound, mut caps, mut seen)
}
return caps
}
fn (mut g Gen) maybe_capture(name string, bound map[string]bool, mut caps []string, mut seen map[string]bool) {
if name in bound {
return // bound inside the closure — a plain local
}
if name !in g.locals {
return // not an enclosing local (global fn/const/enum — resolved elsewhere)
}
if name !in seen {
seen[name] = true
caps << name
}
}
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 {
g.scan_expr(st.expr, mut bound, mut caps, mut seen)
}
.let_stmt {
g.scan_expr(st.expr, mut bound, mut caps, mut seen)
bound[st.target] = true
}
.destruct_stmt {
g.scan_expr(st.expr, mut bound, mut caps, mut seen)
for t in st.destruct_targets {
bound[t] = true
}
}
.assign_stmt {
// assignment to a name that is not a closure-local references the
// enclosing local's captured copy
g.maybe_capture(st.target, bound, mut caps, mut seen)
bound[st.target] = true
g.scan_expr(st.expr, mut bound, mut caps, mut seen)
}
.index_assign {
g.scan_expr(st.base, mut bound, mut caps, mut seen)
g.scan_expr(st.idx, mut bound, mut caps, mut seen)
g.scan_expr(st.expr, mut bound, mut caps, mut seen)
}
.field_assign {
g.scan_expr(st.base, mut bound, mut caps, mut seen)
g.scan_expr(st.expr, mut bound, mut caps, mut seen)
}
.if_stmt {
g.scan_expr(st.cond, mut bound, mut caps, mut seen)
for s in st.body {
g.scan_stmt(s, mut bound, mut caps, mut seen)
}
for s in st.els {
g.scan_stmt(s, mut bound, mut caps, mut seen)
}
}
.match_stmt {
g.scan_expr(st.expr, mut bound, mut caps, mut seen)
for arm in st.arms {
g.scan_expr(arm.val, mut bound, mut caps, mut seen)
for s in arm.body {
g.scan_stmt(s, mut bound, mut caps, mut seen)
}
}
for s in st.els_body {
g.scan_stmt(s, mut bound, mut caps, mut seen)
}
}
.while_stmt {
g.scan_expr(st.cond, mut bound, mut caps, mut seen)
for s in st.body {
g.scan_stmt(s, mut bound, mut caps, mut seen)
}
}
.for_range_stmt {
g.scan_expr(st.expr, mut bound, mut caps, mut seen)
g.scan_expr(st.cond, mut bound, mut caps, mut seen)
had := st.target in bound
bound[st.target] = true
for s in st.body {
g.scan_stmt(s, mut bound, mut caps, mut seen)
}
if !had {
bound.delete(st.target)
}
}
.for_in_stmt {
g.scan_expr(st.expr, mut bound, mut caps, mut seen)
had := st.target in bound
bound[st.target] = true
mut had_idx := false
if st.idx_target.len > 0 {
had_idx = st.idx_target in bound
bound[st.idx_target] = true
}
for s in st.body {
g.scan_stmt(s, mut bound, mut caps, mut seen)
}
if !had {
bound.delete(st.target)
}
if st.idx_target.len > 0 && !had_idx {
bound.delete(st.idx_target)
}
}
.ret_stmt {
if st.has_val {
g.scan_expr(st.expr, mut bound, mut caps, mut seen)
}
}
.assert_stmt {
g.scan_expr(st.expr, mut bound, mut caps, mut seen)
}
.try_stmt {
for s in st.body {
g.scan_stmt(s, mut bound, mut caps, mut seen)
}
had := st.target in bound
bound[st.target] = true
for s in st.els {
g.scan_stmt(s, mut bound, mut caps, mut seen)
}
if !had {
bound.delete(st.target)
}
}
.throw_stmt {
g.scan_expr(st.expr, mut bound, mut caps, mut seen)
}
.break_stmt, .continue_stmt {}
}
}
fn (mut g Gen) scan_expr(e Expr, mut bound map[string]bool, mut caps []string, mut seen map[string]bool) {
match e.kind {
.ident {
g.maybe_capture(e.name, bound, mut caps, mut seen)
}
.call {
// a call to an enclosing local holding a closure must capture it too
g.maybe_capture(e.name, bound, mut caps, mut seen)
for a in e.args {
g.scan_expr(a, mut bound, mut caps, mut seen)
}
}
.field {
g.scan_expr(*e.left, mut bound, mut caps, mut seen)
}
.method_call {
g.scan_expr(*e.left, mut bound, mut caps, mut seen)
for a in e.args {
g.scan_expr(a, mut bound, mut caps, mut seen)
}
}
.index {
g.scan_expr(*e.left, mut bound, mut caps, mut seen)
g.scan_expr(*e.right, mut bound, mut caps, mut seen)
}
.slice {
g.scan_expr(*e.left, mut bound, mut caps, mut seen)
g.scan_expr(*e.right, mut bound, mut caps, mut seen)
g.scan_expr(*e.extra, mut bound, mut caps, mut seen)
}
.unary {
g.scan_expr(*e.right, mut bound, mut caps, mut seen)
}
.binary {
g.scan_expr(*e.left, mut bound, mut caps, mut seen)
g.scan_expr(*e.right, mut bound, mut caps, mut seen)
}
.array_lit {
for el in e.elems {
g.scan_expr(el, mut bound, mut caps, mut seen)
}
}
.struct_lit {
for f in e.fields {
g.scan_expr(f.val, mut bound, mut caps, mut seen)
}
}
.anon_fn {
// a nested closure: its parameters bind inside it, but references to
// enclosing locals still belong to this closure's capture set
mut saved := map[string]bool{}
for p in e.fparams {
saved[p] = p in bound
bound[p] = true
}
for s in e.fn_body {
g.scan_stmt(s, mut bound, mut caps, mut seen)
}
for p in e.fparams {
if !saved[p] {
bound.delete(p)
}
}
}
else {}
}
}
+20
View File
@@ -220,6 +220,11 @@ fn (mut l Lexer) next() !Tok {
return l.lex_number(line, col) return l.lex_number(line, col)
} }
else { else {
// r"..." raw strings (no escape processing) — handy for regex
// patterns like r"\d+" that would otherwise need double escaping
if c == `r` && l.peek2() == `\"` {
return l.lex_raw_string(line, col)!
}
if (c >= `a` && c <= `z`) || (c >= `A` && c <= `Z`) || c == `_` { if (c >= `a` && c <= `z`) || (c >= `A` && c <= `Z`) || c == `_` {
return l.lex_ident(line, col) return l.lex_ident(line, col)
} }
@@ -310,6 +315,21 @@ fn (mut l Lexer) lex_ident(line int, col int) Tok {
return Tok{ kind: kind, lit: lit, line: line, col: col } return Tok{ kind: kind, lit: lit, line: line, col: col }
} }
// lex_raw_string reads an r"..." string verbatim: backslashes, quotes and
// ${...} sequences are all kept literally, so regex patterns pass through
// untouched. The token is a plain str_lit whose content is the raw text.
fn (mut l Lexer) lex_raw_string(line int, col int) !Tok {
l.advance() // 'r'
l.advance() // opening quote
start := l.pos
for l.pos < l.src.len {
if l.advance() == `\"` {
return Tok{ kind: .str_lit, lit: l.src[start..l.pos - 1], line: line, col: col }
}
}
return error('unterminated raw string at line ${line}, col ${col}')
}
fn (mut l Lexer) lex_string(line int, col int) !Tok { fn (mut l Lexer) lex_string(line int, col int) !Tok {
l.advance() // opening quote l.advance() // opening quote
mut s := '' mut s := ''
+21
View File
@@ -151,3 +151,24 @@ const native_now = 170
const native_time_ms = 171 const native_time_ms = 171
const native_format_time = 172 const native_format_time = 172
const native_parse_time = 173 const native_parse_time = 173
// regex builtins
const native_regex_match = 174
const native_regex_find_all = 175
const native_regex_replace = 176
const native_regex_split = 177
// crypto/encoding builtins
const native_base64_encode = 178
const native_base64_decode = 179
const native_sha256 = 180
const native_md5 = 181
const native_csv_parse = 182
// extended HTTP + filesystem/process builtins
const native_http_req = 183
const native_path_ext = 184
const native_path_abs = 185
const native_path_rel = 186
const native_exec_full = 187
const native_weekday = 188
+102
View File
@@ -0,0 +1,102 @@
// stdlib.vr — tests for the extended stdlib modules (regex, crypto, csv,
// path/process helpers, date arithmetic). Run with `vr test examples/stdlib.vr`.
import regex
import crypto
import csv
import os
import time
import http
fn test_regex() {
assert regex.is_match(r"^[a-z]+$", "hello") == 1
assert regex.is_match(r"^[a-z]+$", "Hello1") == 0
let nums = regex.find_all(r"\d+", "a1b22c333")
assert len(nums) == 3
assert nums[0] == "1"
assert nums[2] == "333"
assert regex.replace(r"\d+", "x1y22z", "#") == "x#y#z"
let parts = regex.split(r"[,;]", "a,b;c")
assert len(parts) == 3
assert parts[1] == "b"
// replacing a match in context (no group refs — use find_all for captures)
assert regex.replace(r"\d+", "id:42 name:bob", "#") == "id:# name:bob"
// invalid pattern throws (catchable)
try {
regex.is_match("([", "x")
assert false
} catch e {
assert len(e) > 0
}
}
fn test_crypto() {
assert crypto.sha256("hello") == "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
assert crypto.md5("hello") == "5d41402abc4b2a76b9719d911017c592"
let enc = crypto.base64_encode("hello")
assert enc == "aGVsbG8="
assert crypto.base64_decode(enc) == "hello"
// hashes differ across inputs
assert crypto.sha256("a") != crypto.sha256("b")
}
fn test_csv() {
let rows = csv.parse("name,age\n\"amy, smith\",30\nbob,41")
assert len(rows) == 3
assert rows[0][0] == "name"
assert rows[1][0] == "amy, smith"
assert rows[2][1] == "41"
// empty input yields no rows
assert len(csv.parse("")) == 0
}
fn test_path_helpers() {
assert os.ext("a/b/file.txt") == ".txt"
assert os.ext("noext") == ""
assert os.base("a/b/c.vr") == "c.vr"
assert os.dir("a/b/c.vr") == "a/b"
assert os.rel("a/b/c.txt", "a") == "b/c.txt"
assert os.rel("a/b/c.txt", "a/b/c.txt") == "."
let abs_p = os.abs("main.v")
assert abs_p != "main.v"
assert len(abs_p) > 0
}
fn test_exec_full() {
let res = os.exec_full("echo hello-out; echo hello-err 1>&2")
assert res.code == 0
assert res.stdout == "hello-out\n"
assert res.stderr == "hello-err\n"
let bad = os.exec_full("exit 3")
assert bad.code == 3
}
fn test_time_arithmetic() {
let t = time.now()
assert time.add_days(t, 1) - t == 86400
assert time.add_hours(t, 2) - t == 7200
assert time.add_minutes(t, 5) - t == 300
assert time.add_seconds(t, 10) - t == 10
// 2026-08-25 was a Tuesday
let fixed = time.parse("2026-08-25 12:00:00")
assert time.weekday(fixed) == "Tue"
let later = time.add_days(fixed, 1)
assert time.date(later) == "2026-08-26"
}
fn test_http_headers_timeout() {
// offline-safe: connecting to a closed local port fails fast
try {
let h = { "X-Test": "1" }
http.request("GET", "http://127.0.0.1:1/nope", "", h, 500)
assert false
} catch e {
assert len(e) > 0
}
try {
http.get_timeout("http://127.0.0.1:1/nope", 500)
assert false
} catch e {
assert len(e) > 0
}
}
+74 -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")) == 6 assert len(os.glob("lib/*.vr")) == 9
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
@@ -455,6 +455,79 @@ fn test_time() {
} }
} }
fn test_closure_capture() {
// closures capture enclosing locals by value
let factor = 3
let dbl = fn(x) {
return x * factor
}
assert dbl(7) == 21
let name = "alice"
let age = 30
let greet = fn() {
return "hi " + name + " age=" + str(age)
}
assert greet() == "hi alice age=30"
// capture by value: later changes to the outer var don't affect the closure
let counter = 10
let read = fn() {
return counter
}
assert read() == 10
counter = 99
assert read() == 10
// nested closures capture through both levels
let base = 100
let outer = fn() {
let mid = 5
let inner = fn(x) {
return base + mid + x
}
return inner(1)
}
assert outer() == 106
// assignment to a captured variable writes the closure's own copy
let n = 1
let bump = fn() {
n = n + 1
return n
}
assert bump() == 2
assert n == 1
// closure stored in a struct, called dynamically
let handler = fn() {
return "handled"
}
let rec = { run: handler }
assert rec.run() == "handled"
}
fn test_dynamic_map_keys() {
// map keys may be computed expressions, not just string literals
let m = {}
for i in 0..3 {
m["key" + str(i)] = i * 10
}
assert m["key0"] == 0
assert m["key2"] == 20
let k = "key1"
assert m[k] == 10
assert has(m, "key1") == 1
assert len(m) == 3
delete(m, "key1")
assert has(m, "key1") == 0
assert len(m) == 2
// nested computed access
let cfg = { "db": { "host": "localhost", "port": 5432 } }
let which = "db"
assert cfg[which]["host"] == "localhost"
}
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
+53
View File
@@ -0,0 +1,53 @@
// extension.js — VS Code client for the VuurRaaf language server.
//
// Spawns `vr lsp` (the toolchain's built-in language server) and wires up
// diagnostics, go-to-definition, and hover for .vr / .vrmm files.
// The toolchain binary defaults to `vr` on PATH; override it with the
// `vuurraaf.toolchainPath` setting.
const vscode = require('vscode');
const {
LanguageClient,
TransportKind,
} = require('vscode-languageclient/node');
let client;
function activate(context) {
const config = () => vscode.workspace.getConfiguration('vuurraaf');
const toolchain = () => config().get('toolchainPath', 'vr');
const serverOptions = {
command: toolchain(),
args: ['lsp'],
transport: TransportKind.stdio,
};
const clientOptions = {
documentSelector: [{ scheme: 'file', language: 'vuurraaf' }],
synchronize: {
// re-publish diagnostics when a settings change or a file is saved
fileEvents: vscode.workspace.createFileSystemWatcher('**/*.{vr,vrmm}'),
},
};
client = new LanguageClient('vuurraaf', 'VuurRaaf', serverOptions, clientOptions);
client.start();
context.subscriptions.push(
vscode.commands.registerCommand('vuurraaf.restartServer', async () => {
await client.stop();
client = new LanguageClient('vuurraaf', 'VuurRaaf', serverOptions, clientOptions);
client.start();
vscode.window.showInformationMessage('VuurRaaf language server restarted.');
})
);
}
function deactivate() {
if (client) {
return client.stop();
}
}
module.exports = { activate, deactivate };
+23
View File
@@ -0,0 +1,23 @@
{
"comments": {
"lineComment": "//"
},
"brackets": [
["{", "}"],
["[", "]"],
["(", ")"]
],
"autoClosingPairs": [
{ "open": "{", "close": "}" },
{ "open": "[", "close": "]" },
{ "open": "(", "close": ")" },
{ "open": "\"", "close": "\"", "notIn": ["string"] },
{ "open": "'", "close": "'", "notIn": ["string", "comment"] }
],
"surroundingPairs": [
["{", "}"],
["[", "]"],
["(", ")"],
["\"", "\""]
]
}
+61
View File
@@ -0,0 +1,61 @@
{
"name": "vuurraaf",
"displayName": "VuurRaaf",
"description": "Language support for VuurRaaf: syntax highlighting, diagnostics, and go-to-definition via the vr language server.",
"version": "0.1.0",
"publisher": "vuurraaf",
"license": "MIT",
"engines": {
"vscode": "^1.75.0"
},
"categories": [
"Programming Languages",
"Linters"
],
"activationEvents": [
"onLanguage:vuurraaf"
],
"main": "./client/extension.js",
"contributes": {
"languages": [
{
"id": "vuurraaf",
"aliases": [
"VuurRaaf",
"vr"
],
"extensions": [
".vr",
".vrmm"
],
"configuration": "./language-configuration.json"
}
],
"grammars": [
{
"language": "vuurraaf",
"scopeName": "source.vuurraaf",
"path": "./syntaxes/vuurraaf.tmLanguage.json"
}
],
"configuration": {
"title": "VuurRaaf",
"properties": {
"vuurraaf.toolchainPath": {
"type": "string",
"default": "vr",
"description": "Path to the vr toolchain binary (defaults to `vr` on PATH)."
}
}
}
},
"scripts": {
"vscode:prepublish": "npm install"
},
"dependencies": {
"vscode-languageclient": "^8.1.0"
},
"devDependencies": {
"@types/vscode": "^1.75.0"
}
}
+160
View File
@@ -0,0 +1,160 @@
{
"$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
"name": "VuurRaaf",
"scopeName": "source.vuurraaf",
"patterns": [
{
"include": "#comments"
},
{
"include": "#strings"
},
{
"include": "#numbers"
},
{
"include": "#keywords"
},
{
"include": "#function-declaration"
},
{
"include": "#function-call"
},
{
"include": "#identifiers"
},
{
"include": "#operators"
}
],
"repository": {
"comments": {
"patterns": [
{
"name": "comment.line.double-slash.vuurraaf",
"match": "//.*$"
}
]
},
"strings": {
"patterns": [
{
"name": "string.quoted.raw.vuurraaf",
"begin": "r\"",
"end": "\"",
"patterns": [
{
"name": "constant.character.escape.vuurraaf",
"match": "\\\\(\\\\|n|t|r|\"|$)"
}
]
},
{
"name": "string.quoted.double.vuurraaf",
"begin": "\"",
"end": "\"",
"patterns": [
{
"name": "constant.character.escape.vuurraaf",
"match": "\\\\(\\\\|n|t|r|\"|$)"
},
{
"name": "string.interpolated.vuurraaf",
"begin": "\\$\\{",
"end": "\\}",
"patterns": [
{
"include": "#identifiers"
}
]
}
]
}
]
},
"numbers": {
"patterns": [
{
"name": "constant.numeric.vuurraaf",
"match": "\\b(0x[0-9a-fA-F]+|\\d+(\\.\\d+)?([eE][+-]?\\d+)?)\\b"
}
]
},
"keywords": {
"patterns": [
{
"name": "keyword.control.vuurraaf",
"match": "\\b(fn|struct|enum|interface|import|const|let|if|else|while|for|in|match|break|continue|return|assert|try|catch|throw|and|or|not|true|false|none)\\b"
}
]
},
"function-declaration": {
"patterns": [
{
"name": "meta.function.vuurraaf",
"begin": "\\b(fn)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\(",
"beginCaptures": {
"1": {
"name": "keyword.control.vuurraaf"
},
"2": {
"name": "entity.name.function.vuurraaf"
}
},
"end": "\\)",
"patterns": [
{
"include": "#identifiers"
}
]
}
]
},
"function-call": {
"patterns": [
{
"name": "meta.function-call.vuurraaf",
"begin": "\\b([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\(",
"beginCaptures": {
"1": {
"name": "entity.name.function.vuurraaf"
}
},
"end": "\\)",
"patterns": [
{
"include": "#identifiers"
},
{
"include": "#numbers"
},
{
"include": "#strings"
}
]
}
]
},
"identifiers": {
"patterns": [
{
"name": "entity.name.type.vuurraaf",
"match": "\\b[A-Z][a-zA-Z0-9_]*\\b"
},
{
"name": "variable.other.vuurraaf",
"match": "\\b[a-z_][a-zA-Z0-9_]*\\b"
}
]
},
"operators": {
"patterns": [
{
"name": "keyword.operator.vuurraaf",
"match": "==|!=|<=|>=|<<|>>|\\+|\\-|\\*|/|%|&|\\||\\^|~|=|<|>"
}
]
}
}
}
+23
View File
@@ -0,0 +1,23 @@
// crypto.vr — hashing and encoding (part of the VuurRaaf stdlib).
//
// import crypto
// println(crypto.sha256("hello"))
// println(crypto.md5("hello"))
// println(crypto.base64_encode("hello")) // aGVsbG8=
// println(crypto.base64_decode("aGVsbG8="))
fn sha256(s) {
return sha256(s)
}
fn md5(s) {
return md5(s)
}
fn base64_encode(s) {
return base64_encode(s)
}
fn base64_decode(s) {
return base64_decode(s)
}
+13
View File
@@ -0,0 +1,13 @@
// csv.vr — comma-separated values parsing (part of the VuurRaaf stdlib).
//
// import csv
// let rows = csv.parse("name,age\namy,30\nbob,41")
// // rows == [["name", "age"], ["amy", "30"], ["bob", "41"]]
// println(rows[1][0]) // amy
//
// parse returns an array of rows; each row is an array of cell strings.
// Quoted fields, embedded commas and newlines are handled.
fn parse(s) {
return csv_parse(s)
}
+23
View File
@@ -21,3 +21,26 @@ fn post(url, data) {
fn get_text(url) { fn get_text(url) {
return http_get(url).body return http_get(url).body
} }
// request sends any method with custom headers (a {name: value} struct) and
// a timeout in milliseconds, and returns {status, body}. A timeout <= 0 uses
// the default (30s). Network failures throw; HTTP errors are normal responses.
fn request(method, url, data, headers, timeout_ms) {
return http_req(method, url, data, headers, timeout_ms)
}
fn get_with_headers(url, headers) {
return http_req("GET", url, "", headers, 0)
}
fn post_with_headers(url, data, headers) {
return http_req("POST", url, data, headers, 0)
}
fn get_timeout(url, timeout_ms) {
return http_req("GET", url, "", {}, timeout_ms)
}
fn post_timeout(url, data, timeout_ms) {
return http_req("POST", url, data, {}, timeout_ms)
}
+20
View File
@@ -73,3 +73,23 @@ fn env(name) {
fn exec(cmd) { fn exec(cmd) {
return build_exec(cmd) return build_exec(cmd)
} }
// ext returns the file extension including the dot ("file.txt" -> ".txt").
fn ext(path) {
return path_ext(path)
}
// abs converts a path to an absolute one.
fn abs(path) {
return path_abs(path)
}
// rel returns path relative to base ("a/b/c.txt" relative to "a" -> "b/c.txt").
fn rel(path, base) {
return path_rel(path, base)
}
// exec_full runs a shell command and returns {code, stdout, stderr}.
fn exec_full(cmd) {
return exec_full(cmd)
}
+30
View File
@@ -0,0 +1,30 @@
// regex.vr — regular expressions (part of the VuurRaaf stdlib).
//
// import regex
// if regex.match(r"^[a-z]+$", "hello") {
// println("all lowercase")
// }
// for m in regex.find_all(r"\d+", "a1b22c333") {
// println(m) // "1", "22", "333"
// }
//
// Patterns use RE2 syntax (no backreferences). Invalid patterns throw and
// can be caught with try/catch.
fn is_match(pattern, s) {
return regex_match(pattern, s)
}
// find_all returns an array of every substring that matches the pattern.
fn find_all(pattern, s) {
return regex_find_all(pattern, s)
}
// replace substitutes every match with `repl`.
fn replace(pattern, s, repl) {
return regex_replace(pattern, s, repl)
}
fn split(pattern, s) {
return regex_split(pattern, s)
}
+23
View File
@@ -34,3 +34,26 @@ fn date(t) {
fn clock(t) { fn clock(t) {
return format_time(t, "HH:mm:ss") return format_time(t, "HH:mm:ss")
} }
// weekday(t) returns the day of the week, e.g. "Mon", "Tue", ... "Sun".
fn weekday(t) {
return weekday(t)
}
// --- date arithmetic (all values are Unix epoch seconds) ---
fn add_seconds(t, n) {
return t + n
}
fn add_minutes(t, n) {
return t + n * 60
}
fn add_hours(t, n) {
return t + n * 3600
}
fn add_days(t, n) {
return t + n * 86400
}
+537
View File
@@ -0,0 +1,537 @@
// lsp.v — a minimal Language Server Protocol server for VuurRaaf.
//
// vr lsp
//
// Speaks JSON-RPC 2.0 over stdio with Content-Length framing. The editor
// (see extension/ for a VS Code client) sends the open document, and the
// server replies with:
// - diagnostics: the compiler's first error, positioned via its line/col
// - go-to-definition: jumps to function/struct/enum/const/let declarations
// in the current file (and stdlib module functions)
// - hover: the kind of the symbol under the cursor
//
// Text sync is "full document" (version 1), which keeps the client simple.
module main
import os
import json2
import compiler
// stdio bindings used for the JSON-RPC transport (Content-Length framing)
fn C.fgetc(stream voidptr) int
fn C.fread(dest voidptr, size usize, count usize, stream voidptr) usize
fn C.fwrite(src voidptr, size usize, count usize, stream voidptr) usize
fn C.fflush(stream voidptr) int
// ---------------------------------------------------------------------------
// JSON-RPC framing
struct LspParams {
mut:
text_document TextDocParam @[json: 'textDocument']
position LspPosition
content_changes []TextChangeParam @[json: 'contentChanges']
}
struct TextDocParam {
mut:
uri string
text string
version int
}
struct LspPosition {
mut:
line int
character int
}
struct TextChangeParam {
mut:
text string
}
struct LspMsg {
mut:
jsonrpc string
id int
method string
params LspParams
}
fn toolchain_lsp() {
run_lsp()
}
fn run_lsp() {
mut docs := map[string]string{} // uri -> latest text (full sync)
for {
raw := read_rpc() or { break }
if raw.len == 0 {
continue
}
handle_rpc(raw, mut docs)
}
}
fn handle_rpc(raw string, mut docs map[string]string) {
msg := json2.decode[LspMsg](raw) or { return }
match msg.method {
'initialize' {
// textDocumentSync 1 = full document sync
send_response(msg.id,
'{"capabilities":{"textDocumentSync":1,"definitionProvider":true,"hoverProvider":true}}')
}
'initialized' {
// notification — nothing to do
}
'shutdown' {
send_response(msg.id, 'null')
}
'exit' {
exit(0)
}
'textDocument/didOpen' {
docs[msg.params.text_document.uri] = msg.params.text_document.text
publish_diagnostics(msg.params.text_document.uri, msg.params.text_document.text)
}
'textDocument/didChange' {
// full sync: the last content change carries the whole document
if msg.params.content_changes.len > 0 {
docs[msg.params.text_document.uri] = msg.params.content_changes[msg.params.content_changes.len -
1].text
}
text := docs[msg.params.text_document.uri] or { '' }
publish_diagnostics(msg.params.text_document.uri, text)
}
'textDocument/didSave' {
text := docs[msg.params.text_document.uri] or { '' }
publish_diagnostics(msg.params.text_document.uri, text)
}
'textDocument/definition' {
loc := definition_at(msg.params.text_document.uri, docs, msg.params.position)
send_response(msg.id, loc)
}
'textDocument/hover' {
h := hover_at(msg.params.text_document.uri, docs, msg.params.position)
send_response(msg.id, h)
}
else {
// respond null to unknown requests so editors don't hang
if msg.id > 0 {
send_response(msg.id, 'null')
}
}
}
}
// read_rpc reads one Content-Length framed JSON-RPC message from stdin.
fn read_rpc() !string {
mut content_len := 0
for {
line := read_stdin_line() or { return err }
if line.len == 0 {
break // blank line ends the header block
}
if line.starts_with('Content-Length:') {
content_len = line.all_after(':').trim_space().int()
}
}
if content_len <= 0 {
return ''
}
return read_stdin_bytes(content_len).bytestr()
}
fn read_stdin_line() !string {
mut line := []u8{}
for {
b := read_stdin_byte() or { return err }
if b == `\n` {
break
}
if b != `\r` {
line << b
}
}
return line.bytestr()
}
fn read_stdin_byte() !u8 {
c := unsafe { C.fgetc(C.stdin) }
if c == -1 {
return error('stdin closed')
}
return u8(c)
}
fn read_stdin_bytes(n int) []u8 {
mut buf := []u8{len: n}
if n > 0 {
unsafe {
C.fread(buf.data, 1, usize(n), C.stdin)
}
}
return buf
}
fn send_response(id int, result string) {
send_raw('{"jsonrpc":"2.0","id":${id},"result":${result}}')
}
fn send_raw(body string) {
header := 'Content-Length: ${body.len}\r\n\r\n'
unsafe {
C.fwrite(header.str, 1, usize(header.len), C.stdout)
C.fwrite(body.str, 1, usize(body.len), C.stdout)
C.fflush(C.stdout)
}
}
// ---------------------------------------------------------------------------
// diagnostics
fn publish_diagnostics(uri string, text string) {
diags := collect_diagnostics(text)
send_raw('{"jsonrpc":"2.0","method":"textDocument/publishDiagnostics","params":{"uri":"${json_escape(uri)}","diagnostics":[${diags}]}}')
}
// collect_diagnostics runs the compiler over `text` and renders the first
// error as an LSP diagnostic. Returns the diagnostics array body (or empty).
fn collect_diagnostics(text string) string {
_ = compiler.compile(text) or {
mut line, mut col := extract_pos(err.msg())
if line < 1 {
line = 1
}
start_line := line - 1
msg := json_escape(err.msg())
return '{"range":{"start":{"line":${start_line},"character":${col}},"end":{"line":${start_line},"character":${col + 1}}},"severity":1,"source":"vr","message":"${msg}"}'
}
return ''
}
// extract_pos pulls "line N[, col M]" out of a compiler error message.
// The toolchain's errors are formatted as `... at line 12, col 5`,
// `... at line 12` or `... (line 12)`.
fn extract_pos(msg string) (int, int) {
mut line := 0
mut col := 0
if start := msg.index('line ') {
mut i := start + 5
mut num := ''
for i < msg.len && msg[i] >= `0` && msg[i] <= `9` {
num += msg[i].ascii_str()
i++
}
if num.len > 0 {
line = num.int()
}
if c := msg.index('col ') {
mut j := c + 4
mut cnum := ''
for j < msg.len && msg[j] >= `0` && msg[j] <= `9` {
cnum += msg[j].ascii_str()
j++
}
if cnum.len > 0 {
col = cnum.int()
}
}
}
return line, col
}
// ---------------------------------------------------------------------------
// symbols (definition + hover)
// symbol_at resolves the identifier under the cursor and returns its kind and
// definition line, looking in the current document and then in imported
// stdlib modules.
fn symbol_at(uri string, docs map[string]string, pos LspPosition) (string, int, string) {
text := docs[uri] or { return '', 0, '' }
word := word_at(text, pos)
if word.len == 0 {
return '', 0, ''
}
line, kind, ok := find_symbol(text, word)
if ok {
return uri, line, kind
}
// module call: the cursor may sit on the module part ("os" in "os.exists")
// or the function part ("exists"). Look up imported modules either way.
mut mod_name := ''
mut fn_name := word
if word.contains('.') {
parts := word.split('.')
if parts.len == 2 {
mod_name = parts[0]
fn_name = parts[1]
}
} else {
// cursor on the module part: extend to the dotted name
if pos.character > 0 && pos.character < text.len {
if e := word_at_ext(text, pos) {
parts := e.split('.')
if parts.len == 2 && parts[0] == word {
mod_name = parts[0]
fn_name = parts[1]
}
}
}
}
if mod_name.len > 0 {
if path := compiler.resolve_import(mod_name) {
src := os.read_file(path) or { return '', 0, '' }
l2, k2, ok2 := find_symbol(src, fn_name)
if ok2 {
return 'file://${path}', l2, k2
}
}
}
// bare function name: search every module this file imports
for m in imported_modules(text) {
if path := compiler.resolve_import(m) {
src := os.read_file(path) or { continue }
l2, k2, ok2 := find_symbol(src, word)
if ok2 {
return 'file://${path}', l2, k2
}
}
}
return '', 0, ''
}
// word_at_ext returns the dotted identifier starting at `pos` when the cursor
// is on the module part of a call like os.exists (word_at alone would stop
// at the dot).
fn word_at_ext(text string, pos LspPosition) ?string {
mut line_i := 0
for l in text.split('\n') {
if line_i == pos.line {
if pos.character < 0 || pos.character > l.len {
return none
}
// scan back to the start of the dotted identifier
mut start := pos.character
for start > 0 && (is_ident_char(l[start - 1]) || l[start - 1] == `.`) {
start--
}
mut end := pos.character
for end < l.len && (is_ident_char(l[end]) || l[end] == `.`) {
end++
}
if start == end {
return none
}
return l[start..end]
}
line_i++
}
return none
}
// imported_modules returns the bare module names (`import os`) in a file.
fn imported_modules(text string) []string {
toks := compiler.tokenize(text) or { return []string{} }
prog := compiler.parse(toks) or { return []string{} }
mut out := []string{}
for imp in prog.imports {
if imp.name.len > 0 {
out << imp.name
}
}
return out
}
fn definition_at(uri string, docs map[string]string, pos LspPosition) string {
loc_uri, line, _ := symbol_at(uri, docs, pos)
if loc_uri.len == 0 {
return 'null'
}
return location_json(loc_uri, line)
}
fn hover_at(uri string, docs map[string]string, pos LspPosition) string {
_, line, kind := symbol_at(uri, docs, pos)
if line == 0 {
return 'null'
}
return '{"contents":{"kind":"markdown","value":"**${kind}** at line ${line}"}}'
}
fn location_json(uri string, line int) string {
// 1-based source line → 0-based LSP position; character 0 (we only track lines)
return '{"uri":"${json_escape(uri)}","range":{"start":{"line":${line - 1},"character":0},"end":{"line":${line - 1},"character":0}}}'
}
// word_at returns the identifier covering the given position in `text`.
fn word_at(text string, pos LspPosition) string {
mut line_i := 0
for l in text.split('\n') {
if line_i == pos.line {
if pos.character < 0 || pos.character > l.len {
return ''
}
mut start := pos.character
mut end := pos.character
for start > 0 && is_ident_char(l[start - 1]) {
start--
}
for end < l.len && is_ident_char(l[end]) {
end++
}
if start == end {
return ''
}
return l[start..end]
}
line_i++
}
return ''
}
fn is_ident_char(c u8) bool {
lo := c >= `a` && c <= `z`
hi := c >= `A` && c <= `Z`
dig := c >= `0` && c <= `9`
return lo || hi || dig || c == `_`
}
// find_symbol parses VuurRaaf source and locates the definition line and kind
// of a named symbol (functions, structs, enums, constants, locals, params).
fn find_symbol(src string, name string) (int, string, bool) {
toks := compiler.tokenize(src) or { return 0, '', false }
prog := compiler.parse(toks) or { return 0, '', false }
mut line := 0
mut kind := ''
for fd in prog.fns {
if fd.name == name {
return fd.line, 'function', true
}
for p in fd.params {
if p == name {
line = fd.line
kind = 'parameter'
}
}
}
for sd in prog.structs {
if sd.name == name {
return sd.line, 'struct', true
}
}
for ed in prog.enums {
if ed.name == name {
return ed.line, 'enum', true
}
}
for cd in prog.consts {
if cd.name == name {
return cd.line, 'constant', true
}
}
for fd in prog.fns {
for st in fd.body {
l, k, ok := find_symbol_stmt(st, name)
if ok {
line = l
kind = k
}
}
}
if line > 0 {
return line, kind, true
}
return 0, '', false
}
fn find_symbol_stmt(st compiler.Stmt, name string) (int, string, bool) {
match st.kind {
.let_stmt {
if st.target == name {
return st.line, 'variable', true
}
}
.destruct_stmt {
for t in st.destruct_targets {
if t == name {
return st.line, 'variable', true
}
}
}
.for_range_stmt, .for_in_stmt {
if st.target == name {
return st.line, 'loop variable', true
}
if st.idx_target == name {
return st.line, 'loop variable', true
}
}
.try_stmt {
if st.target == name {
return st.line, 'catch variable', true
}
}
else {}
}
// recurse into nested statements so inner blocks are covered too
for s in st.body {
l, k, ok := find_symbol_stmt(s, name)
if ok {
return l, k, true
}
}
for s in st.els {
l, k, ok := find_symbol_stmt(s, name)
if ok {
return l, k, true
}
}
for s in st.els_body {
l, k, ok := find_symbol_stmt(s, name)
if ok {
return l, k, true
}
}
for arm in st.arms {
for s in arm.body {
l, k, ok := find_symbol_stmt(s, name)
if ok {
return l, k, true
}
}
}
return 0, '', false
}
// ---------------------------------------------------------------------------
// helpers
fn json_escape(s string) string {
mut out := ''
for c in s {
match c {
`"` {
out += '\\"'
}
`\\` {
out += '\\\\'
}
`\n` {
out += '\\n'
}
`\r` {
out += '\\r'
}
`\t` {
out += '\\t'
}
else {
out += c.ascii_str()
}
}
}
return out
}
+107 -5
View File
@@ -73,6 +73,9 @@ fn main() {
'repl', 'i' { 'repl', 'i' {
toolchain_repl() or { die('repl', err) } toolchain_repl() or { die('repl', err) }
} }
'lsp' {
toolchain_lsp()
}
'fmt' { 'fmt' {
toolchain_fmt(rest) or { die('fmt', err) } toolchain_fmt(rest) or { die('fmt', err) }
} }
@@ -190,6 +193,7 @@ fn toolchain_help() {
println(' test <file.vr> run every test_* function') println(' test <file.vr> run every test_* function')
println(' bench <file.vr> [iterations] benchmark main()') println(' bench <file.vr> [iterations] benchmark main()')
println(' repl interactive session') println(' repl interactive session')
println(' lsp language server (JSON-RPC over stdio)')
println(' fmt [-w] <file.vr> format source') println(' fmt [-w] <file.vr> format source')
println(' init [name] scaffold a project') println(' init [name] scaffold a project')
println(' get <owner/repo | url | ./path> fetch a package into vendor/') println(' get <owner/repo | url | ./path> fetch a package into vendor/')
@@ -358,10 +362,23 @@ fn toolchain_link(args []string) ! {
fn toolchain_run(args []string) ! { fn toolchain_run(args []string) ! {
if args.len == 0 { if args.len == 0 {
return error('usage: vr run <file.vr|file.vbin> [program-args...]') return error('usage: vr run <file.vr|file.vbin> [program-args...] (add -w to watch for changes)')
}
mut watch := false
mut rest := args.clone()
if rest[0] == '-w' || rest[0] == '--watch' {
watch = true
rest = rest[1..]
}
if rest.len == 0 {
return error('usage: vr run <file.vr|file.vbin> [program-args...] (add -w to watch for changes)')
}
f := rest[0]
prog_args := rest[1..]
if watch {
run_watch(f, prog_args)!
return
} }
f := args[0]
prog_args := args[1..]
if f.ends_with('.vbin') { if f.ends_with('.vbin') {
bin := obj.read_bin(f)! bin := obj.read_bin(f)!
vm.run_with_args(bin, 'main', false, prog_args)! vm.run_with_args(bin, 'main', false, prog_args)!
@@ -374,6 +391,32 @@ fn toolchain_run(args []string) ! {
return error('unsupported file type: ${f} (expected .vr or .vbin)') return error('unsupported file type: ${f} (expected .vr or .vbin)')
} }
// run_watch recompiles and reruns the program whenever the source file (or
// anything it imports) changes — the classic develop-run-edit loop.
fn run_watch(f string, prog_args []string) ! {
if !f.ends_with('.vr') {
return error('watch mode works on .vr source files, got ${f}')
}
mut last := os.file_last_mod_unix(f)
println('watching ${f} (Ctrl-C to stop)')
for {
// clear the screen between runs for a clean diff of output
print('\x1b[2J\x1b[H')
println('== ${os.file_name(f)} ${time.now().custom_format('HH:mm:ss')} ==')
run_src_with_args(f, 'main', false, prog_args) or {
eprintln('${err.msg()}')
}
for {
time.sleep(400 * time.millisecond)
cur := os.file_last_mod_unix(f)
if cur != last {
last = cur
break
}
}
}
}
fn toolchain_debug(args []string) ! { fn toolchain_debug(args []string) ! {
if args.len == 0 { if args.len == 0 {
return error('usage: vr debug <file.vr|file.vbin>') return error('usage: vr debug <file.vr|file.vbin>')
@@ -394,9 +437,14 @@ fn toolchain_debug(args []string) ! {
fn toolchain_test(args []string) ! { fn toolchain_test(args []string) ! {
if args.len == 0 { if args.len == 0 {
return error('usage: vr test <file.vr>') return error('usage: vr test <file.vr|dir>')
} }
src := args[0] target := args[0]
if os.is_dir(target) {
test_dir(target)!
return
}
src := target
o := compiler.compile_file(src)! o := compiler.compile_file(src)!
mut tests := []string{} mut tests := []string{}
for s in o.symbols { for s in o.symbols {
@@ -434,6 +482,60 @@ fn toolchain_test(args []string) ! {
} }
} }
// test_dir runs every test_* function in every .vr file under a directory
// (recursively), so a whole project's suite runs with one command.
fn test_dir(dir string) ! {
files := os.walk_ext(dir, '.vr', os.WalkParams{})
mut files_sorted := files.clone()
files_sorted.sort()
mut total_pass := 0
mut total_fail := 0
mut file_count := 0
for src in files_sorted {
if os.file_name(src).starts_with('.') {
continue
}
o := compiler.compile_file(src) or {
eprintln(' COMPILE FAIL ${src} ${err.msg()}')
total_fail++
continue
}
mut tests := []string{}
for s in o.symbols {
if s.name.starts_with('test_') {
tests << s.name
}
}
if tests.len == 0 {
continue
}
file_count++
println('-- ${src}')
tmp_obj := os.join_path(os.temp_dir(), 'vr_${os.getpid()}_${file_count}.vobj')
tmp_bin := os.join_path(os.temp_dir(), 'vr_${os.getpid()}_${file_count}.vbin')
defer {
os.rm(tmp_obj) or {}
os.rm(tmp_bin) or {}
}
obj.write(tmp_obj, o)!
linker.link([tmp_obj], tmp_bin)!
bin := obj.read_bin(tmp_bin)!
for t in tests {
if run_test(bin, t) {
println(' PASS ${t}')
total_pass++
} else {
total_fail++
}
}
}
println('')
println('${total_pass} passed, ${total_fail} failed across ${file_count} file(s)')
if total_fail > 0 {
exit(1)
}
}
fn run_test(bin obj.Bin, name string) bool { fn run_test(bin obj.Bin, name string) bool {
vm.run(bin, name, false) or { vm.run(bin, name, false) or {
eprintln(' FAIL ${name} ${err}') eprintln(' FAIL ${name} ${err}')
+10
View File
@@ -84,6 +84,13 @@ fn (mut v Vm) collect() {
} }
} }
} }
for h in 0..v.closures.len {
if closure_mark[h] {
for j in 0..v.closures[h].captured.len {
v.closures[h].captured[j] = v.remap(v.closures[h].captured[j], str_new, arr_new, struct_new, float_new, closure_new)
}
}
}
// ---- compact pools ---- // ---- compact pools ----
mut strings := v.strings[..v.const_strs] mut strings := v.strings[..v.const_strs]
for i in v.const_strs..v.strings.len { for i in v.const_strs..v.strings.len {
@@ -164,6 +171,9 @@ fn (mut v Vm) mark_value(x i64, mut str_mark []bool, mut arr_mark []bool, mut st
h := v.hand(val) h := v.hand(val)
if h >= 0 && h < closure_mark.len && !closure_mark[h] { if h >= 0 && h < closure_mark.len && !closure_mark[h] {
closure_mark[h] = true closure_mark[h] = true
for cv in v.closures[h].captured {
work << cv
}
} }
} }
else {} else {}
+2 -2
View File
@@ -113,7 +113,7 @@ fn (mut v Vm) json_parse_object(mut p JsonParser) !i64 {
p.skip_ws() p.skip_ws()
if p.peek() == `}` { if p.peek() == `}` {
p.pos++ p.pos++
v.structs << StructVal{ fields: fields } v.structs << StructVal{ fields: fields, by_name: v.index_fields(fields) }
return v.mkstruct_handle(v.structs.len - 1) return v.mkstruct_handle(v.structs.len - 1)
} }
for { for {
@@ -152,7 +152,7 @@ fn (mut v Vm) json_parse_object(mut p JsonParser) !i64 {
} }
return p.error('expected "," or "}" in object') return p.error('expected "," or "}" in object')
} }
v.structs << StructVal{ fields: fields } v.structs << StructVal{ fields: fields, by_name: v.index_fields(fields) }
return v.mkstruct_handle(v.structs.len - 1) return v.mkstruct_handle(v.structs.len - 1)
} }
+203 -2
View File
@@ -16,6 +16,11 @@ import obj
import compiler import compiler
import assembler import assembler
import linker import linker
import regex
import encoding.base64
import crypto.sha256
import crypto.md5
import encoding.csv
fn (mut v Vm) native(id int, _argc int) ! { fn (mut v Vm) native(id int, _argc int) ! {
match id { match id {
@@ -250,7 +255,7 @@ fn (mut v Vm) native(id int, _argc int) ! {
v.push(v.mkarr(v.arrays.len - 1))! v.push(v.mkarr(v.arrays.len - 1))!
} else if v.is_struct(x) && v.valid_struct_handle(x) { } else if v.is_struct(x) && v.valid_struct_handle(x) {
s := v.structs[v.hand(x)] s := v.structs[v.hand(x)]
v.structs << StructVal{ fields: s.fields.clone() } v.structs << StructVal{ fields: s.fields.clone(), by_name: s.by_name.clone() }
v.push(v.mkstruct_handle(v.structs.len - 1))! v.push(v.mkstruct_handle(v.structs.len - 1))!
} else if v.is_str(x) && v.valid_handle(x) { } else if v.is_str(x) && v.valid_handle(x) {
v.push(v.alloc_str(v.strings[v.hand(x)]))! v.push(v.alloc_str(v.strings[v.hand(x)]))!
@@ -679,12 +684,208 @@ fn (mut v Vm) native(id int, _argc int) ! {
t := time.parse(s) or { return error('parse_time: ${err.msg()}') } t := time.parse(s) or { return error('parse_time: ${err.msg()}') }
v.push(v.enc_int(t.unix()))! v.push(v.enc_int(t.unix()))!
} }
native_weekday {
t := v.dec_int(v.pop()!)
v.push(v.alloc_str(time.unix(t).weekday_str()))!
}
// -------------------------------------------------------------------
// regex — RE2-style patterns via V's regex module. Every call compiles
// the pattern fresh; the VuurRaaf-level module can cache if it needs to.
native_regex_match {
s := v.pop_str()!
pat := v.pop_str()!
mut re := regex.regex_opt(pat) or { return error('regex: ${err.msg()}') }
v.push(v.enc_int(bool_i64(re.matches_string(s))))!
}
native_regex_find_all {
s := v.pop_str()!
pat := v.pop_str()!
mut re := regex.regex_opt(pat) or { return error('regex: ${err.msg()}') }
matches := re.find_all_str(s)
mut arr := []i64{}
for m in matches {
v.strings << m
arr << v.mkstr(v.strings.len - 1)
}
v.arrays << arr
v.push(v.mkarr(v.arrays.len - 1))!
}
native_regex_replace {
repl := v.pop_str()!
s := v.pop_str()!
pat := v.pop_str()!
mut re := regex.regex_opt(pat) or { return error('regex: ${err.msg()}') }
v.push(v.alloc_str(re.replace(s, repl)))!
}
native_regex_split {
s := v.pop_str()!
pat := v.pop_str()!
mut re := regex.regex_opt(pat) or { return error('regex: ${err.msg()}') }
parts := re.split(s)
mut arr := []i64{}
for p in parts {
v.strings << p
arr << v.mkstr(v.strings.len - 1)
}
v.arrays << arr
v.push(v.mkarr(v.arrays.len - 1))!
}
// -------------------------------------------------------------------
// base64 + hashes
native_base64_encode {
s := v.pop_str()!
v.push(v.alloc_str(base64.encode_str(s)))!
}
native_base64_decode {
s := v.pop_str()!
v.push(v.alloc_str(base64.decode_str(s)))!
}
native_sha256 {
s := v.pop_str()!
v.push(v.alloc_str(sha256.hexhash(s)))!
}
native_md5 {
s := v.pop_str()!
v.push(v.alloc_str(md5.hexhash(s)))!
}
native_csv_parse {
s := v.pop_str()!
mut r := csv.new_reader(s)
// read() yields one row ([]string) at a time until EOF
mut rows := [][]string{}
for {
row := r.read() or { break }
if row.len == 0 {
break
}
rows << row
}
mut outer := []i64{}
for row in rows {
mut inner := []i64{}
for cell in row {
v.strings << cell
inner << v.mkstr(v.strings.len - 1)
}
v.arrays << inner
outer << v.mkarr(v.arrays.len - 1)
}
v.arrays << outer
v.push(v.mkarr(v.arrays.len - 1))!
}
// -------------------------------------------------------------------
// extended HTTP: any method, custom headers (a {name: value} struct),
// and a per-request timeout in milliseconds
native_http_req {
timeout_ms := int(v.dec_int(v.pop()!))
headers_h := v.pop()!
data := v.pop_str()!
url := v.pop_str()!
method := v.pop_str()!
mut h := http.new_header()
if v.is_struct(headers_h) && v.valid_struct_handle(headers_h) {
for f in v.structs[v.hand(headers_h)].fields {
if v.is_str(f.val) && v.valid_handle(f.val) {
h.add_custom(f.name, v.strings[v.hand(f.val)]) or {}
}
}
}
m := match method.to_upper() {
'GET' { http.Method.get }
'POST' { http.Method.post }
'PUT' { http.Method.put }
'DELETE' { http.Method.delete }
'PATCH' { http.Method.patch }
'HEAD' { http.Method.head }
else { return error('http_req: unsupported method "${method}"') }
}
resp := http.fetch(method: m, url: url, data: data, header: h,
read_timeout: i64(timeout_ms) * time.millisecond) or {
return error('http_req: ${err.msg()}')
}
v.push_http_response(resp)!
}
// -------------------------------------------------------------------
// filesystem path helpers
native_path_ext {
p := v.pop_str()!
v.push(v.alloc_str(os.file_ext(p)))!
}
native_path_abs {
p := v.pop_str()!
v.push(v.alloc_str(os.abs_path(p)))!
}
native_path_rel {
base := v.pop_str()!
p := v.pop_str()!
v.push(v.alloc_str(v.rel_path(p, base)))!
}
// -------------------------------------------------------------------
// process spawn with separate stdout/stderr pipes
native_exec_full {
cmd := v.pop_str()!
mut code := 0
mut out := ''
mut errs := ''
if os.user_os() == 'windows' {
res := os.execute(cmd)
code = res.exit_code
out = res.output
} else {
mut p := os.new_process('sh')
p.set_args(['-c', cmd])
p.use_stdio_ctl = true
p.run()
p.wait()
if p.err.len > 0 {
return error('exec_full: ${p.err}')
}
code = p.code
out = p.stdout_slurp()
errs = p.stderr_slurp()
}
out_h := v.alloc_str(out)
err_h := v.alloc_str(errs)
mut fields := []Field{len: 3}
fields[0] = Field{ name: 'code', val: v.enc_int(i64(code)) }
fields[1] = Field{ name: 'stdout', val: out_h }
fields[2] = Field{ name: 'stderr', val: err_h }
v.structs << StructVal{ fields: fields, by_name: v.index_fields(fields) }
v.push(v.mkstruct_handle(v.structs.len - 1))!
}
else { else {
return error('unknown native builtin ${id}') return error('unknown native builtin ${id}')
} }
} }
} }
// rel_path computes a relative path from `base` to `p` (both absolutized),
// e.g. rel_path("/a/b/c.txt", "/a") == "b/c.txt". Used by os.rel().
fn (v Vm) rel_path(p string, base string) string {
ap := os.abs_path(p).replace('\\', '/')
ab := os.abs_path(base).replace('\\', '/')
if ap == ab {
return '.'
}
aparts := ap.split('/')
bparts := ab.split('/')
mut i := 0
for i < aparts.len && i < bparts.len && aparts[i] == bparts[i] {
i++
}
mut out := []string{}
for _ in i..bparts.len {
out << '..'
}
for j in i..aparts.len {
out << aparts[j]
}
if out.len == 0 {
return '.'
}
return out.join('/')
}
// push_http_response wraps an HTTP response as a {status, body} struct value // push_http_response wraps an HTTP response as a {status, body} struct value
// so scripts can read res.status and res.body. // so scripts can read res.status and res.body.
fn (mut v Vm) push_http_response(resp http.Response) ! { fn (mut v Vm) push_http_response(resp http.Response) ! {
@@ -692,7 +893,7 @@ fn (mut v Vm) push_http_response(resp http.Response) ! {
mut fields := []Field{len: 2} mut fields := []Field{len: 2}
fields[0] = Field{ name: 'status', val: v.enc_int(i64(resp.status_code)) } fields[0] = Field{ name: 'status', val: v.enc_int(i64(resp.status_code)) }
fields[1] = Field{ name: 'body', val: body_h } fields[1] = Field{ name: 'body', val: body_h }
v.structs << StructVal{ fields: fields } v.structs << StructVal{ fields: fields, by_name: v.index_fields(fields) }
v.push(v.mkstruct_handle(v.structs.len - 1))! v.push(v.mkstruct_handle(v.structs.len - 1))!
} }
+21
View File
@@ -150,3 +150,24 @@ const native_now = 170
const native_time_ms = 171 const native_time_ms = 171
const native_format_time = 172 const native_format_time = 172
const native_parse_time = 173 const native_parse_time = 173
// regex builtins
const native_regex_match = 174
const native_regex_find_all = 175
const native_regex_replace = 176
const native_regex_split = 177
// crypto/encoding builtins
const native_base64_encode = 178
const native_base64_decode = 179
const native_sha256 = 180
const native_md5 = 181
const native_csv_parse = 182
// extended HTTP + filesystem/process builtins
const native_http_req = 183
const native_path_ext = 184
const native_path_abs = 185
const native_path_rel = 186
const native_exec_full = 187
const native_weekday = 188
+7
View File
@@ -12,14 +12,20 @@ mut:
val i64 val i64
} }
// StructVal is a struct (or map) value. `fields` keeps insertion order for
// rendering and JSON encoding; `by_name` is a hash index for O(1) field
// lookup by name, so map-style access on large records stays fast.
struct StructVal { struct StructVal {
mut: mut:
fields []Field fields []Field
by_name map[string]int // field name -> index into fields
} }
// Handler records a try/catch handler pushed at runtime. // Handler records a try/catch handler pushed at runtime.
struct Closure { struct Closure {
entry int // code IP of the function body entry int // code IP of the function body
mut:
captured []i64 // values of the enclosing locals this closure captures (by value)
} }
struct Handler { struct Handler {
@@ -47,6 +53,7 @@ mut:
did_exit bool did_exit bool
handlers []Handler handlers []Handler
lines []obj.LineInfo // debug info: code offset -> source line lines []obj.LineInfo // debug info: code offset -> source line
fns []obj.BinFn // function table (for stack traces)
const_strs int // strings[0..const_strs] are bytecode constants, never collected const_strs int // strings[0..const_strs] are bytecode constants, never collected
last_heap int // heap size at the last GC check (allocation trigger) last_heap int // heap size at the last GC check (allocation trigger)
build_root string // directory of the .vrmm build module (build_root() builtin) build_root string // directory of the .vrmm build module (build_root() builtin)
+150 -48
View File
@@ -35,6 +35,7 @@ fn run_internal(bin obj.Bin, entry string, trace bool, args []string, root strin
trace: trace trace: trace
prog_args: args prog_args: args
lines: bin.lines lines: bin.lines
fns: bin.fns
const_strs: bin.strings.len const_strs: bin.strings.len
build_root: root build_root: root
} }
@@ -61,7 +62,7 @@ fn run_internal(bin obj.Bin, entry string, trace bool, args []string, root strin
v.bp = v.sp v.bp = v.sp
v.ip = entry_ip v.ip = entry_ip
v.exec() or { v.exec() or {
return error('${err.msg()} at ${v.where()}') return error('${err.msg()} at ${v.where()}\n${v.stack_trace()}')
} }
if v.did_exit { if v.did_exit {
return v.exit_code return v.exit_code
@@ -75,14 +76,57 @@ fn run_internal(bin obj.Bin, entry string, trace bool, args []string, root strin
// where returns a source-level location for the current instruction pointer: // where returns a source-level location for the current instruction pointer:
// `line 12 (ip 345)` when debug info is available, otherwise just `(ip 345)`. // `line 12 (ip 345)` when debug info is available, otherwise just `(ip 345)`.
fn (v Vm) where() string { fn (v Vm) where() string {
return 'line ${v.line_at(v.ip)} (ip ${v.ip})'
}
// line_at maps a code offset to its source line via the line table.
fn (v Vm) line_at(ip int) int {
// line table entries are recorded in code order, so walk backwards from // line table entries are recorded in code order, so walk backwards from
// the most recent entry to find the last one at or before v.ip // the most recent entry to find the last one at or before ip
for i := v.lines.len - 1; i >= 0; i-- { for i := v.lines.len - 1; i >= 0; i-- {
if v.ip >= int(v.lines[i].off) { if ip >= int(v.lines[i].off) {
return 'line ${v.lines[i].line} (ip ${v.ip})' return v.lines[i].line
} }
} }
return '(ip ${v.ip})' return 0
}
// func_at returns the name of the function whose body contains the given
// code offset. Functions are laid out sequentially, so the enclosing
// function is the one with the greatest entry point <= ip.
fn (v Vm) func_at(ip int) string {
mut name := '?'
for f in v.fns {
if f.entry <= ip {
name = f.name
}
}
return name
}
// stack_trace renders the call chain at the moment an error is raised, from
// the innermost frame out to main. Each frame's return address and saved bp
// live in the frame header pushed by `call`: [retaddr, old_bp, argc] at
// bp-3..bp-1. The synthetic entry frame has retaddr == -1 (the halt sentinel).
fn (mut v Vm) stack_trace() string {
mut out := []string{}
mut bp := v.bp
mut ip := v.ip
mut guard := 0
// note: sp may have dropped below bp (an error handler pops values), so
// the frame chain is bounded by the guard and the old_bp < bp invariant
for bp >= 3 && guard < 10000 {
out << ' at ${v.func_at(ip)} (line ${v.line_at(ip)})'
ret := v.dec_int(v.stack[bp - 3])
old_bp := int(v.dec_int(v.stack[bp - 2]))
if ret == -1 || old_bp < 0 || old_bp >= bp {
break // reached the synthetic entry frame
}
ip = int(ret)
bp = old_bp
guard++
}
return out.join('\n')
} }
fn (mut v Vm) exec() ! { fn (mut v Vm) exec() ! {
@@ -303,8 +347,21 @@ fn (mut v Vm) exec() ! {
} }
op_aget { op_aget {
v.ip++ v.ip++
idx := int(v.dec_int(v.pop()!)) idxv := v.pop()!
h := v.pop()! h := v.pop()!
// dynamic map read: m[key_expr] where the base is a struct and the
// index evaluates to a string
if v.is_struct(h) && v.valid_struct_handle(h) && v.is_str(idxv) && v.valid_handle(idxv) {
fname := v.strings[v.hand(idxv)]
s := v.structs[v.hand(h)]
idx, ok := v.field_idx(s, fname)
if ok {
v.push(s.fields[idx].val)!
} else {
return error('no field "${fname}" on struct')
}
} else {
idx := int(v.dec_int(idxv))
if v.is_arr(h) && v.valid_arr_handle(h) { if v.is_arr(h) && v.valid_arr_handle(h) {
a := v.arrays[v.hand(h)] a := v.arrays[v.hand(h)]
if idx < 0 || idx >= a.len { if idx < 0 || idx >= a.len {
@@ -322,11 +379,27 @@ fn (mut v Vm) exec() ! {
return error('indexing a non-array, non-string value') return error('indexing a non-array, non-string value')
} }
} }
}
op_aset { op_aset {
v.ip++ v.ip++
val := v.pop()! val := v.pop()!
idx := int(v.dec_int(v.pop()!)) idxv := v.pop()!
h := v.pop()! h := v.pop()!
// dynamic map write: m[key_expr] = v (string key on a struct)
if v.is_struct(h) && v.valid_struct_handle(h) && v.is_str(idxv) && v.valid_handle(idxv) {
fname := v.strings[v.hand(idxv)]
mut s := v.structs[v.hand(h)]
idx, ok := v.field_idx(s, fname)
if ok {
s.fields[idx].val = val
} else {
// setting a missing field adds it, so maps can grow
s.fields << Field{ name: fname, val: val }
s.by_name[fname] = s.fields.len - 1
}
v.structs[v.hand(h)] = s
} else {
idx := int(v.dec_int(idxv))
if !v.is_arr(h) || !v.valid_arr_handle(h) { if !v.is_arr(h) || !v.valid_arr_handle(h) {
return error('indexing a non-array value') return error('indexing a non-array value')
} }
@@ -335,6 +408,7 @@ fn (mut v Vm) exec() ! {
} }
v.arrays[v.hand(h)][idx] = val v.arrays[v.hand(h)][idx] = val
} }
}
op_alen { op_alen {
v.ip++ v.ip++
h := v.pop()! h := v.pop()!
@@ -371,7 +445,7 @@ fn (mut v Vm) exec() ! {
} }
fields[i] = Field{ name: v.strings[v.hand(name)], val: val } fields[i] = Field{ name: v.strings[v.hand(name)], val: val }
} }
v.structs << StructVal{ fields: fields } v.structs << StructVal{ fields: fields, by_name: v.index_fields(fields) }
v.push(v.mkstruct_handle(v.structs.len - 1))! v.push(v.mkstruct_handle(v.structs.len - 1))!
} }
op_sget { op_sget {
@@ -385,15 +459,11 @@ fn (mut v Vm) exec() ! {
return error('internal: field name is not a string') return error('internal: field name is not a string')
} }
fname := v.strings[v.hand(name)] fname := v.strings[v.hand(name)]
mut found := false s := v.structs[v.hand(h)]
for f in v.structs[v.hand(h)].fields { idx, ok := v.field_idx(s, fname)
if f.name == fname { if ok {
v.push(f.val)! v.push(s.fields[idx].val)!
found = true } else {
break
}
}
if !found {
return error('no field "${fname}" on struct') return error('no field "${fname}" on struct')
} }
} }
@@ -411,18 +481,14 @@ fn (mut v Vm) exec() ! {
} }
fname := v.strings[v.hand(name)] fname := v.strings[v.hand(name)]
mut s := v.structs[v.hand(h)] mut s := v.structs[v.hand(h)]
mut found := false idx, ok := v.field_idx(s, fname)
for i, f in s.fields { if ok {
if f.name == fname { s.fields[idx].val = val
s.fields[i].val = val } else {
found = true
break
}
}
if !found {
// setting a missing field adds it, so records can be built // setting a missing field adds it, so records can be built
// incrementally from an empty `{}` // incrementally from an empty `{}`
s.fields << Field{ name: fname, val: val } s.fields << Field{ name: fname, val: val }
s.by_name[fname] = s.fields.len - 1
} }
v.structs[v.hand(h)] = s v.structs[v.hand(h)] = s
} }
@@ -526,7 +592,14 @@ fn (mut v Vm) exec() ! {
op_closure { op_closure {
v.ip++ v.ip++
entry := int(v.read_i64()) entry := int(v.read_i64())
v.closures << Closure{ entry: entry } n := int(v.read_i64())
// the captured values were pushed by the compiler in capture
// order; pop them back into the closure's own array
mut captured := []i64{len: n}
for i := n - 1; i >= 0; i-- {
captured[i] = v.pop()!
}
v.closures << Closure{ entry: entry, captured: captured }
v.push(v.mkclosure(v.closures.len - 1))! v.push(v.mkclosure(v.closures.len - 1))!
} }
op_call_closure { op_call_closure {
@@ -537,17 +610,30 @@ fn (mut v Vm) exec() ! {
if !v.is_closure(h) || !v.valid_closure_handle(h) { if !v.is_closure(h) || !v.valid_closure_handle(h) {
return error('cannot call a non-function value') return error('cannot call a non-function value')
} }
entry := v.closures[v.hand(h)].entry cl := v.closures[v.hand(h)]
// Shift args left to overwrite the closure slot (and drop the n := cl.captured.len
// duplicated tail), so the callee's retv lands exactly where the c := v.sp - argc - 1 // closure slot
// call sequence began and no stale value is left below the // Rearrange the stack from [...closure, arg_0..arg_{argc-1}] to
// result. The caller's own local holding the closure sits below // [capture_0..capture_{n-1}, arg_0..arg_{argc-1}]: the captures
// the pushed sequence and is never touched. // take over the closure slot, and the args shift by (1 - n) so
for i := 0; i < argc; i++ { // the callee sees captures as its leading locals followed by the
v.stack[v.sp - argc - 1 + i] = v.stack[v.sp - argc + i] // real arguments. v.call cleans the whole region up on ret.
if n > 1 {
// shifting right: copy backwards to avoid clobbering
for i := argc - 1; i >= 0; i-- {
v.stack[c + n + i] = v.stack[c + 1 + i]
} }
v.sp-- // drop the duplicated arg tail; closure slot was consumed } else {
v.call(entry, argc) // shifting left (or no shift): copy forwards
for i := 0; i < argc; i++ {
v.stack[c + n + i] = v.stack[c + 1 + i]
}
}
for i in 0..n {
v.stack[c + i] = cl.captured[i]
}
v.sp = c + n + argc
v.call(cl.entry, argc + n)
} }
op_argc { op_argc {
v.ip++ v.ip++
@@ -595,6 +681,25 @@ fn (mut v Vm) exec() ! {
} }
} }
// field_idx returns the index of a named field via the hash index, or
// (0, false) when the field does not exist.
fn (v Vm) field_idx(s StructVal, fname string) (int, bool) {
if fname in s.by_name {
return s.by_name[fname], true
}
return 0, false
}
// index_fields builds the name -> index hash map for a freshly built field
// list (used by mkstruct and other struct constructors).
fn (v Vm) index_fields(fields []Field) map[string]int {
mut m := map[string]int{}
for i, f in fields {
m[f.name] = i
}
return m
}
// op_shas checks if a struct has a field with the given name. // op_shas checks if a struct has a field with the given name.
// stack: struct, "key" → pushes 1 if found, 0 if not. // stack: struct, "key" → pushes 1 if found, 0 if not.
fn (mut v Vm) op_shas() ! { fn (mut v Vm) op_shas() ! {
@@ -608,13 +713,7 @@ fn (mut v Vm) op_shas() ! {
return error('internal: field name is not a string') return error('internal: field name is not a string')
} }
fname := v.strings[v.hand(name)] fname := v.strings[v.hand(name)]
mut found := false found := fname in v.structs[v.hand(h)].by_name
for f in v.structs[v.hand(h)].fields {
if f.name == fname {
found = true
break
}
}
v.push(v.enc_int(if found { 1 } else { 0 }))! v.push(v.enc_int(if found { 1 } else { 0 }))!
} }
@@ -629,17 +728,20 @@ fn (mut v Vm) op_sdel() ! {
} }
if !v.is_str(name) || !v.valid_handle(name) { if !v.is_str(name) || !v.valid_handle(name) {
return error('internal: field name is not a string') return error('internal: field name is not a string')
} } fname := v.strings[v.hand(name)]
fname := v.strings[v.hand(name)]
mut s := v.structs[v.hand(h)] mut s := v.structs[v.hand(h)]
idx, ok := v.field_idx(s, fname)
if ok {
mut new_fields := []Field{} mut new_fields := []Field{}
for f in s.fields { for i, f in s.fields {
if f.name != fname { if i != idx {
new_fields << f new_fields << f
} }
} }
s.fields = new_fields s.fields = new_fields
s.by_name = v.index_fields(new_fields)
v.structs[v.hand(h)] = s v.structs[v.hand(h)] = s
}
v.push(h)! v.push(h)!
} }