mirror of
https://github.com/bearlanguageorg/bear.git
synced 2026-08-26 15:37:18 +00:00
422 lines
18 KiB
Markdown
422 lines
18 KiB
Markdown
# vuurraaf/v
|
|
|
|
A complete toolchain for **VuurRaaf**, written in V from scratch: a compiler,
|
|
an assembler, a linker, and a stack-based runtime. Everything — including the
|
|
object file format and the virtual machine — lives in this repository.
|
|
|
|
```
|
|
.vr --compiler--> .vobj --linker--> .vbin --vm--> output
|
|
.vasm --assembler--> .vobj
|
|
```
|
|
|
|
## Build
|
|
|
|
Requires [V](https://vlang.io) (`v` in your PATH).
|
|
|
|
```bash
|
|
v -o bin/vr . # build the toolchain
|
|
./bin/vr up # or rebuild from inside the toolchain
|
|
./bin/vr symlink # optionally symlink bin/vr into your PATH
|
|
```
|
|
|
|
> Note: build with `v .` from the project root. Building via an explicit file
|
|
> or path argument makes V pick tcc without the Boehm GC, a combination that
|
|
> miscompiles this codebase (`vr up` already does the right thing).
|
|
|
|
## Usage
|
|
|
|
```
|
|
vr compile <file.vr> [-o out.vobj] source -> object
|
|
vr assemble <file.vasm> [-o out.vobj] assembly -> object
|
|
vr link <a.vobj> [more.vobj ...] [-o out] objects -> executable (.vbin)
|
|
vr run <file.vr|file.vbin> compile+link+run, or run a binary
|
|
vr debug <file.vr|file.vbin> run with an instruction trace
|
|
vr test <file.vr> run every test_* function
|
|
vr bench <file.vr> [iterations] benchmark main()
|
|
vr make [target] [args...] run build.vrmm (target = main)
|
|
vr make -f <file.vrmm> [target] [args...] run another build module
|
|
vr build [target] [args...] alias for make
|
|
vr clean remove .vobj/.vbin artifacts
|
|
vr up rebuild bin/vr
|
|
vr symlink link bin/vr into your PATH
|
|
vr config [set <key> <value>] toolchain config (outdir, verbose)
|
|
vr repl interactive session
|
|
vr lsp language server (JSON-RPC over stdio)
|
|
vr fmt [-w] <file.vr> format source (keeps comments)
|
|
vr init [name] scaffold a project (vr.mod + main.vr)
|
|
vr get <owner/repo | git-url | ./path> fetch a package into vendor/
|
|
vr install install dependencies from vr.mod
|
|
vr list show the project manifest
|
|
vr info | loader | alloc | version | help
|
|
```
|
|
|
|
```bash
|
|
./bin/vr repl # try expressions and functions interactively
|
|
./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 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:
|
|
|
|
```bash
|
|
./bin/vr run examples/hello.vr # run a program
|
|
./bin/vr test examples/tests.vr # run the tests (one fails on purpose)
|
|
./bin/vr debug examples/hello.vr # watch every bytecode instruction
|
|
|
|
# the assembler path
|
|
./bin/vr assemble examples/math.vasm -o math.vobj
|
|
./bin/vr link math.vobj -o math.vbin
|
|
./bin/vr run math.vbin
|
|
|
|
# multi-file programs (functions in one file may call functions in another)
|
|
./bin/vr compile examples/lib.vr -o lib.vobj
|
|
./bin/vr compile examples/use_lib.vr -o use_lib.vobj
|
|
./bin/vr link lib.vobj use_lib.vobj -o use_lib.vbin
|
|
./bin/vr run use_lib.vbin
|
|
```
|
|
|
|
## Build modules (.vrmm)
|
|
|
|
A **VuurRaaf Make Module** (`.vrmm`) is build instructions for the toolchain,
|
|
written in VuurRaaf itself — the same idea as V's `.vsh` scripts. The toolchain
|
|
compiles the module and runs one of its functions (a *target*) with the
|
|
`build_*` builtins available, so the script can drive every stage of the
|
|
pipeline: compile, assemble, link, run, test, bench, clean, and shell out to
|
|
the host.
|
|
|
|
```
|
|
# build.vrmm
|
|
fn main() {
|
|
build_compile("main.vr", "main.vobj")
|
|
build_link(["main.vobj"], "main.vbin")
|
|
}
|
|
|
|
fn clean() {
|
|
build_clean()
|
|
}
|
|
```
|
|
|
|
```bash
|
|
vr make # runs main() from build.vrmm
|
|
vr make clean # runs the clean() target
|
|
vr make deploy --prod # runs deploy() with args() == ["--prod"]
|
|
vr make -f x.vrmm t # run target t from another module
|
|
```
|
|
|
|
A target that returns a nonzero integer, calls `exit(n)` with `n > 0`, or
|
|
`throw`s fails the build. Paths are relative to the working directory;
|
|
`build_root()` returns the module's own directory for absolute paths.
|
|
|
|
### Running build modules as scripts
|
|
|
|
A `.vrmm` (or `.vr`) file may start with a shebang line so it can be executed
|
|
directly like any script — the toolchain skips the shebang when compiling, so
|
|
error line numbers stay aligned with the file:
|
|
|
|
```
|
|
#!/usr/bin/env vr
|
|
fn main() {
|
|
build_compile("main.vr", "main.vobj")
|
|
build_link(["main.vobj"], "main.vbin")
|
|
}
|
|
```
|
|
|
|
```bash
|
|
chmod +x build.vrmm
|
|
./build.vrmm # equivalent to: vr make
|
|
./build.vrmm clean # run the clean() target
|
|
./build.vrmm deploy --prod
|
|
```
|
|
|
|
The kernel invokes `vr <script> [args...]`; the toolchain routes an existing
|
|
`.vrmm` path to `vr make -f <file>` and an existing `.vr` path to
|
|
`vr run <file>`, so `vr myprog.vr` also just works.
|
|
|
|
Build builtins:
|
|
|
|
| builtin | description |
|
|
|---------|-------------|
|
|
| `build_compile(src, out)` | source → object (out defaults to `src.vobj`); returns the out path |
|
|
| `build_assemble(src, out)` | `.vasm` → object; returns the out path |
|
|
| `build_link(objs, out)` | objects → executable; returns the out path |
|
|
| `build_run(file)` | compile+link+run a `.vr`, or run a `.vbin`; returns the exit code |
|
|
| `build_test(file)` | run every `test_*` function; throws if any fail |
|
|
| `build_bench(file, n)` | benchmark `main()` n times |
|
|
| `build_clean()` | remove `.vobj`/`.vbin` in the cwd; returns the count |
|
|
| `build_exec(cmd)` | run a shell command; returns its output (throws on nonzero exit) |
|
|
| `build_exec_status(cmd)` | run a shell command; returns its exit code |
|
|
| `build_exists(path)` | 1 if the path exists, else 0 |
|
|
| `build_mkdir(path)` | create a directory (and parents) |
|
|
| `build_rm(path)` | remove a file or directory tree; returns 1 if something was removed |
|
|
| `build_copy(src, dst)` | copy a file or a whole directory tree |
|
|
| `build_glob(pattern)` | list files matching a glob (e.g. `"src/*.vr"`) |
|
|
| `build_ls(dir)` | list a directory's entries |
|
|
| `build_base(path)` / `build_dir(path)` / `build_join(a, b)` | path helpers |
|
|
| `build_root()` | absolute directory of the running `.vrmm` |
|
|
|
|
`vr init` scaffolds a project with a working `build.vrmm`; see
|
|
`examples/build.vrmm` for a tour (targets: `main`, `multi`, `test`, `bench`,
|
|
`deploy`, `clean`).
|
|
|
|
## Standard library modules
|
|
|
|
The toolchain ships a small stdlib in `lib/`, imported by name like in V
|
|
(`import os` resolves against `lib/` and `vendor/` from any directory).
|
|
Module functions are namespaced: `os.exists()`, `json.encode()`, ...
|
|
|
|
| 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`, `exec_full` (`{code, stdout, stderr}`), `ext`, `abs`, `rel` |
|
|
| `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` |
|
|
| `math` | `abs`, `min`, `max`, `floor`, `ceil`, `round`, `sqrt`, `pow`, `clamp`, `sign`, `pi` |
|
|
| `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")`, `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`.
|
|
|
|
## The VuurRaaf language
|
|
|
|
A small, V-flavored language. Values are 64-bit integers, 64-bit floats,
|
|
strings, arrays, structs, enums, and closures (strings concatenate with `+`
|
|
and compare with `==`/`!=`; arrays and structs are mutable references that
|
|
compare by identity).
|
|
|
|
```
|
|
fn sum(items) {
|
|
let total = 0
|
|
for x in items { // iterate an array
|
|
total = total + x
|
|
}
|
|
return total
|
|
}
|
|
|
|
fn main() {
|
|
let x = 6 * 7
|
|
assert x == 42
|
|
let big = x > 40 and x < 50 // and / or / not, short-circuiting
|
|
if big {
|
|
println("x is big")
|
|
} else {
|
|
println("x is small")
|
|
}
|
|
|
|
let a = [10, 20, 30]
|
|
a[1] = 99 // index assignment
|
|
push(a, 40) // grow in place
|
|
println(a) // [10, 99, 30, 40]
|
|
println(len(a)) // 4
|
|
println(sum(a)) // 179
|
|
|
|
for i in 0..5 { ... } // 0 1 2 3 4 (exclusive ..)
|
|
for i in 1...3 { ... } // 1 2 3 (inclusive ...)
|
|
|
|
for i in 0..10 {
|
|
if i == 2 {
|
|
continue // skip this iteration
|
|
}
|
|
if i == 5 {
|
|
break // leave the loop early
|
|
}
|
|
}
|
|
|
|
let grid = [[1, 2], [3, 4]] // nested arrays
|
|
println(grid[1][0]) // 3
|
|
|
|
let i = 100
|
|
for i in 0..3 { ... } // loop vars are scoped to the loop
|
|
println(i) // 100
|
|
|
|
if score >= 90 { // else-if chains
|
|
grade = "A"
|
|
} else if score >= 80 {
|
|
grade = "B"
|
|
} else {
|
|
grade = "F"
|
|
}
|
|
|
|
match day { // match on any comparable value
|
|
"sat" {
|
|
println("weekend")
|
|
}
|
|
"sun" {
|
|
println("weekend")
|
|
}
|
|
else { // optional fallback arm
|
|
println("workday")
|
|
}
|
|
}
|
|
|
|
let pt = { x: 3, y: 4 } // struct literal: { name: value, ... }
|
|
println(pt.x) // 3 — field access
|
|
pt.y = 5 // field assignment
|
|
let p = { name: "amy", addr: { city: "nyc" } } // nested structs
|
|
println(p.addr.city) // nyc
|
|
}
|
|
```
|
|
|
|
- functions: `fn name(a, b) { ... }` with `return expr`; default parameter
|
|
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 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
|
|
`first[int](arr)` — the VM is dynamically typed, so type parameters erase
|
|
to a single function but arity and duplicates are validated
|
|
- variables: `let name = expr`, reassignment `name = expr`
|
|
- floats: `3.14`, `0.5`, `1e3` — float literals and `float(x)`; arithmetic
|
|
promotes to float; `floor` / `ceil` / `round` / `sqrt` / `pow` / `abs` /
|
|
`min` / `max` / `rand` / `rand_int`
|
|
- strings are UTF-8: `len(s)` counts characters, `s[i]` and `s[a..b]` index
|
|
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()`
|
|
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)`,
|
|
`push/insert/remove/pop/sort/reverse/clone/index_of/join`; array literals
|
|
may nest
|
|
- error handling: `try { ... } catch e { ... }` and `throw "message"` — the
|
|
runtime unwinds to the nearest catch
|
|
- bitwise operators: `& | ^ ~ << >>`
|
|
- host builtins: `read_file` / `write_file`, `args()`, `getenv` / `setenv`,
|
|
`exit`, `sleep`, `time()`, `type(x)`, `str(x)`, `int(x)`, `split` / `join`
|
|
- HTTP: `http_get(url)` / `http_post(url, data)` return `{status, body}`
|
|
structs and throw on network failure (catchable) — or use `import http`
|
|
- date/time: `now()` (epoch s), `time_ms()`, `format_time(t, "YYYY-MM-DD")`,
|
|
`parse_time(s)` (ISO timestamps) — or use `import time`
|
|
- JSON: `json_encode(x)` / `json_decode(s)` — objects become structs, arrays
|
|
become arrays, integral numbers decode as ints, `null` decodes to `none`
|
|
- `none`: a literal for "no value" (JSON null); `x == none` compares,
|
|
and it renders as `none` / encodes as `null`
|
|
- string formatting: `format(x, "%.2f")` (printf-style: `%d %i %f %s %x %X`,
|
|
width, `-`/`0` flags, precision), `replace`, `split_lines`, `pad` /
|
|
`pad_left`, `repeat` — all also available as string methods (`s.replace()`,
|
|
`s.pad(4)`, ...)
|
|
- native builtin errors (failed `read_file`, `json_decode`, ...) are caught by
|
|
`try { } catch e { }` like explicit `throw`s
|
|
- for loops: `for x in arr { }` and ranges `for i in 0..10 { }` /
|
|
`for i in 0...10 { }`; loop variables are scoped to the loop body
|
|
- `break` / `continue` inside `while` and `for` loops (in `for` loops
|
|
`continue` advances the loop variable / iterator first)
|
|
- else-if chains: `if a { } else if b { } else { }`
|
|
- `match`: `match expr { v1 { } v2 { } else { } }` — arms test equality on
|
|
any comparable value (ints, strings, ...); the `else` arm is optional
|
|
- structs: literals `{ name: value, ... }` (may nest and may be empty `{}`),
|
|
field access `a.b` and assignment `a.b = v` (chained: `a[i].b`, `a.b[i]`);
|
|
structs are mutable references (identity `==`/`!=`), and setting a missing
|
|
field adds it, so records can be built incrementally
|
|
- enums: `enum Color { red green blue }` with `Color.red`, `e.to_string()`,
|
|
`e.count()`, and iteration in `for`
|
|
- constants: `const NAME = 42` (compile-time integer/bool values)
|
|
- operators: `+ - * / %`, `== != < <= > >=`, `and or not`, `& | ^ ~ << >>`,
|
|
unary `-`; constant expressions fold at compile time
|
|
- statements: `let`, assignment, `if/else`, `match`, `while`, `for`,
|
|
`break`, `continue`, `return`, `assert`, `try/catch`/`throw`, calls,
|
|
`print(...)` / `println(...)`
|
|
- comments: `//`
|
|
|
|
## Assembly
|
|
|
|
`.vasm` files talk to the VM directly. Labels, `.global` exports, and the full
|
|
opcode set:
|
|
|
|
```
|
|
; comment
|
|
.global main
|
|
main:
|
|
push_int 42
|
|
call helper 1 ; call <target> <argc>
|
|
println
|
|
halt
|
|
|
|
.global helper
|
|
helper:
|
|
enter 0 ; reserve extra locals (args were copied in by `call`)
|
|
load 0
|
|
retv
|
|
```
|
|
|
|
Opcodes: `halt push_int push_str load store pop dup add sub mul div mod neg
|
|
eq ne lt le gt ge and or not jmp jz jnz call ret retv print println assert
|
|
enter mkarray aget aset alen apush mkstruct sget sset`.
|
|
|
|
Struct opcodes: `mkstruct n` pops `n` (name, value) pairs and pushes a struct
|
|
handle; `sget "field"` / `sset "field"` read/write a named field (pushing the
|
|
field name as a string first, exactly like the compiler does).
|
|
|
|
## Formats
|
|
|
|
- **VROBJ** (`.vobj`) — linker input: bytecode, exported symbols (function
|
|
name -> code offset), string constants, and relocations (call sites and
|
|
string references).
|
|
- **VRBIN** (`.vbin`) — the executable: function table, string table, bytecode.
|
|
|
|
## Architecture
|
|
|
|
| module | role |
|
|
|--------------|-------------------------------------------------------------|
|
|
| `compiler/` | lexer, parser, type checker, bytecode codegen (VROBJ) |
|
|
| `assembler/` | `.vasm` -> VROBJ |
|
|
| `linker/` | resolves relocations, rebases strings, emits VRBIN |
|
|
| `vm/` | stack VM: tagged values, call frames, string/array heaps, hash-indexed structs, GC-traced closure captures |
|
|
| `obj/` | VROBJ/VRBIN binary formats |
|
|
| `bin/` | small standalone tools: `tl_alloc.v`, `tl_loader.v` |
|
|
|
|
The VM is a stack machine with 64-bit tagged values using three tag bits:
|
|
ints, string/array/struct/float/closure handles — so no integer ever
|
|
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
|
|
every frame's locals) and compacting the pools; string constants baked into
|
|
bytecode are never collected. Structs carry a hash index, so map-style field
|
|
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,
|
|
field access on numbers, arithmetic on strings, wrong arity) while leaving
|
|
dynamic programs alone. `vr debug` prints every instruction with the stack
|
|
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
|
|
|
|
```
|
|
main.v CLI entry point (vr <command> ...)
|
|
repl.v interactive REPL
|
|
fmt.v source formatter
|
|
pkg.v package manager (init/get/install/list)
|
|
vm/native.v host builtins incl. the build_* (.vrmm) builtins
|
|
v.mod module definition
|
|
compiler/ assembler/ linker/ vm/ obj/ the toolchain itself
|
|
bin/ built binary + standalone tools
|
|
examples/ runnable examples (hello, lib, asm, tests, fib, json)
|
|
```
|