mirror of
https://github.com/bearlanguageorg/bear.git
synced 2026-08-26 15:17:19 +00:00
More buildins and vscode exstention <CO-AUthored, ai>
This commit is contained in:
@@ -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,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/
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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 {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 := ''
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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 };
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"comments": {
|
||||||
|
"lineComment": "//"
|
||||||
|
},
|
||||||
|
"brackets": [
|
||||||
|
["{", "}"],
|
||||||
|
["[", "]"],
|
||||||
|
["(", ")"]
|
||||||
|
],
|
||||||
|
"autoClosingPairs": [
|
||||||
|
{ "open": "{", "close": "}" },
|
||||||
|
{ "open": "[", "close": "]" },
|
||||||
|
{ "open": "(", "close": ")" },
|
||||||
|
{ "open": "\"", "close": "\"", "notIn": ["string"] },
|
||||||
|
{ "open": "'", "close": "'", "notIn": ["string", "comment"] }
|
||||||
|
],
|
||||||
|
"surroundingPairs": [
|
||||||
|
["{", "}"],
|
||||||
|
["[", "]"],
|
||||||
|
["(", ")"],
|
||||||
|
["\"", "\""]
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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": "==|!=|<=|>=|<<|>>|\\+|\\-|\\*|/|%|&|\\||\\^|~|=|<|>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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
@@ -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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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}')
|
||||||
|
|||||||
@@ -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 {}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+207
-6
@@ -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 {
|
||||||
@@ -248,11 +253,11 @@ fn (mut v Vm) native(id int, _argc int) ! {
|
|||||||
if v.is_arr(x) && v.valid_arr_handle(x) {
|
if v.is_arr(x) && v.valid_arr_handle(x) {
|
||||||
v.arrays << v.arrays[v.hand(x)].clone()
|
v.arrays << v.arrays[v.hand(x)].clone()
|
||||||
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)]))!
|
||||||
} else if v.is_float(x) {
|
} else if v.is_float(x) {
|
||||||
v.push(v.push_float(v.fval(x)))!
|
v.push(v.push_float(v.fval(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))!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+28
-21
@@ -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 {
|
||||||
@@ -30,26 +36,27 @@ struct Handler {
|
|||||||
|
|
||||||
struct Vm {
|
struct Vm {
|
||||||
mut:
|
mut:
|
||||||
code []u8
|
code []u8
|
||||||
strings []string
|
strings []string
|
||||||
arrays [][]i64
|
arrays [][]i64
|
||||||
structs []StructVal
|
structs []StructVal
|
||||||
floats []f64
|
floats []f64
|
||||||
closures []Closure
|
closures []Closure
|
||||||
stack []i64
|
stack []i64
|
||||||
sp int
|
sp int
|
||||||
bp int
|
bp int
|
||||||
ip int
|
ip int
|
||||||
trace bool
|
trace bool
|
||||||
halted bool
|
halted bool
|
||||||
prog_args []string
|
prog_args []string
|
||||||
exit_code i64
|
exit_code i64
|
||||||
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
|
||||||
const_strs int // strings[0..const_strs] are bytecode constants, never collected
|
fns []obj.BinFn // function table (for stack traces)
|
||||||
last_heap int // heap size at the last GC check (allocation trigger)
|
const_strs int // strings[0..const_strs] are bytecode constants, never collected
|
||||||
build_root string // directory of the .vrmm build module (build_root() builtin)
|
last_heap int // heap size at the last GC check (allocation trigger)
|
||||||
|
build_root string // directory of the .vrmm build module (build_root() builtin)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bool_i64(b bool) i64 {
|
fn bool_i64(b bool) i64 {
|
||||||
|
|||||||
@@ -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,37 +347,67 @@ 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()!
|
||||||
if v.is_arr(h) && v.valid_arr_handle(h) {
|
// dynamic map read: m[key_expr] where the base is a struct and the
|
||||||
a := v.arrays[v.hand(h)]
|
// index evaluates to a string
|
||||||
if idx < 0 || idx >= a.len {
|
if v.is_struct(h) && v.valid_struct_handle(h) && v.is_str(idxv) && v.valid_handle(idxv) {
|
||||||
return error('array index ${idx} out of bounds (len ${a.len})')
|
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')
|
||||||
}
|
}
|
||||||
v.push(a[idx])!
|
|
||||||
} else if v.is_str(h) && v.valid_handle(h) {
|
|
||||||
// rune-based string indexing: s[i] is the i-th character
|
|
||||||
runes := v.strings[v.hand(h)].runes()
|
|
||||||
if idx < 0 || idx >= runes.len {
|
|
||||||
return error('string index ${idx} out of bounds (len ${runes.len})')
|
|
||||||
}
|
|
||||||
v.push(v.alloc_str(runes[idx].str()))!
|
|
||||||
} else {
|
} else {
|
||||||
return error('indexing a non-array, non-string value')
|
idx := int(v.dec_int(idxv))
|
||||||
|
if v.is_arr(h) && v.valid_arr_handle(h) {
|
||||||
|
a := v.arrays[v.hand(h)]
|
||||||
|
if idx < 0 || idx >= a.len {
|
||||||
|
return error('array index ${idx} out of bounds (len ${a.len})')
|
||||||
|
}
|
||||||
|
v.push(a[idx])!
|
||||||
|
} else if v.is_str(h) && v.valid_handle(h) {
|
||||||
|
// rune-based string indexing: s[i] is the i-th character
|
||||||
|
runes := v.strings[v.hand(h)].runes()
|
||||||
|
if idx < 0 || idx >= runes.len {
|
||||||
|
return error('string index ${idx} out of bounds (len ${runes.len})')
|
||||||
|
}
|
||||||
|
v.push(v.alloc_str(runes[idx].str()))!
|
||||||
|
} else {
|
||||||
|
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()!
|
||||||
if !v.is_arr(h) || !v.valid_arr_handle(h) {
|
// dynamic map write: m[key_expr] = v (string key on a struct)
|
||||||
return error('indexing a non-array value')
|
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) {
|
||||||
|
return error('indexing a non-array value')
|
||||||
|
}
|
||||||
|
if idx < 0 || idx >= v.arrays[v.hand(h)].len {
|
||||||
|
return error('array index ${idx} out of bounds (len ${v.arrays[v.hand(h)].len})')
|
||||||
|
}
|
||||||
|
v.arrays[v.hand(h)][idx] = val
|
||||||
}
|
}
|
||||||
if idx < 0 || idx >= v.arrays[v.hand(h)].len {
|
|
||||||
return error('array index ${idx} out of bounds (len ${v.arrays[v.hand(h)].len})')
|
|
||||||
}
|
|
||||||
v.arrays[v.hand(h)][idx] = val
|
|
||||||
}
|
}
|
||||||
op_alen {
|
op_alen {
|
||||||
v.ip++
|
v.ip++
|
||||||
@@ -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]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// shifting left (or no shift): copy forwards
|
||||||
|
for i := 0; i < argc; i++ {
|
||||||
|
v.stack[c + n + i] = v.stack[c + 1 + i]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
v.sp-- // drop the duplicated arg tail; closure slot was consumed
|
for i in 0..n {
|
||||||
v.call(entry, argc)
|
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,19 +728,22 @@ 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)
|
||||||
mut new_fields := []Field{}
|
if ok {
|
||||||
for f in s.fields {
|
mut new_fields := []Field{}
|
||||||
if f.name != fname {
|
for i, f in s.fields {
|
||||||
new_fields << f
|
if i != idx {
|
||||||
|
new_fields << f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.fields = new_fields
|
||||||
|
s.by_name = v.index_fields(new_fields)
|
||||||
|
v.structs[v.hand(h)] = s
|
||||||
}
|
}
|
||||||
|
v.push(h)!
|
||||||
}
|
}
|
||||||
s.fields = new_fields
|
|
||||||
v.structs[v.hand(h)] = s
|
|
||||||
v.push(h)!
|
|
||||||
}
|
|
||||||
|
|
||||||
// op_slen returns the number of fields in a struct.
|
// op_slen returns the number of fields in a struct.
|
||||||
// stack: struct → pushes field count.
|
// stack: struct → pushes field count.
|
||||||
|
|||||||
Reference in New Issue
Block a user