mirror of
https://github.com/bearlanguageorg/bear.git
synced 2026-08-26 14:17:18 +00:00
Enhacnementsand toolchain
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
bin/vr
|
||||
*.vobj
|
||||
*.vbin
|
||||
@@ -1 +1,162 @@
|
||||
// SOON
|
||||
# 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 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 info | loader | alloc | version | help
|
||||
```
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
## The VuurRaaf language
|
||||
|
||||
A small, V-flavored language. Everything is a 64-bit integer or a string
|
||||
(strings concatenate with `+` and compare with `==`/`!=`; `print`/`println`
|
||||
accept both).
|
||||
|
||||
```
|
||||
fn greet(name) {
|
||||
println("hello, " + name + "!")
|
||||
}
|
||||
|
||||
fn fib(n) {
|
||||
if n < 2 {
|
||||
return n
|
||||
}
|
||||
return fib(n - 1) + fib(n - 2)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let x = 6 * 7 // let with type inference
|
||||
assert x == 42 // checked by the vm at runtime
|
||||
let big = x > 40 and x < 50 // and / or / not, short-circuiting
|
||||
if big {
|
||||
println("x is big")
|
||||
} else {
|
||||
println("x is small")
|
||||
}
|
||||
let i = 0
|
||||
while i < 3 {
|
||||
println("counting " + i) // "counting 0", ...
|
||||
i = i + 1
|
||||
}
|
||||
println(fib(10)) // 55
|
||||
}
|
||||
```
|
||||
|
||||
- functions: `fn name(a, b) { ... }` with `return expr`
|
||||
- variables: `let name = expr`, reassignment `name = expr`
|
||||
- operators: `+ - * / %`, `== != < <= > >=`, `and or not`, unary `-`
|
||||
- statements: `let`, assignment, `if/else`, `while`, `return`, `assert`, 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`.
|
||||
|
||||
## 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, recursive-descent parser, bytecode codegen (VROBJ) |
|
||||
| `assembler/` | `.vasm` -> VROBJ |
|
||||
| `linker/` | resolves relocations, rebases strings, emits VRBIN |
|
||||
| `vm/` | stack VM: tagged values, call frames, string heap, tracing |
|
||||
| `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 (numbers are stored
|
||||
shifted left so no integer collides with a string handle). Calls push a frame
|
||||
(return address, base pointer, argc), copy arguments into local slots, and
|
||||
reserve extra locals with `enter n`. `vr debug` prints every instruction with
|
||||
the stack contents.
|
||||
|
||||
## Repository layout
|
||||
|
||||
```
|
||||
main.v CLI entry point (vr <command> ...)
|
||||
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)
|
||||
```
|
||||
|
||||
@@ -0,0 +1,311 @@
|
||||
// assembler.v — assembles VuurRaaf assembly (.vasm) into VROBJ object files.
|
||||
//
|
||||
// Assembly syntax:
|
||||
// ; comment
|
||||
// .global main ; export the label as a linkable symbol
|
||||
// main: ; label definition (local unless .global)
|
||||
// push_int 42
|
||||
// push_str "hello"
|
||||
// call helper 1 ; call <symbol-or-label> <argc>
|
||||
// println
|
||||
// halt
|
||||
//
|
||||
// Jumps (jmp/jz/jnz) target local labels and are resolved here; calls to
|
||||
// exported symbols become relocations resolved by the linker.
|
||||
module assembler
|
||||
|
||||
import os
|
||||
import obj
|
||||
|
||||
pub fn assemble(src string) !obj.Obj {
|
||||
lines := src.split_into_lines()
|
||||
// pass 1: collect labels, globals, and code offsets
|
||||
mut labels := map[string]int{}
|
||||
mut globals := map[string]bool{}
|
||||
mut offset := 0
|
||||
for raw in lines {
|
||||
line := clean(raw)
|
||||
if line.len == 0 {
|
||||
continue
|
||||
}
|
||||
if line.starts_with('.global') {
|
||||
name := line.all_after('.global').trim_space()
|
||||
if name.len == 0 {
|
||||
return error('`.global` needs a name')
|
||||
}
|
||||
globals[name] = true
|
||||
continue
|
||||
}
|
||||
if line.ends_with(':') {
|
||||
name := line[..line.len - 1].trim_space()
|
||||
if name in labels {
|
||||
return error('duplicate label "${name}"')
|
||||
}
|
||||
labels[name] = offset
|
||||
continue
|
||||
}
|
||||
offset += instr_len(line)!
|
||||
}
|
||||
// pass 2: emit code
|
||||
mut o := obj.Obj{}
|
||||
for raw in lines {
|
||||
line := clean(raw)
|
||||
if line.len == 0 || line.starts_with('.global') || line.ends_with(':') {
|
||||
continue
|
||||
}
|
||||
parts := split_line(line)!
|
||||
op := parts[0]
|
||||
arg := if parts.len > 1 { parts[1] } else { '' }
|
||||
match op {
|
||||
'halt' {
|
||||
o.code << u8(0)
|
||||
}
|
||||
'push_int' {
|
||||
val := parse_int(arg, 'push_int')!
|
||||
o.code << u8(1)
|
||||
o.code << obj.encode_i64(val)
|
||||
}
|
||||
'push_str' {
|
||||
s := unquote(arg)!
|
||||
o.code << u8(2)
|
||||
o.code << obj.encode_i64(0) // placeholder — rebased by the linker
|
||||
o.relocs << obj.Reloc{ offset: u32(o.code.len) - 8, name: s, kind: 1 }
|
||||
}
|
||||
'load' {
|
||||
o.code << u8(3)
|
||||
o.code << obj.encode_i64(parse_int(arg, 'load')!)
|
||||
}
|
||||
'store' {
|
||||
o.code << u8(4)
|
||||
o.code << obj.encode_i64(parse_int(arg, 'store')!)
|
||||
}
|
||||
'pop' {
|
||||
o.code << u8(5)
|
||||
}
|
||||
'dup' {
|
||||
o.code << u8(6)
|
||||
}
|
||||
'add' {
|
||||
o.code << u8(7)
|
||||
}
|
||||
'sub' {
|
||||
o.code << u8(8)
|
||||
}
|
||||
'mul' {
|
||||
o.code << u8(9)
|
||||
}
|
||||
'div' {
|
||||
o.code << u8(10)
|
||||
}
|
||||
'mod' {
|
||||
o.code << u8(11)
|
||||
}
|
||||
'neg' {
|
||||
o.code << u8(12)
|
||||
}
|
||||
'eq' {
|
||||
o.code << u8(13)
|
||||
}
|
||||
'ne' {
|
||||
o.code << u8(14)
|
||||
}
|
||||
'lt' {
|
||||
o.code << u8(15)
|
||||
}
|
||||
'le' {
|
||||
o.code << u8(16)
|
||||
}
|
||||
'gt' {
|
||||
o.code << u8(17)
|
||||
}
|
||||
'ge' {
|
||||
o.code << u8(18)
|
||||
}
|
||||
'and' {
|
||||
o.code << u8(19)
|
||||
}
|
||||
'or' {
|
||||
o.code << u8(20)
|
||||
}
|
||||
'not' {
|
||||
o.code << u8(21)
|
||||
}
|
||||
'jmp' {
|
||||
o.code << u8(22)
|
||||
o.code << obj.encode_i64(i64(label(labels, arg)!))
|
||||
}
|
||||
'jz' {
|
||||
o.code << u8(23)
|
||||
o.code << obj.encode_i64(i64(label(labels, arg)!))
|
||||
}
|
||||
'jnz' {
|
||||
o.code << u8(24)
|
||||
o.code << obj.encode_i64(i64(label(labels, arg)!))
|
||||
}
|
||||
'call' {
|
||||
o.code << u8(25)
|
||||
o.code << obj.encode_i64(0) // placeholder for the target
|
||||
if arg in labels && arg !in globals {
|
||||
obj.patch_i64(mut o.code, u32(o.code.len) - 8, i64(labels[arg]))
|
||||
} else {
|
||||
// reference to an exported symbol — resolved at link time
|
||||
o.relocs << obj.Reloc{ offset: u32(o.code.len) - 8, name: arg, kind: 0 }
|
||||
}
|
||||
argc := if parts.len > 2 { parse_int(parts[2], 'call argc')! } else { i64(0) }
|
||||
o.code << obj.encode_i64(argc)
|
||||
}
|
||||
'ret' {
|
||||
o.code << u8(26)
|
||||
}
|
||||
'retv' {
|
||||
o.code << u8(27)
|
||||
}
|
||||
'print' {
|
||||
o.code << u8(28)
|
||||
}
|
||||
'println' {
|
||||
o.code << u8(29)
|
||||
}
|
||||
'assert' {
|
||||
o.code << u8(30)
|
||||
}
|
||||
'enter' {
|
||||
o.code << u8(31)
|
||||
o.code << obj.encode_i64(parse_int(arg, 'enter')!)
|
||||
}
|
||||
else {
|
||||
return error('unknown instruction "${op}"')
|
||||
}
|
||||
}
|
||||
}
|
||||
// export the globals as symbols
|
||||
for name in globals.keys() {
|
||||
entry := labels[name] or { return error('.global "${name}" is not a defined label') }
|
||||
o.symbols << obj.Symbol{ name: name, entry: entry }
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
||||
pub fn assemble_file(path string) !obj.Obj {
|
||||
src := os.read_file(path)!
|
||||
return assemble(src)!
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn clean(line string) string {
|
||||
// strip ';' comments and trim
|
||||
if idx := line.index(';') {
|
||||
return line[..idx].trim_space()
|
||||
}
|
||||
return line.trim_space()
|
||||
}
|
||||
|
||||
// split_line splits an assembly line into whitespace-separated fields while
|
||||
// keeping a double-quoted string (which may contain spaces) as a single field.
|
||||
fn split_line(line string) ![]string {
|
||||
mut parts := []string{}
|
||||
mut i := 0
|
||||
for i < line.len {
|
||||
for i < line.len && (line[i] == ` ` || line[i] == `\t`) {
|
||||
i++
|
||||
}
|
||||
if i >= line.len {
|
||||
break
|
||||
}
|
||||
if line[i] == `"` {
|
||||
// scan to the closing quote
|
||||
start := i
|
||||
i++
|
||||
for i < line.len && line[i] != `"` {
|
||||
i++
|
||||
}
|
||||
i++
|
||||
parts << line[start..if i <= line.len { i } else { line.len }]
|
||||
} else {
|
||||
start := i
|
||||
for i < line.len && line[i] != ` ` && line[i] != `\t` {
|
||||
i++
|
||||
}
|
||||
parts << line[start..i]
|
||||
}
|
||||
}
|
||||
return parts
|
||||
}
|
||||
|
||||
fn instr_len(line string) !int {
|
||||
parts := split_line(line)!
|
||||
op := parts[0]
|
||||
match op {
|
||||
'halt', 'pop', 'dup', 'add', 'sub', 'mul', 'div', 'mod', 'neg', 'eq', 'ne', 'lt', 'le',
|
||||
'gt', 'ge', 'and', 'or', 'not', 'ret', 'retv', 'print', 'println', 'assert' {
|
||||
return 1
|
||||
}
|
||||
'push_int', 'push_str', 'load', 'store', 'jmp', 'jz', 'jnz', 'enter' {
|
||||
return 9
|
||||
}
|
||||
'call' {
|
||||
return 17 // opcode + target(8) + argc(8)
|
||||
}
|
||||
else {
|
||||
return error('unknown instruction "${op}"')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn label(labels map[string]int, name string) !int {
|
||||
return labels[name] or { return error('unknown label "${name}"') }
|
||||
}
|
||||
|
||||
fn parse_int(s string, what string) !i64 {
|
||||
if s.len == 0 {
|
||||
return error('${what}: bad integer "${s}"')
|
||||
}
|
||||
start := if s[0] == `-` { 1 } else { 0 }
|
||||
if start >= s.len {
|
||||
return error('${what}: bad integer "${s}"')
|
||||
}
|
||||
for i in start..s.len {
|
||||
if s[i] < `0` || s[i] > `9` {
|
||||
return error('${what}: bad integer "${s}"')
|
||||
}
|
||||
}
|
||||
return s.i64()
|
||||
}
|
||||
|
||||
fn unquote(s string) !string {
|
||||
if s.len < 2 || s[0] != `"` || s[s.len - 1] != `"` {
|
||||
return error('expected a string literal, got "${s}"')
|
||||
}
|
||||
inner := s[1..s.len - 1]
|
||||
mut out := ''
|
||||
mut i := 0
|
||||
for i < inner.len {
|
||||
c := inner[i]
|
||||
if c == `\\` && i + 1 < inner.len {
|
||||
i++
|
||||
match inner[i] {
|
||||
`n` {
|
||||
out += '\n'
|
||||
}
|
||||
`t` {
|
||||
out += '\t'
|
||||
}
|
||||
`"` {
|
||||
out += '"'
|
||||
}
|
||||
`\\` {
|
||||
out += '\\'
|
||||
}
|
||||
else {
|
||||
out += '\\${inner[i].ascii_str()}'
|
||||
}
|
||||
}
|
||||
} else {
|
||||
out += c.ascii_str()
|
||||
}
|
||||
i++
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
// toolchain alloc
|
||||
// A tiny byte arena for toolchain scratch memory: allocate fixed-size blocks
|
||||
// from a pre-sized arena and release them all at once.
|
||||
// Run standalone with: v run bin/tl_alloc.v
|
||||
|
||||
module main
|
||||
|
||||
struct Arena {
|
||||
mut:
|
||||
data []u8
|
||||
used int
|
||||
}
|
||||
|
||||
// new_arena reserves `cap` bytes of scratch memory.
|
||||
fn new_arena(cap int) Arena {
|
||||
return Arena{
|
||||
data: []u8{len: cap}
|
||||
used: 0
|
||||
}
|
||||
}
|
||||
|
||||
// alloc returns an offset into the arena for a block of `n` bytes.
|
||||
fn (mut a Arena) alloc(n int) !int {
|
||||
if a.used + n > a.data.len {
|
||||
return error('arena out of memory: need ${n} bytes, ${a.data.len - a.used} left')
|
||||
}
|
||||
off := a.used
|
||||
a.used += n
|
||||
return off
|
||||
}
|
||||
|
||||
// free_all releases everything allocated so far.
|
||||
fn (mut a Arena) free_all() {
|
||||
a.used = 0
|
||||
}
|
||||
|
||||
fn (a Arena) stats() string {
|
||||
return 'arena: ${a.used} / ${a.data.len} bytes used'
|
||||
}
|
||||
|
||||
fn main() {
|
||||
mut arena := new_arena(1024 * 1024) // 1 MiB
|
||||
println('toolchain alloc: arena ready')
|
||||
println(arena.stats())
|
||||
off1 := arena.alloc(64) or { eprintln(err); exit(1) }
|
||||
off2 := arena.alloc(4096) or { eprintln(err); exit(1) }
|
||||
println('allocated 64 bytes at offset ${off1}')
|
||||
println('allocated 4096 bytes at offset ${off2}')
|
||||
println(arena.stats())
|
||||
arena.free_all()
|
||||
println('after free_all: ${arena.stats()}')
|
||||
}
|
||||
+47
-22
@@ -1,28 +1,53 @@
|
||||
// toolchain loader
|
||||
// Discovers and describes the toolchain components baked into the binary.
|
||||
// Run standalone with: v run bin/tl_loader.v
|
||||
|
||||
module tl_loader
|
||||
module main
|
||||
|
||||
import os
|
||||
import v.vmod
|
||||
|
||||
pub fn load_toolchain() {
|
||||
// Load the toolchain modules and initialize them
|
||||
// We call the package functions to load the toolchain modules and initialize them
|
||||
// each package has their own load argument, which is passed to the package function.
|
||||
toolchain_loader(load)
|
||||
toolchain_alloc(load)
|
||||
toolchain_compile(load)
|
||||
toolchain_assemble(load)
|
||||
toolchain_link(load)
|
||||
toolchain_run(load)
|
||||
toolchain_debug(load)
|
||||
toolchain_help(load)
|
||||
toolchain_version(load)
|
||||
toolchain_info(load)
|
||||
toolchain_config(load)
|
||||
toolchain_test(load)
|
||||
toolchain_bench(load)
|
||||
toolchain_clean(load)
|
||||
toolchain_up(load)
|
||||
toolchain_symlink(load)
|
||||
struct Component {
|
||||
mut:
|
||||
name string
|
||||
role string
|
||||
path string
|
||||
enabled bool
|
||||
}
|
||||
|
||||
// load_toolchain returns the components that make up this toolchain.
|
||||
fn load_toolchain() []Component {
|
||||
root := os.dir(os.executable())
|
||||
return [
|
||||
Component{
|
||||
name: 'compiler'
|
||||
role: 'lexer, parser, bytecode codegen'
|
||||
path: os.join_path(root, 'compiler')
|
||||
enabled: true
|
||||
},
|
||||
Component{
|
||||
name: 'assembler'
|
||||
role: 'vasm -> vobj'
|
||||
path: os.join_path(root, 'assembler')
|
||||
enabled: true
|
||||
},
|
||||
Component{
|
||||
name: 'linker'
|
||||
role: 'vobj set -> vbin'
|
||||
path: os.join_path(root, 'linker')
|
||||
enabled: true
|
||||
},
|
||||
Component{
|
||||
name: 'vm'
|
||||
role: 'stack-based runtime'
|
||||
path: os.join_path(root, 'vm')
|
||||
enabled: true
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn main() {
|
||||
for c in load_toolchain() {
|
||||
status := if c.enabled { 'loaded' } else { 'disabled' }
|
||||
println('[${status}] ${c.name:-12} ${c.role} (${c.path})')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
// compiler.v — bytecode code generator for VuurRaaf.
|
||||
//
|
||||
// Compiles a parsed program into a VROBJ object file: flat bytecode plus a
|
||||
// symbol per function and a relocation per call site. Call targets are left as
|
||||
// relocations and resolved by the linker, so functions may live in other files.
|
||||
module compiler
|
||||
|
||||
import os
|
||||
import obj
|
||||
|
||||
// opcodes — keep in sync with vm/vm.v and assembler/assembler.v
|
||||
const op_halt = u8(0)
|
||||
const op_push_i = u8(1)
|
||||
const op_push_s = u8(2)
|
||||
const op_load = u8(3)
|
||||
const op_store = u8(4)
|
||||
const op_pop = u8(5)
|
||||
const op_dup = u8(6)
|
||||
const op_add = u8(7)
|
||||
const op_sub = u8(8)
|
||||
const op_mul = u8(9)
|
||||
const op_div = u8(10)
|
||||
const op_mod = u8(11)
|
||||
const op_neg = u8(12)
|
||||
const op_eq = u8(13)
|
||||
const op_ne = u8(14)
|
||||
const op_lt = u8(15)
|
||||
const op_le = u8(16)
|
||||
const op_gt = u8(17)
|
||||
const op_ge = u8(18)
|
||||
const op_and = u8(19)
|
||||
const op_or = u8(20)
|
||||
const op_not = u8(21)
|
||||
const op_jmp = u8(22)
|
||||
const op_jz = u8(23)
|
||||
const op_jnz = u8(24)
|
||||
const op_call = u8(25)
|
||||
const op_ret = u8(26)
|
||||
const op_retv = u8(27)
|
||||
const op_print = u8(28)
|
||||
const op_println = u8(29)
|
||||
const op_assert = u8(30)
|
||||
const op_enter = u8(31)
|
||||
|
||||
// compile parses and compiles VuurRaaf source into an object file.
|
||||
pub fn compile(src string) !obj.Obj {
|
||||
toks := tokenize(src)!
|
||||
prog := parse(toks)!
|
||||
return gen(prog)
|
||||
}
|
||||
|
||||
pub fn compile_file(path string) !obj.Obj {
|
||||
src := os.read_file(path)!
|
||||
return compile(src)!
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
struct Fixup {
|
||||
name string
|
||||
off u32
|
||||
}
|
||||
|
||||
struct Gen {
|
||||
mut:
|
||||
code []u8
|
||||
strings []string
|
||||
str_map map[string]int
|
||||
symbols []obj.Symbol
|
||||
relocs []obj.Reloc
|
||||
locals map[string]int
|
||||
local_cnt int
|
||||
argc int
|
||||
cur_fn string
|
||||
labels map[string]int
|
||||
fixups []Fixup
|
||||
enter_off u32
|
||||
next_lbl int
|
||||
}
|
||||
|
||||
fn gen(prog Program) !obj.Obj {
|
||||
mut g := Gen{}
|
||||
for fd in prog.fns {
|
||||
g.gen_fn(fd)!
|
||||
}
|
||||
return obj.Obj{
|
||||
symbols: g.symbols
|
||||
strings: g.strings
|
||||
code: g.code
|
||||
relocs: g.relocs
|
||||
}
|
||||
}
|
||||
|
||||
fn (mut g Gen) gen_fn(fd FnDecl) ! {
|
||||
g.cur_fn = fd.name
|
||||
g.symbols << obj.Symbol{ name: fd.name, entry: g.code.len }
|
||||
g.locals.clear()
|
||||
g.local_cnt = 0
|
||||
g.argc = fd.params.len
|
||||
for i, p in fd.params {
|
||||
g.locals[p] = i
|
||||
}
|
||||
g.local_cnt = g.argc
|
||||
// `enter n` reserves the non-parameter locals; n is patched once the body
|
||||
// has been scanned.
|
||||
g.code << op_enter
|
||||
g.enter_off = u32(g.code.len)
|
||||
g.code << obj.encode_i64(0)
|
||||
for st in fd.body {
|
||||
g.gen_stmt(st)!
|
||||
}
|
||||
g.code << op_ret // trailing return for fall-through
|
||||
obj.patch_i64(mut g.code, g.enter_off, i64(g.local_cnt - g.argc))
|
||||
// resolve intra-function jump targets
|
||||
for f in g.fixups {
|
||||
target := g.labels[f.name] or {
|
||||
return error('internal error: unresolved label ${f.name} in fn ${fd.name}')
|
||||
}
|
||||
obj.patch_i64(mut g.code, f.off, i64(target))
|
||||
}
|
||||
g.fixups.clear()
|
||||
g.labels.clear()
|
||||
g.cur_fn = ''
|
||||
}
|
||||
|
||||
fn (mut g Gen) gen_stmt(st Stmt) ! {
|
||||
match st.kind {
|
||||
.expr_stmt {
|
||||
g.gen_expr(st.expr)!
|
||||
// print/println already consume their value; everything else
|
||||
// leaves one on the stack that must be discarded
|
||||
if st.expr.kind == .call && (st.expr.name == 'print' || st.expr.name == 'println') {
|
||||
// nothing to discard
|
||||
} else {
|
||||
g.code << op_pop
|
||||
}
|
||||
}
|
||||
.let_stmt {
|
||||
g.gen_expr(st.expr)!
|
||||
idx := g.local_cnt
|
||||
g.local_cnt++
|
||||
g.locals[st.target] = idx
|
||||
g.code << op_store
|
||||
g.code << obj.encode_i64(i64(idx))
|
||||
}
|
||||
.assign_stmt {
|
||||
idx := g.locals[st.target] or {
|
||||
return error('unknown variable "${st.target}" at line ${st.line}')
|
||||
}
|
||||
g.gen_expr(st.expr)!
|
||||
g.code << op_store
|
||||
g.code << obj.encode_i64(i64(idx))
|
||||
}
|
||||
.if_stmt {
|
||||
else_l := g.new_label()
|
||||
end_l := g.new_label()
|
||||
g.gen_expr(st.cond)!
|
||||
g.code << op_jz
|
||||
g.code << obj.encode_i64(0)
|
||||
g.fixups << Fixup{ name: else_l, off: u32(g.code.len) - 8 }
|
||||
for s in st.body {
|
||||
g.gen_stmt(s)!
|
||||
}
|
||||
g.code << op_jmp
|
||||
g.code << obj.encode_i64(0)
|
||||
g.fixups << Fixup{ name: end_l, off: u32(g.code.len) - 8 }
|
||||
g.emit_label(else_l)
|
||||
for s in st.els {
|
||||
g.gen_stmt(s)!
|
||||
}
|
||||
g.emit_label(end_l)
|
||||
}
|
||||
.while_stmt {
|
||||
loop_l := g.new_label()
|
||||
end_l := g.new_label()
|
||||
g.emit_label(loop_l)
|
||||
g.gen_expr(st.cond)!
|
||||
g.code << op_jz
|
||||
g.code << obj.encode_i64(0)
|
||||
g.fixups << Fixup{ name: end_l, off: u32(g.code.len) - 8 }
|
||||
for s in st.body {
|
||||
g.gen_stmt(s)!
|
||||
}
|
||||
g.code << op_jmp
|
||||
g.code << obj.encode_i64(0)
|
||||
g.fixups << Fixup{ name: loop_l, off: u32(g.code.len) - 8 }
|
||||
g.emit_label(end_l)
|
||||
}
|
||||
.ret_stmt {
|
||||
if st.has_val {
|
||||
g.gen_expr(st.expr)!
|
||||
g.code << op_retv
|
||||
} else {
|
||||
g.code << op_ret
|
||||
}
|
||||
}
|
||||
.assert_stmt {
|
||||
g.gen_expr(st.expr)!
|
||||
g.code << op_assert
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn (mut g Gen) gen_expr(e Expr) ! {
|
||||
match e.kind {
|
||||
.int_lit {
|
||||
g.code << op_push_i
|
||||
g.code << obj.encode_i64(e.int_v)
|
||||
}
|
||||
.str_lit {
|
||||
// the index is a placeholder; the linker rebases it via a string
|
||||
// relocation so multi-file links keep working
|
||||
g.code << op_push_s
|
||||
g.code << obj.encode_i64(0)
|
||||
g.relocs << obj.Reloc{ offset: u32(g.code.len) - 8, name: e.str_v, kind: 1 }
|
||||
}
|
||||
.bool_lit {
|
||||
g.code << op_push_i
|
||||
g.code << obj.encode_i64(e.int_v)
|
||||
}
|
||||
.ident {
|
||||
idx := g.locals[e.name] or {
|
||||
return error('unknown variable "${e.name}" at line ${e.line}')
|
||||
}
|
||||
g.code << op_load
|
||||
g.code << obj.encode_i64(i64(idx))
|
||||
}
|
||||
.unary {
|
||||
g.gen_expr(*e.right)!
|
||||
if e.op == .kw_not {
|
||||
g.code << op_not
|
||||
} else {
|
||||
g.code << op_neg
|
||||
}
|
||||
}
|
||||
.binary {
|
||||
g.gen_binary(e)!
|
||||
}
|
||||
.call {
|
||||
g.gen_call(e)!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn (mut g Gen) gen_call(e Expr) ! {
|
||||
if e.name == 'print' || e.name == 'println' {
|
||||
if e.args.len != 1 {
|
||||
return error('${e.name}() takes exactly one argument (line ${e.line})')
|
||||
}
|
||||
g.gen_expr(e.args[0])!
|
||||
g.code << if e.name == 'print' { op_print } else { op_println }
|
||||
return
|
||||
}
|
||||
for a in e.args {
|
||||
g.gen_expr(a)!
|
||||
}
|
||||
g.code << op_call
|
||||
g.code << obj.encode_i64(0) // placeholder — patched by the linker
|
||||
g.relocs << obj.Reloc{ offset: u32(g.code.len) - 8, name: e.name, kind: 0 }
|
||||
g.code << obj.encode_i64(i64(e.args.len)) // argc
|
||||
}
|
||||
|
||||
fn (mut g Gen) gen_binary(e Expr) ! {
|
||||
match e.op {
|
||||
.kw_and {
|
||||
// a and b → short-circuit: if !a or !b then 0 else 1
|
||||
false_l := g.new_label()
|
||||
end_l := g.new_label()
|
||||
g.gen_expr(*e.left)!
|
||||
g.code << op_jz
|
||||
g.code << obj.encode_i64(0)
|
||||
g.fixups << Fixup{ name: false_l, off: u32(g.code.len) - 8 }
|
||||
g.gen_expr(*e.right)!
|
||||
g.code << op_jz
|
||||
g.code << obj.encode_i64(0)
|
||||
g.fixups << Fixup{ name: false_l, off: u32(g.code.len) - 8 }
|
||||
g.code << op_push_i
|
||||
g.code << obj.encode_i64(1)
|
||||
g.code << op_jmp
|
||||
g.code << obj.encode_i64(0)
|
||||
g.fixups << Fixup{ name: end_l, off: u32(g.code.len) - 8 }
|
||||
g.emit_label(false_l)
|
||||
g.code << op_push_i
|
||||
g.code << obj.encode_i64(0)
|
||||
g.emit_label(end_l)
|
||||
}
|
||||
.kw_or {
|
||||
// a or b → short-circuit: if a or b then 1 else 0
|
||||
true_l := g.new_label()
|
||||
end_l := g.new_label()
|
||||
g.gen_expr(*e.left)!
|
||||
g.code << op_jnz
|
||||
g.code << obj.encode_i64(0)
|
||||
g.fixups << Fixup{ name: true_l, off: u32(g.code.len) - 8 }
|
||||
g.gen_expr(*e.right)!
|
||||
g.code << op_jnz
|
||||
g.code << obj.encode_i64(0)
|
||||
g.fixups << Fixup{ name: true_l, off: u32(g.code.len) - 8 }
|
||||
g.code << op_push_i
|
||||
g.code << obj.encode_i64(0)
|
||||
g.code << op_jmp
|
||||
g.code << obj.encode_i64(0)
|
||||
g.fixups << Fixup{ name: end_l, off: u32(g.code.len) - 8 }
|
||||
g.emit_label(true_l)
|
||||
g.code << op_push_i
|
||||
g.code << obj.encode_i64(1)
|
||||
g.emit_label(end_l)
|
||||
}
|
||||
else {
|
||||
g.gen_expr(*e.left)!
|
||||
g.gen_expr(*e.right)!
|
||||
op := match e.op {
|
||||
.plus { op_add }
|
||||
.minus { op_sub }
|
||||
.star { op_mul }
|
||||
.slash { op_div }
|
||||
.percent { op_mod }
|
||||
.eq_eq { op_eq }
|
||||
.not_eq { op_ne }
|
||||
.lt { op_lt }
|
||||
.le { op_le }
|
||||
.gt { op_gt }
|
||||
.ge { op_ge }
|
||||
else {
|
||||
return error('unsupported binary operator at line ${e.line}')
|
||||
}
|
||||
}
|
||||
g.code << op
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn (mut g Gen) intern(s string) int {
|
||||
if s in g.str_map {
|
||||
return g.str_map[s]
|
||||
}
|
||||
idx := g.strings.len
|
||||
g.strings << s
|
||||
g.str_map[s] = idx
|
||||
return idx
|
||||
}
|
||||
|
||||
fn (mut g Gen) new_label() string {
|
||||
g.next_lbl++
|
||||
return 'L${g.next_lbl}'
|
||||
}
|
||||
|
||||
fn (mut g Gen) emit_label(name string) {
|
||||
g.labels[name] = g.code.len
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
// lexer.v — tokenizer for the VuurRaaf source language (.vr).
|
||||
module compiler
|
||||
|
||||
pub enum TokKind {
|
||||
eof
|
||||
ident
|
||||
int_lit
|
||||
str_lit
|
||||
lparen
|
||||
rparen
|
||||
lbrace
|
||||
rbrace
|
||||
comma
|
||||
plus
|
||||
minus
|
||||
star
|
||||
slash
|
||||
percent
|
||||
eq_eq
|
||||
not_eq
|
||||
lt
|
||||
le
|
||||
gt
|
||||
ge
|
||||
assign
|
||||
kw_fn
|
||||
kw_let
|
||||
kw_if
|
||||
kw_else
|
||||
kw_while
|
||||
kw_return
|
||||
kw_true
|
||||
kw_false
|
||||
kw_and
|
||||
kw_or
|
||||
kw_not
|
||||
kw_print
|
||||
kw_println
|
||||
kw_assert
|
||||
}
|
||||
|
||||
pub struct Tok {
|
||||
pub:
|
||||
kind TokKind
|
||||
lit string
|
||||
line int
|
||||
}
|
||||
|
||||
pub fn tokenize(src string) ![]Tok {
|
||||
mut l := Lexer{ src: src }
|
||||
mut toks := []Tok{}
|
||||
for {
|
||||
t := l.next()!
|
||||
toks << t
|
||||
if t.kind == .eof {
|
||||
break
|
||||
}
|
||||
}
|
||||
return toks
|
||||
}
|
||||
|
||||
struct Lexer {
|
||||
mut:
|
||||
src string
|
||||
pos int
|
||||
line int
|
||||
}
|
||||
|
||||
fn (mut l Lexer) peek() u8 {
|
||||
if l.pos >= l.src.len {
|
||||
return 0
|
||||
}
|
||||
return l.src[l.pos]
|
||||
}
|
||||
|
||||
fn (mut l Lexer) peek2() u8 {
|
||||
if l.pos + 1 >= l.src.len {
|
||||
return 0
|
||||
}
|
||||
return l.src[l.pos + 1]
|
||||
}
|
||||
|
||||
fn (mut l Lexer) advance() u8 {
|
||||
c := l.src[l.pos]
|
||||
l.pos++
|
||||
if c == `\n` {
|
||||
l.line++
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
fn (mut l Lexer) next() !Tok {
|
||||
// skip whitespace and // comments
|
||||
for l.pos < l.src.len {
|
||||
c := l.peek()
|
||||
if c == ` ` || c == `\t` || c == `\r` || c == `\n` {
|
||||
l.advance()
|
||||
continue
|
||||
}
|
||||
if c == `/` && l.peek2() == `/` {
|
||||
for l.pos < l.src.len && l.peek() != `\n` {
|
||||
l.advance()
|
||||
}
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
line := l.line
|
||||
if l.pos >= l.src.len {
|
||||
return Tok{ kind: .eof, lit: '', line: line }
|
||||
}
|
||||
c := l.peek()
|
||||
match c {
|
||||
`(` {
|
||||
l.advance()
|
||||
return Tok{ kind: .lparen, lit: '(', line: line }
|
||||
}
|
||||
`)` {
|
||||
l.advance()
|
||||
return Tok{ kind: .rparen, lit: ')', line: line }
|
||||
}
|
||||
`{` {
|
||||
l.advance()
|
||||
return Tok{ kind: .lbrace, lit: '{', line: line }
|
||||
}
|
||||
`}` {
|
||||
l.advance()
|
||||
return Tok{ kind: .rbrace, lit: '}', line: line }
|
||||
}
|
||||
`,` {
|
||||
l.advance()
|
||||
return Tok{ kind: .comma, lit: ',', line: line }
|
||||
}
|
||||
`+` {
|
||||
l.advance()
|
||||
return Tok{ kind: .plus, lit: '+', line: line }
|
||||
}
|
||||
`-` {
|
||||
l.advance()
|
||||
return Tok{ kind: .minus, lit: '-', line: line }
|
||||
}
|
||||
`*` {
|
||||
l.advance()
|
||||
return Tok{ kind: .star, lit: '*', line: line }
|
||||
}
|
||||
`/` {
|
||||
l.advance()
|
||||
return Tok{ kind: .slash, lit: '/', line: line }
|
||||
}
|
||||
`%` {
|
||||
l.advance()
|
||||
return Tok{ kind: .percent, lit: '%', line: line }
|
||||
}
|
||||
`=` {
|
||||
l.advance()
|
||||
if l.peek() == `=` {
|
||||
l.advance()
|
||||
return Tok{ kind: .eq_eq, lit: '==', line: line }
|
||||
}
|
||||
return Tok{ kind: .assign, lit: '=', line: line }
|
||||
}
|
||||
`!` {
|
||||
l.advance()
|
||||
if l.peek() == `=` {
|
||||
l.advance()
|
||||
return Tok{ kind: .not_eq, lit: '!=', line: line }
|
||||
}
|
||||
return error('unexpected character "!" at line ${line} (did you mean "not"?)')
|
||||
}
|
||||
`<` {
|
||||
l.advance()
|
||||
if l.peek() == `=` {
|
||||
l.advance()
|
||||
return Tok{ kind: .le, lit: '<=', line: line }
|
||||
}
|
||||
return Tok{ kind: .lt, lit: '<', line: line }
|
||||
}
|
||||
`>` {
|
||||
l.advance()
|
||||
if l.peek() == `=` {
|
||||
l.advance()
|
||||
return Tok{ kind: .ge, lit: '>=', line: line }
|
||||
}
|
||||
return Tok{ kind: .gt, lit: '>', line: line }
|
||||
}
|
||||
`"` {
|
||||
return l.lex_string(line)!
|
||||
}
|
||||
`0`...`9` {
|
||||
return l.lex_number(line)
|
||||
}
|
||||
else {
|
||||
if (c >= `a` && c <= `z`) || (c >= `A` && c <= `Z`) || c == `_` {
|
||||
return l.lex_ident(line)
|
||||
}
|
||||
return error('unexpected character "${c.ascii_str()}" at line ${line}')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn (mut l Lexer) lex_number(line int) Tok {
|
||||
start := l.pos
|
||||
for l.pos < l.src.len && l.peek() >= `0` && l.peek() <= `9` {
|
||||
l.advance()
|
||||
}
|
||||
return Tok{ kind: .int_lit, lit: l.src[start..l.pos], line: line }
|
||||
}
|
||||
|
||||
fn (mut l Lexer) lex_ident(line int) Tok {
|
||||
start := l.pos
|
||||
for l.pos < l.src.len {
|
||||
c := l.peek()
|
||||
if (c >= `a` && c <= `z`) || (c >= `A` && c <= `Z`) || (c >= `0` && c <= `9`) || c == `_` {
|
||||
l.advance()
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
lit := l.src[start..l.pos]
|
||||
kind := match lit {
|
||||
'fn' { TokKind.kw_fn }
|
||||
'let' { TokKind.kw_let }
|
||||
'if' { TokKind.kw_if }
|
||||
'else' { TokKind.kw_else }
|
||||
'while' { TokKind.kw_while }
|
||||
'return' { TokKind.kw_return }
|
||||
'true' { TokKind.kw_true }
|
||||
'false' { TokKind.kw_false }
|
||||
'and' { TokKind.kw_and }
|
||||
'or' { TokKind.kw_or }
|
||||
'not' { TokKind.kw_not }
|
||||
'print' { TokKind.kw_print }
|
||||
'println' { TokKind.kw_println }
|
||||
'assert' { TokKind.kw_assert }
|
||||
else { TokKind.ident }
|
||||
}
|
||||
return Tok{ kind: kind, lit: lit, line: line }
|
||||
}
|
||||
|
||||
fn (mut l Lexer) lex_string(line int) !Tok {
|
||||
l.advance() // opening quote
|
||||
mut s := ''
|
||||
for l.pos < l.src.len {
|
||||
c := l.advance()
|
||||
if c == `"` {
|
||||
return Tok{ kind: .str_lit, lit: s, line: line }
|
||||
}
|
||||
if c == `\\` {
|
||||
if l.pos >= l.src.len {
|
||||
break
|
||||
}
|
||||
e := l.advance()
|
||||
match e {
|
||||
`n` {
|
||||
s += '\n'
|
||||
}
|
||||
`t` {
|
||||
s += '\t'
|
||||
}
|
||||
`"` {
|
||||
s += '"'
|
||||
}
|
||||
`\\` {
|
||||
s += '\\'
|
||||
}
|
||||
else {
|
||||
return error('invalid escape \\${e.ascii_str()} at line ${line}')
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
s += c.ascii_str()
|
||||
}
|
||||
return error('unterminated string at line ${line}')
|
||||
}
|
||||
@@ -0,0 +1,388 @@
|
||||
// parser.v — recursive-descent parser for the VuurRaaf language.
|
||||
//
|
||||
// Grammar (informal):
|
||||
// program := fn*
|
||||
// fn := 'fn' IDENT '(' [IDENT (',' IDENT)*] ')' block
|
||||
// block := '{' stmt* '}'
|
||||
// stmt := 'let' IDENT '=' expr
|
||||
// | IDENT '=' expr
|
||||
// | 'if' cond block ['else' block]
|
||||
// | 'while' cond block
|
||||
// | 'return' [expr]
|
||||
// | 'assert' expr
|
||||
// | expr
|
||||
// cond := ['('] expr [')'] // parens optional
|
||||
// expr := or ('or' or)*
|
||||
// or := and ('and' and)*
|
||||
// and := eq (('=='|'!=') eq)*
|
||||
// eq := rel (('<'|'<='|'>'|'>=') rel)*
|
||||
// rel := add (('+'|'-') add)*
|
||||
// add := mul (('*'|'/'|'%') mul)*
|
||||
// mul := ('not'|'-') mul | primary
|
||||
// primary := INT | STR | 'true' | 'false' | IDENT ['(' args ')'] | '(' expr ')'
|
||||
module compiler
|
||||
|
||||
pub enum ExprKind {
|
||||
int_lit
|
||||
str_lit
|
||||
bool_lit
|
||||
ident
|
||||
unary
|
||||
binary
|
||||
call
|
||||
}
|
||||
|
||||
pub struct Expr {
|
||||
pub mut:
|
||||
kind ExprKind
|
||||
int_v i64
|
||||
str_v string
|
||||
name string
|
||||
op TokKind
|
||||
left &Expr = unsafe { nil }
|
||||
right &Expr = unsafe { nil }
|
||||
args []Expr
|
||||
line int
|
||||
}
|
||||
|
||||
pub enum StmtKind {
|
||||
expr_stmt
|
||||
let_stmt
|
||||
assign_stmt
|
||||
if_stmt
|
||||
while_stmt
|
||||
ret_stmt
|
||||
assert_stmt
|
||||
}
|
||||
|
||||
pub struct Stmt {
|
||||
pub mut:
|
||||
kind StmtKind
|
||||
target string
|
||||
expr Expr
|
||||
cond Expr
|
||||
body []Stmt
|
||||
els []Stmt
|
||||
has_val bool
|
||||
line int
|
||||
}
|
||||
|
||||
pub struct FnDecl {
|
||||
pub mut:
|
||||
name string
|
||||
params []string
|
||||
body []Stmt
|
||||
line int
|
||||
}
|
||||
|
||||
pub struct Program {
|
||||
pub mut:
|
||||
fns []FnDecl
|
||||
}
|
||||
|
||||
pub fn parse(toks []Tok) !Program {
|
||||
mut p := Parser{ toks: toks }
|
||||
return p.parse_program()
|
||||
}
|
||||
|
||||
struct Parser {
|
||||
mut:
|
||||
toks []Tok
|
||||
pos int
|
||||
}
|
||||
|
||||
fn (mut p Parser) cur() Tok {
|
||||
if p.pos < p.toks.len {
|
||||
return p.toks[p.pos]
|
||||
}
|
||||
return p.toks[p.toks.len - 1]
|
||||
}
|
||||
|
||||
fn (mut p Parser) advance() Tok {
|
||||
t := p.cur()
|
||||
if p.pos < p.toks.len - 1 {
|
||||
p.pos++
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
fn (mut p Parser) expect(k TokKind, what string) !Tok {
|
||||
t := p.cur()
|
||||
if t.kind != k {
|
||||
return error('expected ${what}, got "${t.lit}" at line ${t.line}')
|
||||
}
|
||||
return p.advance()
|
||||
}
|
||||
|
||||
// parse_cond parses a condition, accepting either `if cond {` or `if (cond) {`.
|
||||
fn (mut p Parser) parse_cond() !Expr {
|
||||
if p.cur().kind == .lparen {
|
||||
p.advance()
|
||||
e := p.parse_expr()!
|
||||
p.expect(.rparen, "')'")!
|
||||
return e
|
||||
}
|
||||
return p.parse_expr()!
|
||||
}
|
||||
|
||||
fn (mut p Parser) parse_program() !Program {
|
||||
mut prog := Program{}
|
||||
for p.cur().kind != .eof {
|
||||
prog.fns << p.parse_fn()!
|
||||
}
|
||||
if prog.fns.len == 0 {
|
||||
return error('no functions found in source')
|
||||
}
|
||||
return prog
|
||||
}
|
||||
|
||||
fn (mut p Parser) parse_fn() !FnDecl {
|
||||
fn_tok := p.expect(.kw_fn, "'fn'")!
|
||||
name := p.expect(.ident, 'function name')!
|
||||
p.expect(.lparen, "'('")!
|
||||
mut params := []string{}
|
||||
if p.cur().kind != .rparen {
|
||||
for {
|
||||
params << p.expect(.ident, 'parameter name')!.lit
|
||||
if p.cur().kind == .comma {
|
||||
p.advance()
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
p.expect(.rparen, "')'")!
|
||||
body := p.parse_block()!
|
||||
return FnDecl{ name: name.lit, params: params, body: body, line: fn_tok.line }
|
||||
}
|
||||
|
||||
fn (mut p Parser) parse_block() ![]Stmt {
|
||||
p.expect(.lbrace, "'{'")!
|
||||
mut stmts := []Stmt{}
|
||||
for p.cur().kind != .rbrace {
|
||||
if p.cur().kind == .eof {
|
||||
return error('unexpected end of file inside block (missing "}")')
|
||||
}
|
||||
stmts << p.parse_stmt()!
|
||||
}
|
||||
p.expect(.rbrace, "'}'")!
|
||||
return stmts
|
||||
}
|
||||
|
||||
fn (mut p Parser) parse_stmt() !Stmt {
|
||||
t := p.cur()
|
||||
match t.kind {
|
||||
.kw_let {
|
||||
p.advance()
|
||||
name := p.expect(.ident, 'variable name')!
|
||||
p.expect(.assign, "'='")!
|
||||
e := p.parse_expr()!
|
||||
return Stmt{ kind: .let_stmt, target: name.lit, expr: e, line: t.line }
|
||||
} .kw_if {
|
||||
p.advance()
|
||||
cond := p.parse_cond()!
|
||||
body := p.parse_block()!
|
||||
mut els := []Stmt{}
|
||||
if p.cur().kind == .kw_else {
|
||||
p.advance()
|
||||
els = p.parse_block()!
|
||||
}
|
||||
return Stmt{ kind: .if_stmt, cond: cond, body: body, els: els, line: t.line }
|
||||
}
|
||||
.kw_while {
|
||||
p.advance()
|
||||
cond := p.parse_cond()!
|
||||
body := p.parse_block()!
|
||||
return Stmt{ kind: .while_stmt, cond: cond, body: body, line: t.line }
|
||||
}
|
||||
.kw_return {
|
||||
p.advance()
|
||||
mut e := Expr{}
|
||||
has_val := p.cur().kind != .rbrace
|
||||
if has_val {
|
||||
e = p.parse_expr()!
|
||||
}
|
||||
return Stmt{ kind: .ret_stmt, expr: e, has_val: has_val, line: t.line }
|
||||
} .kw_assert {
|
||||
p.advance()
|
||||
mut e := Expr{}
|
||||
if p.cur().kind == .lparen {
|
||||
p.advance()
|
||||
e = p.parse_expr()!
|
||||
p.expect(.rparen, "')'")!
|
||||
} else {
|
||||
e = p.parse_expr()!
|
||||
}
|
||||
return Stmt{ kind: .assert_stmt, expr: e, line: t.line }
|
||||
}
|
||||
.ident {
|
||||
p.advance()
|
||||
if p.cur().kind == .assign {
|
||||
p.advance()
|
||||
e := p.parse_expr()!
|
||||
return Stmt{ kind: .assign_stmt, target: t.lit, expr: e, line: t.line }
|
||||
}
|
||||
e := p.parse_call_or_ident(t)!
|
||||
return Stmt{ kind: .expr_stmt, expr: e, line: t.line }
|
||||
}
|
||||
.kw_print, .kw_println {
|
||||
p.advance()
|
||||
e := p.parse_call(t)!
|
||||
return Stmt{ kind: .expr_stmt, expr: e, line: t.line }
|
||||
}
|
||||
else {
|
||||
return error('unexpected token "${t.lit}" at line ${t.line}')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// bin_node allocates a binary-operator node. It takes copies of the operands
|
||||
// so that `&l`/`&r` target fresh heap objects (taking the address of a local
|
||||
// that is later reassigned would create a self-referential node).
|
||||
fn bin_node(op TokKind, left Expr, right Expr, line int) Expr {
|
||||
mut l := left
|
||||
mut r := right
|
||||
return Expr{ kind: .binary, op: op, left: &l, right: &r, line: line }
|
||||
}
|
||||
|
||||
fn unary_node(op TokKind, operand Expr, line int) Expr {
|
||||
mut o := operand
|
||||
return Expr{ kind: .unary, op: op, right: &o, line: line }
|
||||
}
|
||||
|
||||
fn (mut p Parser) parse_expr() !Expr {
|
||||
return p.parse_or()!
|
||||
}
|
||||
|
||||
fn (mut p Parser) parse_or() !Expr {
|
||||
mut e := p.parse_and()!
|
||||
for p.cur().kind == .kw_or {
|
||||
op := p.advance()
|
||||
rhs := p.parse_and()!
|
||||
e = bin_node(op.kind, e, rhs, op.line)
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
fn (mut p Parser) parse_and() !Expr {
|
||||
mut e := p.parse_eq()!
|
||||
for p.cur().kind == .kw_and {
|
||||
op := p.advance()
|
||||
rhs := p.parse_eq()!
|
||||
e = bin_node(op.kind, e, rhs, op.line)
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
fn (mut p Parser) parse_eq() !Expr {
|
||||
mut e := p.parse_rel()!
|
||||
for p.cur().kind == .eq_eq || p.cur().kind == .not_eq {
|
||||
op := p.advance()
|
||||
rhs := p.parse_rel()!
|
||||
e = bin_node(op.kind, e, rhs, op.line)
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
fn (mut p Parser) parse_rel() !Expr {
|
||||
mut e := p.parse_add()!
|
||||
for p.cur().kind == .lt || p.cur().kind == .le || p.cur().kind == .gt || p.cur().kind == .ge {
|
||||
op := p.advance()
|
||||
rhs := p.parse_add()!
|
||||
e = bin_node(op.kind, e, rhs, op.line)
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
fn (mut p Parser) parse_add() !Expr {
|
||||
mut e := p.parse_mul()!
|
||||
for p.cur().kind == .plus || p.cur().kind == .minus {
|
||||
op := p.advance()
|
||||
rhs := p.parse_mul()!
|
||||
e = bin_node(op.kind, e, rhs, op.line)
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
fn (mut p Parser) parse_mul() !Expr {
|
||||
mut e := p.parse_unary()!
|
||||
for p.cur().kind == .star || p.cur().kind == .slash || p.cur().kind == .percent {
|
||||
op := p.advance()
|
||||
rhs := p.parse_unary()!
|
||||
e = bin_node(op.kind, e, rhs, op.line)
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
fn (mut p Parser) parse_unary() !Expr {
|
||||
t := p.cur()
|
||||
if t.kind == .kw_not || t.kind == .minus {
|
||||
p.advance()
|
||||
e := p.parse_unary()!
|
||||
return unary_node(t.kind, e, t.line)
|
||||
}
|
||||
return p.parse_primary()!
|
||||
}
|
||||
|
||||
fn (mut p Parser) parse_primary() !Expr {
|
||||
t := p.cur()
|
||||
match t.kind {
|
||||
.int_lit {
|
||||
p.advance()
|
||||
return Expr{ kind: .int_lit, int_v: t.lit.i64(), line: t.line }
|
||||
}
|
||||
.str_lit {
|
||||
p.advance()
|
||||
return Expr{ kind: .str_lit, str_v: t.lit, line: t.line }
|
||||
}
|
||||
.kw_true {
|
||||
p.advance()
|
||||
return Expr{ kind: .bool_lit, int_v: 1, line: t.line }
|
||||
}
|
||||
.kw_false {
|
||||
p.advance()
|
||||
return Expr{ kind: .bool_lit, int_v: 0, line: t.line }
|
||||
}
|
||||
.lparen {
|
||||
p.advance()
|
||||
e := p.parse_expr()!
|
||||
p.expect(.rparen, "')'")!
|
||||
return e
|
||||
}
|
||||
.ident {
|
||||
p.advance()
|
||||
return p.parse_call_or_ident(t)!
|
||||
}
|
||||
.kw_print, .kw_println {
|
||||
p.advance()
|
||||
return p.parse_call(t)!
|
||||
}
|
||||
else {
|
||||
return error('unexpected token "${t.lit}" at line ${t.line}')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn (mut p Parser) parse_call_or_ident(t Tok) !Expr {
|
||||
if p.cur().kind == .lparen {
|
||||
return p.parse_call(t)!
|
||||
}
|
||||
return Expr{ kind: .ident, name: t.lit, line: t.line }
|
||||
}
|
||||
|
||||
fn (mut p Parser) parse_call(name Tok) !Expr {
|
||||
p.expect(.lparen, "'('")!
|
||||
mut args := []Expr{}
|
||||
if p.cur().kind != .rparen {
|
||||
for {
|
||||
args << p.parse_expr()!
|
||||
if p.cur().kind == .comma {
|
||||
p.advance()
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
p.expect(.rparen, "')'")!
|
||||
return Expr{ kind: .call, name: name.lit, args: args, line: name.line }
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// fib.vr — recursive fibonacci, small enough to benchmark with `vr bench`.
|
||||
|
||||
fn fib(n) {
|
||||
if n < 2 {
|
||||
return n
|
||||
}
|
||||
return fib(n - 1) + fib(n - 2)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
println(fib(20))
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// hello.vr — a tour of the VuurRaaf language.
|
||||
// Run with: vr run examples/hello.vr
|
||||
|
||||
fn greet(name) {
|
||||
println("hello, " + name + "!")
|
||||
}
|
||||
|
||||
// recursive fibonacci
|
||||
fn fib(n) {
|
||||
if n < 2 {
|
||||
return n
|
||||
}
|
||||
return fib(n - 1) + fib(n - 2)
|
||||
}
|
||||
|
||||
// returns n! via a loop
|
||||
fn fact(n) {
|
||||
let result = 1
|
||||
let i = 1
|
||||
while i <= n {
|
||||
result = result * i
|
||||
i = i + 1
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fn main() {
|
||||
greet("vuurraaf")
|
||||
|
||||
let x = 6 * 7
|
||||
println("6 * 7 = ")
|
||||
println(x)
|
||||
|
||||
let name = "world"
|
||||
let msg = "hello " + name
|
||||
println(msg)
|
||||
println(msg == "hello world")
|
||||
|
||||
// booleans and short-circuiting
|
||||
let big = x > 40 and x < 50
|
||||
let small = x < 10 or x > 100
|
||||
println("x > 40 and x < 50: ")
|
||||
println(big)
|
||||
println("x < 10 or x > 100: ")
|
||||
println(small)
|
||||
println("not big: ")
|
||||
println(not big)
|
||||
|
||||
// if / else
|
||||
if x > 40 {
|
||||
println("x is big")
|
||||
} else {
|
||||
println("x is small")
|
||||
}
|
||||
|
||||
// while loop
|
||||
let i = 0
|
||||
while i < 3 {
|
||||
println("counting " + i)
|
||||
i = i + 1
|
||||
}
|
||||
|
||||
println("fib(10) = ")
|
||||
println(fib(10))
|
||||
|
||||
println("fact(5) = ")
|
||||
println(fact(5))
|
||||
|
||||
// asserts are checked at runtime by the vm
|
||||
assert x == 42
|
||||
assert fact(5) == 120
|
||||
assert fib(10) == 55
|
||||
println("all asserts passed")
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// lib.vr — a small library compiled to its own object file.
|
||||
// vr compile examples/lib.vr -o /tmp/lib.vobj
|
||||
|
||||
fn double(n) {
|
||||
return n * 2
|
||||
}
|
||||
|
||||
fn square(n) {
|
||||
return n * n
|
||||
}
|
||||
|
||||
fn describe(n) {
|
||||
if n > 100 {
|
||||
return "big"
|
||||
}
|
||||
return "small"
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
; math.vasm — raw bytecode, hand-written in VuurRaaf assembly.
|
||||
; Assemble + link + run with:
|
||||
; vr assemble examples/math.vasm
|
||||
; vr link math.vobj -o math.vbin
|
||||
; vr run math.vbin
|
||||
|
||||
.global main
|
||||
|
||||
main:
|
||||
push_int 6
|
||||
push_int 7
|
||||
mul
|
||||
println
|
||||
push_str "computed 6 * 7 = 42"
|
||||
println
|
||||
push_int 40
|
||||
push_int 2
|
||||
call add 2 ; calls the exported `add` symbol below
|
||||
println
|
||||
halt
|
||||
|
||||
; a callable function: takes two args, returns their sum
|
||||
.global add
|
||||
add:
|
||||
enter 0 ; no extra locals (args were copied in by `call`)
|
||||
load 0
|
||||
load 1
|
||||
add
|
||||
retv
|
||||
@@ -0,0 +1,41 @@
|
||||
// tests.vr — functions named test_* are picked up by `vr test`.
|
||||
// vr test examples/tests.vr
|
||||
|
||||
fn add(a, b) {
|
||||
return a + b
|
||||
}
|
||||
|
||||
fn test_add() {
|
||||
assert add(2, 3) == 5
|
||||
assert add(-1, 1) == 0
|
||||
}
|
||||
|
||||
fn test_strings() {
|
||||
let a = "foo"
|
||||
let b = "bar"
|
||||
assert a + b == "foobar"
|
||||
assert a != b
|
||||
assert a == "foo"
|
||||
}
|
||||
|
||||
fn test_loops() {
|
||||
let total = 0
|
||||
let i = 1
|
||||
while i <= 10 {
|
||||
total = total + i
|
||||
i = i + 1
|
||||
}
|
||||
assert total == 55
|
||||
}
|
||||
|
||||
fn test_booleans() {
|
||||
let x = 7
|
||||
assert x > 3 and x < 10
|
||||
assert x == 7 or x == 8
|
||||
assert not (x == 0)
|
||||
}
|
||||
|
||||
fn test_failing() {
|
||||
// this one is meant to fail — shows up in `vr test` output
|
||||
assert 1 == 2
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// use_lib.vr — calls functions defined in lib.vr; the two object files are
|
||||
// linked together by the linker:
|
||||
// vr compile examples/lib.vr -o /tmp/lib.vobj
|
||||
// vr compile examples/use_lib.vr -o /tmp/use_lib.vobj
|
||||
// vr link /tmp/lib.vobj /tmp/use_lib.vobj -o /tmp/use_lib.vbin
|
||||
// vr run /tmp/use_lib.vbin
|
||||
|
||||
fn main() {
|
||||
let d = double(21)
|
||||
println("double(21) = ")
|
||||
println(d)
|
||||
|
||||
let s = square(9)
|
||||
println("square(9) = ")
|
||||
println(s)
|
||||
|
||||
println("describe(500) = ")
|
||||
println(describe(500))
|
||||
|
||||
assert double(21) == 42
|
||||
assert square(9) == 81
|
||||
println("cross-file calls ok")
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// linker.v — links one or more VROBJ object files into a VRBIN executable.
|
||||
//
|
||||
// Code sections are concatenated, symbol entries are rebased, and call-site
|
||||
// relocations are patched with the final code offsets. Duplicate or unresolved
|
||||
// symbols are link errors.
|
||||
module linker
|
||||
|
||||
import obj
|
||||
|
||||
pub fn link(paths []string, out string) ! {
|
||||
if paths.len == 0 {
|
||||
return error('no object files to link')
|
||||
}
|
||||
mut code := []u8{}
|
||||
mut strings := []string{}
|
||||
mut symbols := map[string]int{}
|
||||
mut relocs := []obj.Reloc{}
|
||||
for p in paths {
|
||||
o := obj.read(p)!
|
||||
base := code.len
|
||||
for s in o.symbols {
|
||||
if s.name in symbols {
|
||||
return error('duplicate symbol "${s.name}" (in ${p})')
|
||||
}
|
||||
symbols[s.name] = base + s.entry
|
||||
}
|
||||
code << o.code
|
||||
for r in o.relocs {
|
||||
relocs << obj.Reloc{ offset: r.offset + u32(base), name: r.name, kind: r.kind }
|
||||
}
|
||||
}
|
||||
// resolve relocations
|
||||
for r in relocs {
|
||||
if r.kind == 1 {
|
||||
// string constant: rebase to its index in the merged table
|
||||
idx := intern_str(mut strings, r.name)
|
||||
obj.patch_i64(mut code, r.offset, i64(idx))
|
||||
continue
|
||||
}
|
||||
entry := symbols[r.name] or {
|
||||
return error('unresolved symbol "${r.name}" (no such function)')
|
||||
}
|
||||
obj.patch_i64(mut code, r.offset, i64(entry))
|
||||
}
|
||||
// build the executable
|
||||
mut fns := []obj.BinFn{}
|
||||
for name, entry in symbols {
|
||||
fns << obj.BinFn{ name: name, entry: entry }
|
||||
}
|
||||
obj.write_bin(out, obj.Bin{ fns: fns, strings: strings, code: code })!
|
||||
}
|
||||
|
||||
fn intern_str(mut table []string, s string) int {
|
||||
for i, t in table {
|
||||
if t == s {
|
||||
return i
|
||||
}
|
||||
}
|
||||
table << s
|
||||
return table.len - 1
|
||||
}
|
||||
@@ -1,46 +1,487 @@
|
||||
// vuurraaf/v
|
||||
// A complete toolchain for VuurRaaf, in V. This toolchain is made from zero,
|
||||
// and includes custom compiler, assembler, linker, and runtime.
|
||||
// and includes a custom compiler, assembler, linker, and runtime.
|
||||
// It is designed to be simple and easy to understand, while still being powerful
|
||||
// enough to compile and run VuurRaaf programs.
|
||||
//
|
||||
// vr run hello.vr compile + link + run
|
||||
// vr compile hello.vr source -> object (.vobj)
|
||||
// vr assemble math.vasm assembly -> object (.vobj)
|
||||
// vr link a.vobj b.vobj objects -> executable (.vbin)
|
||||
// vr debug hello.vr run with an instruction trace
|
||||
// vr test tests.vr run every test_* function
|
||||
// vr bench fib.vr 1000 benchmark main()
|
||||
// vr help everything else
|
||||
|
||||
module main
|
||||
|
||||
import os
|
||||
import v.vmod
|
||||
import json2
|
||||
import time
|
||||
import obj
|
||||
import compiler
|
||||
import assembler
|
||||
import linker
|
||||
import vm
|
||||
|
||||
fn toolchain_loader() {}
|
||||
const name = 'vuurraaf/v'
|
||||
const version = '0.1.0'
|
||||
const project_root = @VMODROOT
|
||||
|
||||
fn toolchain_unloader() {}
|
||||
fn main() {
|
||||
args := os.args[1..]
|
||||
if args.len == 0 {
|
||||
toolchain_help()
|
||||
return
|
||||
}
|
||||
cmd := args[0]
|
||||
rest := args[1..]
|
||||
match cmd {
|
||||
'help', '-h', '--help' {
|
||||
toolchain_help()
|
||||
}
|
||||
'version', '-v', '--version' {
|
||||
toolchain_version()
|
||||
}
|
||||
'info' {
|
||||
toolchain_info()
|
||||
}
|
||||
'config' {
|
||||
toolchain_config(rest) or { die('config', err) }
|
||||
}
|
||||
'compile', 'c' {
|
||||
toolchain_compile(rest) or { die('compile', err) }
|
||||
}
|
||||
'assemble', 'a' {
|
||||
toolchain_assemble(rest) or { die('assemble', err) }
|
||||
}
|
||||
'link', 'l' {
|
||||
toolchain_link(rest) or { die('link', err) }
|
||||
}
|
||||
'run', 'r' {
|
||||
toolchain_run(rest) or { die('run', err) }
|
||||
}
|
||||
'debug', 'd' {
|
||||
toolchain_debug(rest) or { die('debug', err) }
|
||||
}
|
||||
'test', 't' {
|
||||
toolchain_test(rest) or { die('test', err) }
|
||||
}
|
||||
'bench', 'b' {
|
||||
toolchain_bench(rest) or { die('bench', err) }
|
||||
}
|
||||
'clean' {
|
||||
toolchain_clean()
|
||||
}
|
||||
'up' {
|
||||
toolchain_up() or { die('up', err) }
|
||||
}
|
||||
'symlink' {
|
||||
toolchain_symlink() or { die('symlink', err) }
|
||||
}
|
||||
'loader' {
|
||||
toolchain_loader()
|
||||
}
|
||||
'alloc' {
|
||||
toolchain_alloc()
|
||||
}
|
||||
'free' {
|
||||
toolchain_free()
|
||||
}
|
||||
'unloader' {
|
||||
toolchain_unloader()
|
||||
}
|
||||
else {
|
||||
eprintln('vr: unknown command "${cmd}"')
|
||||
eprintln("run 'vr help' for usage")
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn toolchain_alloc() {}
|
||||
fn die(cmd string, err IError) {
|
||||
eprintln('vr ${cmd}: ${err.msg()}')
|
||||
exit(1)
|
||||
}
|
||||
|
||||
fn toolchain_free() {}
|
||||
// ---------------------------------------------------------------------------
|
||||
// commands
|
||||
|
||||
fn toolchain_compile() {}
|
||||
fn toolchain_loader() {
|
||||
components := [
|
||||
'compiler (lexer, parser, bytecode codegen)',
|
||||
'assembler (vasm -> vobj)',
|
||||
'linker (vobj set -> vbin)',
|
||||
'vm (stack-based runtime)',
|
||||
'obj (VROBJ/VRBIN formats)',
|
||||
]
|
||||
println('VuurRaaf toolchain components:')
|
||||
for c in components {
|
||||
println(' [loaded] ${c}')
|
||||
}
|
||||
println('')
|
||||
println('all components are statically linked into this binary (${os.executable()})')
|
||||
}
|
||||
|
||||
fn toolchain_assemble() {}
|
||||
fn toolchain_unloader() {
|
||||
println('unloader: nothing to unload — the toolchain is a single static binary')
|
||||
}
|
||||
|
||||
fn toolchain_link() {}
|
||||
fn toolchain_alloc() {
|
||||
// the toolchain keeps one pre-sized arena for scratch work
|
||||
arena := 1 * 1024 * 1024
|
||||
vm_stack := 65536 * 8
|
||||
println('toolchain memory arena:')
|
||||
println(' byte arena: ${arena} bytes (1 MiB, reserved)')
|
||||
println(' vm stack: ${vm_stack} bytes (64k slots x 8)')
|
||||
println(' strings: grow-on-demand heap inside the vm')
|
||||
}
|
||||
|
||||
fn toolchain_run() {}
|
||||
fn toolchain_free() {
|
||||
println('free: no persistent allocations to release')
|
||||
}
|
||||
|
||||
fn toolchain_debug() {}
|
||||
fn toolchain_help() {
|
||||
println('VuurRaaf toolchain (vuurraaf/v) v${version}')
|
||||
println('')
|
||||
println('usage: vr <command> [args]')
|
||||
println('')
|
||||
println(' compile <file.vr> [-o out.vobj] compile source to an object file')
|
||||
println(' assemble <file.vasm> [-o out.vobj] assemble raw bytecode to an object file')
|
||||
println(' link <a.vobj> [more.vobj ...] [-o out] link objects into an executable')
|
||||
println(' run <file.vr|file.vbin> compile, link and run (or run a binary)')
|
||||
println(' debug <file.vr|file.vbin> run with an instruction trace')
|
||||
println(' test <file.vr> run every test_* function')
|
||||
println(' bench <file.vr> [iterations] benchmark main()')
|
||||
println(' clean remove build artifacts')
|
||||
println(' up rebuild the vr binary into bin/')
|
||||
println(' symlink link bin/vr into your PATH')
|
||||
println(' config [set <key> <value>] show or change toolchain config')
|
||||
println(' info show toolchain information')
|
||||
println(' loader list toolchain components')
|
||||
println(' alloc show toolchain memory arena')
|
||||
println(' free | unloader release toolchain resources')
|
||||
println(' version print version')
|
||||
println(' help this help')
|
||||
}
|
||||
|
||||
fn toolchain_help() {}
|
||||
fn toolchain_version() {
|
||||
println('VuurRaaf toolchain v${version}')
|
||||
}
|
||||
|
||||
fn toolchain_version() {}
|
||||
fn toolchain_info() {
|
||||
v_version := os.execute('v version').output.trim_space()
|
||||
println('${name} v${version}')
|
||||
println(' root: ${project_root}')
|
||||
println(' v compiler: ${v_version}')
|
||||
println(' platform: ${os.user_os()}')
|
||||
println(' components: compiler, assembler, linker, vm, obj')
|
||||
println(' config: ${config_path()}')
|
||||
}
|
||||
|
||||
fn toolchain_info() {}
|
||||
// ---------------------------------------------------------------------------
|
||||
// config
|
||||
|
||||
fn toolchain_config() {}
|
||||
struct Config {
|
||||
mut:
|
||||
outdir string = '.'
|
||||
verbose bool
|
||||
}
|
||||
|
||||
fn toolchain_test() {}
|
||||
fn config_path() string {
|
||||
return os.join_path(os.home_dir(), '.config', 'vuurraaf', 'config.json')
|
||||
}
|
||||
|
||||
fn toolchain_bench() {}
|
||||
fn load_config() Config {
|
||||
p := config_path()
|
||||
if !os.exists(p) {
|
||||
return Config{}
|
||||
}
|
||||
text := os.read_file(p) or { return Config{} }
|
||||
return json2.decode[Config](text) or { Config{} }
|
||||
}
|
||||
|
||||
fn toolchain_clean() {}
|
||||
fn save_config(c Config) ! {
|
||||
dir := os.dir(config_path())
|
||||
os.mkdir_all(dir) or { return error('cannot create config dir: ${dir}') }
|
||||
os.write_file(config_path(), json2.encode(c))!
|
||||
}
|
||||
|
||||
fn toolchain_up() {}
|
||||
fn toolchain_config(args []string) ! {
|
||||
if args.len == 0 {
|
||||
c := load_config()
|
||||
println('config: ${config_path()}')
|
||||
println(' outdir: ${c.outdir}')
|
||||
println(' verbose: ${c.verbose}')
|
||||
return
|
||||
}
|
||||
if args[0] == 'set' && args.len == 3 {
|
||||
mut c := load_config()
|
||||
match args[1] {
|
||||
'outdir' {
|
||||
c.outdir = args[2]
|
||||
}
|
||||
'verbose' {
|
||||
c.verbose = args[2] == 'true'
|
||||
}
|
||||
else {
|
||||
return error('unknown config key "${args[1]}" (known: outdir, verbose)')
|
||||
}
|
||||
}
|
||||
save_config(c)!
|
||||
println('config updated: ${args[1]} = ${args[2]}')
|
||||
return
|
||||
}
|
||||
return error('usage: vr config [set <key> <value>]')
|
||||
}
|
||||
|
||||
fn toolchain_symlink() {}
|
||||
// ---------------------------------------------------------------------------
|
||||
// compile / assemble / link / run / debug / test / bench
|
||||
|
||||
fn toolchain_compile(args []string) ! {
|
||||
mut src := ''
|
||||
mut out := ''
|
||||
mut i := 0
|
||||
for i < args.len {
|
||||
if args[i] == '-o' && i + 1 < args.len {
|
||||
out = args[i + 1]
|
||||
i += 2
|
||||
} else {
|
||||
src = args[i]
|
||||
i++
|
||||
}
|
||||
}
|
||||
if src == '' {
|
||||
return error('usage: vr compile <file.vr> [-o out.vobj]')
|
||||
}
|
||||
o := compiler.compile_file(src)!
|
||||
if out == '' {
|
||||
out = src.all_before_last('.') + '.vobj'
|
||||
}
|
||||
obj.write(out, o)!
|
||||
println('compiled ${src} -> ${out} (${o.code.len} bytes code, ${o.symbols.len} symbols, ${o.relocs.len} relocations)')
|
||||
}
|
||||
|
||||
fn toolchain_assemble(args []string) ! {
|
||||
mut src := ''
|
||||
mut out := ''
|
||||
mut i := 0
|
||||
for i < args.len {
|
||||
if args[i] == '-o' && i + 1 < args.len {
|
||||
out = args[i + 1]
|
||||
i += 2
|
||||
} else {
|
||||
src = args[i]
|
||||
i++
|
||||
}
|
||||
}
|
||||
if src == '' {
|
||||
return error('usage: vr assemble <file.vasm> [-o out.vobj]')
|
||||
}
|
||||
o := assembler.assemble_file(src)!
|
||||
if out == '' {
|
||||
out = src.all_before_last('.') + '.vobj'
|
||||
}
|
||||
obj.write(out, o)!
|
||||
println('assembled ${src} -> ${out} (${o.code.len} bytes code, ${o.symbols.len} symbols, ${o.relocs.len} relocations)')
|
||||
}
|
||||
|
||||
fn toolchain_link(args []string) ! {
|
||||
mut objs := []string{}
|
||||
mut out := ''
|
||||
mut i := 0
|
||||
for i < args.len {
|
||||
if args[i] == '-o' && i + 1 < args.len {
|
||||
out = args[i + 1]
|
||||
i += 2
|
||||
} else {
|
||||
objs << args[i]
|
||||
i++
|
||||
}
|
||||
}
|
||||
if objs.len == 0 {
|
||||
return error('usage: vr link <a.vobj> [more.vobj ...] [-o out.vbin]')
|
||||
}
|
||||
if out == '' {
|
||||
out = os.base(objs[0]).all_before_last('.') + '.vbin'
|
||||
}
|
||||
linker.link(objs, out)!
|
||||
println('linked ${objs.len} object file(s) -> ${out}')
|
||||
}
|
||||
|
||||
fn toolchain_run(args []string) ! {
|
||||
if args.len == 0 {
|
||||
return error('usage: vr run <file.vr|file.vbin>')
|
||||
}
|
||||
f := args[0]
|
||||
if f.ends_with('.vbin') {
|
||||
bin := obj.read_bin(f)!
|
||||
vm.run(bin, 'main', false)!
|
||||
return
|
||||
}
|
||||
if f.ends_with('.vr') {
|
||||
run_src(f, 'main', false)!
|
||||
return
|
||||
}
|
||||
return error('unsupported file type: ${f} (expected .vr or .vbin)')
|
||||
}
|
||||
|
||||
fn toolchain_debug(args []string) ! {
|
||||
if args.len == 0 {
|
||||
return error('usage: vr debug <file.vr|file.vbin>')
|
||||
}
|
||||
f := args[0]
|
||||
println('debug: tracing execution of ${f}')
|
||||
if f.ends_with('.vbin') {
|
||||
bin := obj.read_bin(f)!
|
||||
vm.run(bin, 'main', true)!
|
||||
return
|
||||
}
|
||||
if f.ends_with('.vr') {
|
||||
run_src(f, 'main', true)!
|
||||
return
|
||||
}
|
||||
return error('unsupported file type: ${f} (expected .vr or .vbin)')
|
||||
}
|
||||
|
||||
fn toolchain_test(args []string) ! {
|
||||
if args.len == 0 {
|
||||
return error('usage: vr test <file.vr>')
|
||||
}
|
||||
src := args[0]
|
||||
o := compiler.compile_file(src)!
|
||||
mut tests := []string{}
|
||||
for s in o.symbols {
|
||||
if s.name.starts_with('test_') {
|
||||
tests << s.name
|
||||
}
|
||||
}
|
||||
if tests.len == 0 {
|
||||
println('no test_* functions found in ${src}')
|
||||
return
|
||||
}
|
||||
tmp_obj := os.join_path(os.temp_dir(), 'vr_${os.getpid()}.vobj')
|
||||
tmp_bin := os.join_path(os.temp_dir(), 'vr_${os.getpid()}.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)!
|
||||
mut passes := 0
|
||||
mut fails := 0
|
||||
for t in tests {
|
||||
if run_test(bin, t) {
|
||||
println(' PASS ${t}')
|
||||
passes++
|
||||
} else {
|
||||
fails++
|
||||
}
|
||||
}
|
||||
println('')
|
||||
println('${passes} passed, ${fails} failed (${tests.len} total)')
|
||||
if fails > 0 {
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
fn run_test(bin obj.Bin, name string) bool {
|
||||
vm.run(bin, name, false) or {
|
||||
eprintln(' FAIL ${name} — ${err}')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fn toolchain_bench(args []string) ! {
|
||||
if args.len == 0 {
|
||||
return error('usage: vr bench <file.vr> [iterations]')
|
||||
}
|
||||
src := args[0]
|
||||
mut n := 1000
|
||||
if args.len > 1 {
|
||||
n = args[1].int()
|
||||
}
|
||||
tmp_obj := os.join_path(os.temp_dir(), 'vr_${os.getpid()}.vobj')
|
||||
tmp_bin := os.join_path(os.temp_dir(), 'vr_${os.getpid()}.vbin')
|
||||
defer {
|
||||
os.rm(tmp_obj) or {}
|
||||
os.rm(tmp_bin) or {}
|
||||
}
|
||||
o := compiler.compile_file(src)!
|
||||
obj.write(tmp_obj, o)!
|
||||
linker.link([tmp_obj], tmp_bin)!
|
||||
bin := obj.read_bin(tmp_bin)!
|
||||
start := time.now().unix_milli()
|
||||
for _ in 0..n {
|
||||
vm.run(bin, 'main', false)!
|
||||
}
|
||||
ms := time.now().unix_milli() - start
|
||||
rate := if ms > 0 { f64(n) / (f64(ms) / 1000.0) } else { f64(0) }
|
||||
println('bench: ${n} runs of main() in ${ms}ms (${rate:.0} runs/s)')
|
||||
}
|
||||
|
||||
fn run_src(src string, entry string, trace bool) ! {
|
||||
tmp_obj := os.join_path(os.temp_dir(), 'vr_${os.getpid()}.vobj')
|
||||
tmp_bin := os.join_path(os.temp_dir(), 'vr_${os.getpid()}.vbin')
|
||||
defer {
|
||||
os.rm(tmp_obj) or {}
|
||||
os.rm(tmp_bin) or {}
|
||||
}
|
||||
o := compiler.compile_file(src)!
|
||||
obj.write(tmp_obj, o)!
|
||||
linker.link([tmp_obj], tmp_bin)!
|
||||
bin := obj.read_bin(tmp_bin)!
|
||||
vm.run(bin, entry, trace)!
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// housekeeping
|
||||
|
||||
fn toolchain_clean() {
|
||||
mut n := 0
|
||||
if files := os.ls('.') {
|
||||
for f in files {
|
||||
if f.ends_with('.vobj') || f.ends_with('.vbin') {
|
||||
os.rm(f) or {}
|
||||
n++
|
||||
}
|
||||
}
|
||||
}
|
||||
println('cleaned ${n} artifact(s) (the vr binary in bin/ is left alone; use "vr up" to rebuild it)')
|
||||
}
|
||||
|
||||
fn toolchain_up() ! {
|
||||
vcmd := os.find_abs_path_of_executable('v') or { 'v' }
|
||||
out := os.join_path(project_root, 'bin', 'vr')
|
||||
// build from the project root with `.`, not an explicit file/path:
|
||||
// the latter makes V auto-select tcc without the boehm GC, a combination
|
||||
// that miscompiles this codebase (verified: `v -o out .` is the safe one)
|
||||
res := os.execute('cd "${project_root}" && ${vcmd} -o "${out}" .')
|
||||
if res.exit_code != 0 {
|
||||
return error('build failed:\n${res.output}')
|
||||
}
|
||||
println('rebuilt: ${out}')
|
||||
}
|
||||
|
||||
fn toolchain_symlink() ! {
|
||||
src := os.join_path(project_root, 'bin', 'vr')
|
||||
if !os.exists(src) {
|
||||
return error('bin/vr does not exist — run "vr up" first')
|
||||
}
|
||||
candidates := [os.join_path(os.home_dir(), '.local', 'bin'), '/usr/local/bin']
|
||||
for dir in candidates {
|
||||
if !os.exists(dir) {
|
||||
continue
|
||||
}
|
||||
link_path := os.join_path(dir, 'vr')
|
||||
if os.exists(link_path) {
|
||||
os.rm(link_path) or {}
|
||||
}
|
||||
os.symlink(src, link_path) or { continue }
|
||||
println('linked ${src} -> ${link_path}')
|
||||
return
|
||||
}
|
||||
return error('no writable bin dir found; symlink ${src} into your PATH manually')
|
||||
}
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
// obj.v — the VuurRaaf object file format (VROBJ) and executable format (VRBIN).
|
||||
//
|
||||
// VROBJ is the linker input: code, exported symbols, string constants, and
|
||||
// relocations for call sites that the linker must resolve.
|
||||
// VRBIN is the final executable consumed by the runtime VM.
|
||||
//
|
||||
// Both formats are little-endian and self-describing:
|
||||
//
|
||||
// VROBJ: "VROBJ" ver u32 nsym { u32 name_len name i64 entry }*
|
||||
// u32 nstr { u32 len bytes }* u32 ncode code u32 nreloc { u32 off u32 name_len name u8 kind }*
|
||||
// VRBIN: "VRBIN" ver u32 nfn { u32 name_len name i64 entry }*
|
||||
// u32 nstr { u32 len bytes }* u32 ncode code
|
||||
module obj
|
||||
|
||||
import os
|
||||
|
||||
pub const magic = 'VROBJ'
|
||||
pub const bin_magic = 'VRBIN'
|
||||
|
||||
pub struct Symbol {
|
||||
pub mut:
|
||||
name string
|
||||
entry int // code offset of the function entry
|
||||
}
|
||||
|
||||
pub struct Reloc {
|
||||
pub mut:
|
||||
offset u32 // byte offset of the 8-byte operand inside `code`
|
||||
name string // kind 0: symbol name; kind 1: the string constant itself
|
||||
kind u8 // 0 = call site, 1 = string constant reference
|
||||
}
|
||||
|
||||
pub struct Obj {
|
||||
pub mut:
|
||||
symbols []Symbol
|
||||
strings []string
|
||||
code []u8
|
||||
relocs []Reloc
|
||||
}
|
||||
|
||||
pub struct BinFn {
|
||||
pub mut:
|
||||
name string
|
||||
entry int
|
||||
}
|
||||
|
||||
pub struct Bin {
|
||||
pub mut:
|
||||
fns []BinFn
|
||||
strings []string
|
||||
code []u8
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// encoding helpers
|
||||
|
||||
pub fn encode_u32(v u32) []u8 {
|
||||
return [u8(v & 0xff), u8((v >> 8) & 0xff), u8((v >> 16) & 0xff), u8((v >> 24) & 0xff)]
|
||||
}
|
||||
|
||||
pub fn encode_i64(v i64) []u8 {
|
||||
mut b := []u8{}
|
||||
for i in 0..8 {
|
||||
b << u8((v >> (8 * i)) & 0xff)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// patch_i64 writes a little-endian i64 over `code[off..off+8]`.
|
||||
pub fn patch_i64(mut code []u8, off u32, v i64) {
|
||||
for i in 0..8 {
|
||||
code[off + u32(i)] = u8((v >> (8 * i)) & 0xff)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// reading
|
||||
|
||||
struct Reader {
|
||||
mut:
|
||||
b []u8
|
||||
pos int
|
||||
}
|
||||
|
||||
fn (mut r Reader) u8_() !u8 {
|
||||
if r.pos >= r.b.len {
|
||||
return error('object file truncated')
|
||||
}
|
||||
b := r.b[r.pos]
|
||||
r.pos++
|
||||
return b
|
||||
}
|
||||
|
||||
fn (mut r Reader) u32_() !u32 {
|
||||
mut v := u32(0)
|
||||
for i in 0..4 {
|
||||
b := r.u8_()!
|
||||
v |= u32(b) << u32(8 * i)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
fn (mut r Reader) i64_() !i64 {
|
||||
mut v := i64(0)
|
||||
for i in 0..8 {
|
||||
b := r.u8_()!
|
||||
v |= i64(u64(b) << u32(8 * i))
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
fn (mut r Reader) read_str() !string {
|
||||
n := int(r.u32_()!)
|
||||
if r.pos + n > r.b.len {
|
||||
return error('object file truncated')
|
||||
}
|
||||
s := r.b[r.pos..r.pos + n].bytestr()
|
||||
r.pos += n
|
||||
return s
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// VROBJ
|
||||
|
||||
pub fn write(path string, o Obj) ! {
|
||||
mut b := []u8{}
|
||||
b << magic.bytes()
|
||||
b << u8(1) // format version
|
||||
b << encode_u32(u32(o.symbols.len))
|
||||
for s in o.symbols {
|
||||
b << encode_u32(u32(s.name.len))
|
||||
b << s.name.bytes()
|
||||
b << encode_i64(i64(s.entry))
|
||||
}
|
||||
b << encode_u32(u32(o.strings.len))
|
||||
for s in o.strings {
|
||||
b << encode_u32(u32(s.len))
|
||||
b << s.bytes()
|
||||
}
|
||||
b << encode_u32(u32(o.code.len))
|
||||
b << o.code
|
||||
b << encode_u32(u32(o.relocs.len))
|
||||
for r in o.relocs {
|
||||
b << encode_u32(r.offset)
|
||||
b << encode_u32(u32(r.name.len))
|
||||
b << r.name.bytes()
|
||||
b << r.kind
|
||||
}
|
||||
os.write_bytes(path, b)!
|
||||
}
|
||||
|
||||
pub fn read(path string) !Obj {
|
||||
b := os.read_bytes(path)!
|
||||
if b.len < magic.len || b[0..magic.len].bytestr() != magic {
|
||||
return error('not a VROBJ file: ${path}')
|
||||
}
|
||||
mut r := Reader{ b: b, pos: magic.len }
|
||||
_ := r.u8_()! // version
|
||||
mut o := Obj{}
|
||||
nsym := int(r.u32_()!)
|
||||
for _ in 0..nsym {
|
||||
name := r.read_str()!
|
||||
entry := int(r.i64_()!)
|
||||
o.symbols << Symbol{ name: name, entry: entry }
|
||||
}
|
||||
nstr := int(r.u32_()!)
|
||||
for _ in 0..nstr {
|
||||
o.strings << r.read_str()!
|
||||
}
|
||||
ncode := int(r.u32_()!)
|
||||
if r.pos + ncode > b.len {
|
||||
return error('object file truncated')
|
||||
}
|
||||
o.code = b[r.pos..r.pos + ncode]
|
||||
r.pos += ncode
|
||||
nrel := int(r.u32_()!)
|
||||
for _ in 0..nrel {
|
||||
off := r.u32_()!
|
||||
name := r.read_str()!
|
||||
kind := r.u8_()!
|
||||
o.relocs << Reloc{ offset: off, name: name, kind: kind }
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// VRBIN
|
||||
|
||||
pub fn write_bin(path string, bin Bin) ! {
|
||||
mut b := []u8{}
|
||||
b << bin_magic.bytes()
|
||||
b << u8(1) // format version
|
||||
b << encode_u32(u32(bin.fns.len))
|
||||
for f in bin.fns {
|
||||
b << encode_u32(u32(f.name.len))
|
||||
b << f.name.bytes()
|
||||
b << encode_i64(i64(f.entry))
|
||||
}
|
||||
b << encode_u32(u32(bin.strings.len))
|
||||
for s in bin.strings {
|
||||
b << encode_u32(u32(s.len))
|
||||
b << s.bytes()
|
||||
}
|
||||
b << encode_u32(u32(bin.code.len))
|
||||
b << bin.code
|
||||
os.write_bytes(path, b)!
|
||||
}
|
||||
|
||||
pub fn read_bin(path string) !Bin {
|
||||
b := os.read_bytes(path)!
|
||||
if b.len < bin_magic.len || b[0..bin_magic.len].bytestr() != bin_magic {
|
||||
return error('not a VRBIN file: ${path}')
|
||||
}
|
||||
mut r := Reader{ b: b, pos: bin_magic.len }
|
||||
_ := r.u8_()! // version
|
||||
mut bin := Bin{}
|
||||
nfn := int(r.u32_()!)
|
||||
for _ in 0..nfn {
|
||||
name := r.read_str()!
|
||||
entry := int(r.i64_()!)
|
||||
bin.fns << BinFn{ name: name, entry: entry }
|
||||
}
|
||||
nstr := int(r.u32_()!)
|
||||
for _ in 0..nstr {
|
||||
bin.strings << r.read_str()!
|
||||
}
|
||||
ncode := int(r.u32_()!)
|
||||
if r.pos + ncode > b.len {
|
||||
return error('executable file truncated')
|
||||
}
|
||||
bin.code = b[r.pos..r.pos + ncode]
|
||||
return bin
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
Module {
|
||||
name: 'vuurraaf'
|
||||
description: 'A complete toolchain for VuurRaaf, written in V: compiler, assembler, linker, and runtime.'
|
||||
version: '0.1.0'
|
||||
license: 'MIT'
|
||||
author: 'allexanderbergmns'
|
||||
}
|
||||
@@ -0,0 +1,523 @@
|
||||
// vm.v — the VuurRaaf runtime: a small stack-based virtual machine.
|
||||
//
|
||||
// Stack values are 64-bit tagged integers: odd values are string handles into
|
||||
// the runtime string heap (handle = value >> 1); even values are encoded
|
||||
// numbers (value = raw << 1). Encoding numbers as even values means no
|
||||
// integer ever collides with a string handle.
|
||||
//
|
||||
// Call convention: CALL pushes a frame (retaddr, old bp, argc) and copies the
|
||||
// arguments into the callee's local slots; the callee reserves extra locals
|
||||
// with `enter n` and cleans up with `ret`/`retv`.
|
||||
module vm
|
||||
|
||||
import obj
|
||||
|
||||
// opcodes — keep in sync with the compiler, assembler, and this interpreter
|
||||
const op_halt = u8(0)
|
||||
const op_push_i = u8(1)
|
||||
const op_push_s = u8(2)
|
||||
const op_load = u8(3)
|
||||
const op_store = u8(4)
|
||||
const op_pop = u8(5)
|
||||
const op_dup = u8(6)
|
||||
const op_add = u8(7)
|
||||
const op_sub = u8(8)
|
||||
const op_mul = u8(9)
|
||||
const op_div = u8(10)
|
||||
const op_mod = u8(11)
|
||||
const op_neg = u8(12)
|
||||
const op_eq = u8(13)
|
||||
const op_ne = u8(14)
|
||||
const op_lt = u8(15)
|
||||
const op_le = u8(16)
|
||||
const op_gt = u8(17)
|
||||
const op_ge = u8(18)
|
||||
const op_and = u8(19)
|
||||
const op_or = u8(20)
|
||||
const op_not = u8(21)
|
||||
const op_jmp = u8(22)
|
||||
const op_jz = u8(23)
|
||||
const op_jnz = u8(24)
|
||||
const op_call = u8(25)
|
||||
const op_ret = u8(26)
|
||||
const op_retv = u8(27)
|
||||
const op_print = u8(28)
|
||||
const op_println = u8(29)
|
||||
const op_assert = u8(30)
|
||||
const op_enter = u8(31)
|
||||
|
||||
const stack_cap = 65536
|
||||
|
||||
struct Vm {
|
||||
mut:
|
||||
code []u8
|
||||
strings []string
|
||||
stack []i64
|
||||
sp int
|
||||
bp int
|
||||
ip int
|
||||
trace bool
|
||||
halted bool
|
||||
}
|
||||
|
||||
// run executes the function named `entry` from the executable `bin` and
|
||||
// returns its return value (0 if it never returns one).
|
||||
pub fn run(bin obj.Bin, entry string, trace bool) !i64 {
|
||||
mut v := Vm{
|
||||
code: bin.code
|
||||
strings: bin.strings.clone()
|
||||
stack: []i64{len: stack_cap}
|
||||
trace: trace
|
||||
}
|
||||
mut entry_ip := -1
|
||||
for f in bin.fns {
|
||||
if f.name == entry {
|
||||
entry_ip = f.entry
|
||||
break
|
||||
}
|
||||
}
|
||||
if entry_ip < 0 {
|
||||
names := bin.fns.map(fn (f obj.BinFn) string {
|
||||
return f.name
|
||||
})
|
||||
return error('no function "${entry}" in program (available: ${names.join(', ')})')
|
||||
}
|
||||
// synthetic frame: retaddr = -1 (halt sentinel), old bp = 0, argc = 0
|
||||
v.stack[v.sp] = v.enc_int(-1)
|
||||
v.sp++
|
||||
v.stack[v.sp] = v.enc_int(0)
|
||||
v.sp++
|
||||
v.stack[v.sp] = v.enc_int(0)
|
||||
v.sp++
|
||||
v.bp = v.sp
|
||||
v.ip = entry_ip
|
||||
v.exec()!
|
||||
if v.sp > 0 {
|
||||
return v.dec_int(v.stack[0])
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
fn (mut v Vm) exec() ! {
|
||||
for !v.halted {
|
||||
op := v.code[v.ip]
|
||||
if v.trace {
|
||||
v.trace_op(op)
|
||||
}
|
||||
match op {
|
||||
op_halt {
|
||||
v.halted = true
|
||||
}
|
||||
op_push_i {
|
||||
v.ip++
|
||||
v.push(v.enc_int(v.read_i64()))!
|
||||
}
|
||||
op_push_s {
|
||||
v.ip++
|
||||
idx := int(v.read_i64())
|
||||
v.push(v.mkstr(idx))!
|
||||
}
|
||||
op_load {
|
||||
v.ip++
|
||||
idx := int(v.read_i64())
|
||||
v.push(v.stack[v.bp + idx])!
|
||||
}
|
||||
op_store {
|
||||
v.ip++
|
||||
idx := int(v.read_i64())
|
||||
v.stack[v.bp + idx] = v.pop()!
|
||||
}
|
||||
op_pop {
|
||||
v.ip++
|
||||
v.pop()!
|
||||
}
|
||||
op_dup {
|
||||
v.ip++
|
||||
a := v.pop()!
|
||||
v.push(a)!
|
||||
v.push(a)!
|
||||
}
|
||||
op_add {
|
||||
v.ip++
|
||||
b := v.pop()!
|
||||
a := v.pop()!
|
||||
v.push(v.add(a, b)!)!
|
||||
}
|
||||
op_sub {
|
||||
v.ip++
|
||||
b := v.pop()!
|
||||
a := v.pop()!
|
||||
v.push(v.arith(a, b, '-')!)!
|
||||
}
|
||||
op_mul {
|
||||
v.ip++
|
||||
b := v.pop()!
|
||||
a := v.pop()!
|
||||
v.push(v.arith(a, b, '*')!)!
|
||||
}
|
||||
op_div {
|
||||
v.ip++
|
||||
b := v.pop()!
|
||||
a := v.pop()!
|
||||
v.push(v.arith(a, b, '/')!)!
|
||||
}
|
||||
op_mod {
|
||||
v.ip++
|
||||
b := v.pop()!
|
||||
a := v.pop()!
|
||||
v.push(v.arith(a, b, '%')!)!
|
||||
}
|
||||
op_neg {
|
||||
v.ip++
|
||||
a := v.pop()!
|
||||
if v.is_str(a) {
|
||||
return error('cannot negate a string')
|
||||
}
|
||||
v.push(v.enc_int(-v.dec_int(a)))!
|
||||
}
|
||||
op_eq {
|
||||
v.ip++
|
||||
b := v.pop()!
|
||||
a := v.pop()!
|
||||
v.push(v.enc_int(v.cmp(a, b, '==')!))!
|
||||
}
|
||||
op_ne {
|
||||
v.ip++
|
||||
b := v.pop()!
|
||||
a := v.pop()!
|
||||
v.push(v.enc_int(v.cmp(a, b, '!=')!))!
|
||||
}
|
||||
op_lt {
|
||||
v.ip++
|
||||
b := v.pop()!
|
||||
a := v.pop()!
|
||||
v.push(v.enc_int(v.cmp(a, b, '<')!))!
|
||||
}
|
||||
op_le {
|
||||
v.ip++
|
||||
b := v.pop()!
|
||||
a := v.pop()!
|
||||
v.push(v.enc_int(v.cmp(a, b, '<=')!))!
|
||||
}
|
||||
op_gt {
|
||||
v.ip++
|
||||
b := v.pop()!
|
||||
a := v.pop()!
|
||||
v.push(v.enc_int(v.cmp(a, b, '>')!))!
|
||||
}
|
||||
op_ge {
|
||||
v.ip++
|
||||
b := v.pop()!
|
||||
a := v.pop()!
|
||||
v.push(v.enc_int(v.cmp(a, b, '>=')!))!
|
||||
}
|
||||
op_and {
|
||||
v.ip++
|
||||
b := v.pop()!
|
||||
a := v.pop()!
|
||||
v.push(v.enc_int(bool_i64(v.truthy(a) && v.truthy(b))))!
|
||||
}
|
||||
op_or {
|
||||
v.ip++
|
||||
b := v.pop()!
|
||||
a := v.pop()!
|
||||
v.push(v.enc_int(bool_i64(v.truthy(a) || v.truthy(b))))!
|
||||
}
|
||||
op_not {
|
||||
v.ip++
|
||||
a := v.pop()!
|
||||
v.push(v.enc_int(bool_i64(!v.truthy(a))))!
|
||||
}
|
||||
op_jmp {
|
||||
v.ip++
|
||||
v.ip = int(v.read_i64())
|
||||
}
|
||||
op_jz {
|
||||
v.ip++
|
||||
target := int(v.read_i64())
|
||||
if !v.truthy(v.pop()!) {
|
||||
v.ip = target
|
||||
}
|
||||
}
|
||||
op_jnz {
|
||||
v.ip++
|
||||
target := int(v.read_i64())
|
||||
if v.truthy(v.pop()!) {
|
||||
v.ip = target
|
||||
}
|
||||
}
|
||||
op_call {
|
||||
v.ip++
|
||||
target := int(v.read_i64())
|
||||
argc := int(v.read_i64())
|
||||
v.call(target, argc)
|
||||
}
|
||||
op_ret {
|
||||
v.ret(false)!
|
||||
}
|
||||
op_retv {
|
||||
v.ret(true)!
|
||||
}
|
||||
op_print {
|
||||
v.ip++
|
||||
v.print_val(v.pop()!)
|
||||
}
|
||||
op_println {
|
||||
v.ip++
|
||||
v.print_val(v.pop()!)
|
||||
println('')
|
||||
}
|
||||
op_assert {
|
||||
v.ip++
|
||||
if !v.truthy(v.pop()!) {
|
||||
return error('assertion failed (ip ${v.ip})')
|
||||
}
|
||||
}
|
||||
op_enter {
|
||||
v.ip++
|
||||
n := int(v.read_i64())
|
||||
for _ in 0..n {
|
||||
v.push(0)!
|
||||
}
|
||||
}
|
||||
else {
|
||||
return error('unknown opcode ${op} at ip ${v.ip}')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn (mut v Vm) read_i64() i64 {
|
||||
mut val := u64(0)
|
||||
for i in 0..8 {
|
||||
val |= u64(v.code[v.ip + i]) << u32(8 * i)
|
||||
}
|
||||
v.ip += 8
|
||||
return i64(val)
|
||||
}
|
||||
|
||||
fn (mut v Vm) push(x i64) ! {
|
||||
if v.sp >= v.stack.len {
|
||||
return error('stack overflow')
|
||||
}
|
||||
v.stack[v.sp] = x
|
||||
v.sp++
|
||||
}
|
||||
|
||||
fn (mut v Vm) pop() !i64 {
|
||||
if v.sp <= 0 {
|
||||
return error('stack underflow')
|
||||
}
|
||||
v.sp--
|
||||
return v.stack[v.sp]
|
||||
}
|
||||
|
||||
fn (mut v Vm) call(target int, argc int) {
|
||||
v.stack[v.sp] = v.enc_int(i64(v.ip)) // return address (ip already past both operands)
|
||||
v.sp++
|
||||
v.stack[v.sp] = v.enc_int(i64(v.bp))
|
||||
v.sp++
|
||||
v.stack[v.sp] = v.enc_int(i64(argc))
|
||||
v.sp++
|
||||
v.bp = v.sp
|
||||
// copy the arguments below the frame into local slots 0..argc-1
|
||||
for i in 0..argc {
|
||||
v.stack[v.bp + i] = v.stack[v.bp - 3 - argc + i]
|
||||
}
|
||||
v.sp = v.bp + argc
|
||||
v.ip = target
|
||||
}
|
||||
|
||||
fn (mut v Vm) ret(with_val bool) ! {
|
||||
retval := if with_val { v.pop()! } else { v.enc_int(0) }
|
||||
v.sp = v.bp - 1
|
||||
argc := int(v.dec_int(v.stack[v.sp]))
|
||||
v.sp = v.bp - 2
|
||||
old_bp := int(v.dec_int(v.stack[v.sp]))
|
||||
v.sp = v.bp - 3
|
||||
ip := int(v.dec_int(v.stack[v.sp]))
|
||||
v.sp -= argc
|
||||
v.bp = old_bp
|
||||
if ip == -1 {
|
||||
// returned to the synthetic frame: we are done
|
||||
v.halted = true
|
||||
v.push(retval)!
|
||||
return
|
||||
}
|
||||
v.ip = ip
|
||||
v.push(retval)!
|
||||
}
|
||||
|
||||
fn (mut v Vm) is_str(x i64) bool {
|
||||
return x & 1 == 1
|
||||
}
|
||||
|
||||
fn (mut v Vm) enc_int(x i64) i64 {
|
||||
return x << 1
|
||||
}
|
||||
|
||||
fn (mut v Vm) dec_int(x i64) i64 {
|
||||
return x >> 1
|
||||
}
|
||||
|
||||
fn (mut v Vm) hand(x i64) int {
|
||||
return int(x >> 1)
|
||||
}
|
||||
|
||||
fn (mut v Vm) mkstr(idx int) i64 {
|
||||
return (i64(idx) << 1) | 1
|
||||
}
|
||||
|
||||
fn (mut v Vm) truthy(x i64) bool {
|
||||
return x != 0
|
||||
}
|
||||
|
||||
fn bool_i64(b bool) i64 {
|
||||
return if b { i64(1) } else { i64(0) }
|
||||
}
|
||||
|
||||
fn (mut v Vm) add(a i64, b i64) !i64 {
|
||||
if v.is_str(a) && v.is_str(b) {
|
||||
return v.alloc_str(v.strings[v.hand(a)] + v.strings[v.hand(b)])
|
||||
}
|
||||
if v.is_str(a) {
|
||||
return v.alloc_str(v.strings[v.hand(a)] + v.num_str(b))
|
||||
}
|
||||
if v.is_str(b) {
|
||||
return v.alloc_str(v.num_str(a) + v.strings[v.hand(b)])
|
||||
}
|
||||
return v.enc_int(v.dec_int(a) + v.dec_int(b))
|
||||
}
|
||||
|
||||
fn (mut v Vm) alloc_str(s string) i64 {
|
||||
v.strings << s
|
||||
return v.mkstr(v.strings.len - 1)
|
||||
}
|
||||
|
||||
fn (mut v Vm) num_str(x i64) string {
|
||||
return v.dec_int(x).str()
|
||||
}
|
||||
|
||||
fn (mut v Vm) arith(a i64, b i64, op string) !i64 {
|
||||
if v.is_str(a) || v.is_str(b) {
|
||||
return error('cannot use strings with "${op}"')
|
||||
}
|
||||
x := v.dec_int(a)
|
||||
y := v.dec_int(b)
|
||||
match op {
|
||||
'-' {
|
||||
return v.enc_int(x - y)
|
||||
}
|
||||
'*' {
|
||||
return v.enc_int(x * y)
|
||||
}
|
||||
'/' {
|
||||
if y == 0 {
|
||||
return error('division by zero')
|
||||
}
|
||||
return v.enc_int(x / y)
|
||||
}
|
||||
'%' {
|
||||
if y == 0 {
|
||||
return error('division by zero')
|
||||
}
|
||||
return v.enc_int(x % y)
|
||||
}
|
||||
else {
|
||||
return error('internal: bad arith op "${op}"')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn (mut v Vm) cmp(a i64, b i64, op string) !i64 {
|
||||
if v.is_str(a) && v.is_str(b) {
|
||||
sa := v.strings[v.hand(a)]
|
||||
sb := v.strings[v.hand(b)]
|
||||
return bool_i64(match op {
|
||||
'==' { sa == sb }
|
||||
'!=' { sa != sb }
|
||||
'<' { sa < sb }
|
||||
'<=' { sa <= sb }
|
||||
'>' { sa > sb }
|
||||
'>=' { sa >= sb }
|
||||
else { return error('internal: bad cmp op "${op}"') }
|
||||
})
|
||||
}
|
||||
if v.is_str(a) || v.is_str(b) {
|
||||
return error('cannot compare a string and a number')
|
||||
}
|
||||
x := v.dec_int(a)
|
||||
y := v.dec_int(b)
|
||||
return bool_i64(match op {
|
||||
'==' { x == y }
|
||||
'!=' { x != y }
|
||||
'<' { x < y }
|
||||
'<=' { x <= y }
|
||||
'>' { x > y }
|
||||
'>=' { x >= y }
|
||||
else { return error('internal: bad cmp op "${op}"') }
|
||||
})
|
||||
}
|
||||
|
||||
fn (mut v Vm) print_val(x i64) {
|
||||
if v.is_str(x) && v.valid_handle(x) {
|
||||
print(v.strings[v.hand(x)])
|
||||
} else {
|
||||
print(v.dec_int(x))
|
||||
}
|
||||
}
|
||||
|
||||
fn (mut v Vm) valid_handle(x i64) bool {
|
||||
h := v.hand(x)
|
||||
return h >= 0 && h < v.strings.len
|
||||
}
|
||||
|
||||
fn (mut v Vm) trace_op(op u8) {
|
||||
name := match op {
|
||||
op_halt { 'halt' }
|
||||
op_push_i { 'push_int' }
|
||||
op_push_s { 'push_str' }
|
||||
op_load { 'load' }
|
||||
op_store { 'store' }
|
||||
op_pop { 'pop' }
|
||||
op_dup { 'dup' }
|
||||
op_add { 'add' }
|
||||
op_sub { 'sub' }
|
||||
op_mul { 'mul' }
|
||||
op_div { 'div' }
|
||||
op_mod { 'mod' }
|
||||
op_neg { 'neg' }
|
||||
op_eq { 'eq' }
|
||||
op_ne { 'ne' }
|
||||
op_lt { 'lt' }
|
||||
op_le { 'le' }
|
||||
op_gt { 'gt' }
|
||||
op_ge { 'ge' }
|
||||
op_and { 'and' }
|
||||
op_or { 'or' }
|
||||
op_not { 'not' }
|
||||
op_jmp { 'jmp' }
|
||||
op_jz { 'jz' }
|
||||
op_jnz { 'jnz' }
|
||||
op_call { 'call' }
|
||||
op_ret { 'ret' }
|
||||
op_retv { 'retv' }
|
||||
op_print { 'print' }
|
||||
op_println { 'println' }
|
||||
op_assert { 'assert' }
|
||||
op_enter { 'enter' }
|
||||
else { '??' }
|
||||
}
|
||||
mut s := ''
|
||||
for i in 0..v.sp {
|
||||
if i > 0 {
|
||||
s += ' '
|
||||
}
|
||||
if v.is_str(v.stack[i]) && v.valid_handle(v.stack[i]) {
|
||||
s += '"${v.strings[v.hand(v.stack[i])]}"'
|
||||
} else {
|
||||
s += '${v.dec_int(v.stack[i])}'
|
||||
}
|
||||
}
|
||||
println(' [ip=${v.ip:4}] ${name:-9} stack: [${s}]')
|
||||
}
|
||||
Reference in New Issue
Block a user