mirror of
https://github.com/bearlanguageorg/bear.git
synced 2026-08-26 15:17:19 +00:00
Debugger
This commit is contained in:
@@ -77,3 +77,17 @@ jobs:
|
|||||||
- name: Watch-mode and directory-test smoke
|
- name: Watch-mode and directory-test smoke
|
||||||
run: |
|
run: |
|
||||||
./bin/vr test examples | grep -q 'passed'
|
./bin/vr test examples | grep -q 'passed'
|
||||||
|
|
||||||
|
- name: Debugger smoke — breakpoint, locals, backtrace, continue
|
||||||
|
run: |
|
||||||
|
printf 'fn main() {\n\tlet x = 10\n\tlet y = x + 5\n\tprintln(y)\n}\n' > /tmp/dbg_smoke.vr
|
||||||
|
printf 'b 2\nc\nl\np x\nbt\nc\n' | ./bin/vr debug /tmp/dbg_smoke.vr | grep -q 'x = 0'
|
||||||
|
|
||||||
|
- name: Profiler smoke — per-function instruction counts
|
||||||
|
run: |
|
||||||
|
printf 'fn fib(n) {\n\tif n < 2 {\n\t\treturn n\n\t}\n\treturn fib(n - 1) + fib(n - 2)\n}\nfn main() {\n\tprintln(fib(10))\n}\n' > /tmp/prof_smoke.vr
|
||||||
|
./bin/vr profile /tmp/prof_smoke.vr | grep -q 'fib'
|
||||||
|
|
||||||
|
- name: Fuzzer smoke — short run must finish clean
|
||||||
|
run: |
|
||||||
|
./bin/vr fuzz --seed 1 --iters 25 --max-ops 50000 --save-dir /tmp/fuzz_repros | grep -q 'no bugs found'
|
||||||
|
|||||||
@@ -30,8 +30,10 @@ vr compile <file.vr> [-o out.vobj] source -> object
|
|||||||
vr assemble <file.vasm> [-o out.vobj] assembly -> object
|
vr assemble <file.vasm> [-o out.vobj] assembly -> object
|
||||||
vr link <a.vobj> [more.vobj ...] [-o out] objects -> executable (.vbin)
|
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 run <file.vr|file.vbin> compile+link+run, or run a binary
|
||||||
vr debug <file.vr|file.vbin> run with an instruction trace
|
vr debug <file.vr|file.vbin> interactive debugger (breakpoints, step, locals)
|
||||||
vr test <file.vr> run every test_* function
|
vr profile <file.vr|file.vbin> run and report per-function instruction counts
|
||||||
|
vr fuzz [--seed N] [--iters N] fuzz the compiler and VM for crashes/hangs
|
||||||
|
vr test <file.vr|dir> run every test_* function
|
||||||
vr bench <file.vr> [iterations] benchmark main()
|
vr bench <file.vr> [iterations] benchmark main()
|
||||||
vr make [target] [args...] run build.vrmm (target = main)
|
vr make [target] [args...] run build.vrmm (target = main)
|
||||||
vr make -f <file.vrmm> [target] [args...] run another build module
|
vr make -f <file.vrmm> [target] [args...] run another build module
|
||||||
@@ -57,6 +59,55 @@ vr info | loader | alloc | version | help
|
|||||||
./bin/vr run -w f.vr # watch f.vr and rerun on every save
|
./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 test examples # run every test_* fn in every .vr file under examples/
|
||||||
./bin/vr lsp # language server for editors (diagnostics, go-to-def)
|
./bin/vr lsp # language server for editors (diagnostics, go-to-def)
|
||||||
|
./bin/vr run --profile f.vr # hot-function report (instructions + calls)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Debugging
|
||||||
|
|
||||||
|
`vr debug` is an interactive debugger. Run to the first breakpoint (or stop at
|
||||||
|
entry), then inspect state:
|
||||||
|
|
||||||
|
```
|
||||||
|
vr debug f.vr --break 12
|
||||||
|
== stopped at main (line 12, ip 105) — help: h
|
||||||
|
(vr-dbg) l # list locals with values
|
||||||
|
(vr-dbg) p x # print one local
|
||||||
|
(vr-dbg) bt # backtrace (call chain)
|
||||||
|
(vr-dbg) n # next: run to the next line (skips calls)
|
||||||
|
(vr-dbg) s # step: one instruction
|
||||||
|
(vr-dbg) f # finish: run until the current function returns
|
||||||
|
(vr-dbg) b 20 # set another breakpoint
|
||||||
|
(vr-dbg) c # continue
|
||||||
|
```
|
||||||
|
|
||||||
|
Breakpoints stop at the *start* of a source line, so locals hold the values
|
||||||
|
from before that line executes. `vr debug --trace f.vr` keeps the old
|
||||||
|
instruction-by-instruction trace.
|
||||||
|
|
||||||
|
### Fuzzing
|
||||||
|
|
||||||
|
`vr fuzz` generates random programs, compiles them, and runs each one twice
|
||||||
|
with an instruction budget — every case in its own subprocess so a crash in
|
||||||
|
the compiler or VM is isolated and reported instead of killing the run:
|
||||||
|
|
||||||
|
```
|
||||||
|
vr fuzz --seed 42 --iters 500 --save-dir fuzz-repros
|
||||||
|
fuzz: no bugs found
|
||||||
|
```
|
||||||
|
|
||||||
|
Findings (crashes, hangs via `--max-ops`, nondeterminism) are saved as .vr
|
||||||
|
repros; a nonzero exit reports bugs for CI.
|
||||||
|
|
||||||
|
### Profiling
|
||||||
|
|
||||||
|
`vr profile f.vr` (or `vr run --profile f.vr`) counts instructions and calls
|
||||||
|
per function and prints the hot-function table:
|
||||||
|
|
||||||
|
```
|
||||||
|
function calls instr %
|
||||||
|
fib 25 271 96.8%
|
||||||
|
main 0 9 3.2%
|
||||||
|
total 280
|
||||||
```
|
```
|
||||||
|
|
||||||
Quick start:
|
Quick start:
|
||||||
@@ -64,7 +115,7 @@ Quick start:
|
|||||||
```bash
|
```bash
|
||||||
./bin/vr run examples/hello.vr # run a program
|
./bin/vr run examples/hello.vr # run a program
|
||||||
./bin/vr test examples/tests.vr # run the tests (one fails on purpose)
|
./bin/vr test examples/tests.vr # run the tests (one fails on purpose)
|
||||||
./bin/vr debug examples/hello.vr # watch every bytecode instruction
|
./bin/vr debug examples/hello.vr # interactive debugger (breakpoints, step, locals)
|
||||||
|
|
||||||
# the assembler path
|
# the assembler path
|
||||||
./bin/vr assemble examples/math.vasm -o math.vobj
|
./bin/vr assemble examples/math.vasm -o math.vobj
|
||||||
@@ -288,7 +339,7 @@ fn main() {
|
|||||||
literals; `has(m, k)`, `delete(m, k)`, `keys(m)`, `len(m)` all work
|
literals; `has(m, k)`, `delete(m, k)`, `keys(m)`, `len(m)` all work
|
||||||
- arrays: `[e1, e2, ...]`, indexing `a[i]` (read and write), `len(a)`,
|
- arrays: `[e1, e2, ...]`, indexing `a[i]` (read and write), `len(a)`,
|
||||||
`push/insert/remove/pop/sort/reverse/clone/index_of/join`; array literals
|
`push/insert/remove/pop/sort/reverse/clone/index_of/join`; array literals
|
||||||
may nest
|
may nest; `sort` is a stable merge sort — guaranteed O(n log n)
|
||||||
- error handling: `try { ... } catch e { ... }` and `throw "message"` — the
|
- error handling: `try { ... } catch e { ... }` and `throw "message"` — the
|
||||||
runtime unwinds to the nearest catch
|
runtime unwinds to the nearest catch
|
||||||
- bitwise operators: `& | ^ ~ << >>`
|
- bitwise operators: `& | ^ ~ << >>`
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ mut:
|
|||||||
enum_vals map[string]int // 'Enum.variant' -> integer value
|
enum_vals map[string]int // 'Enum.variant' -> integer value
|
||||||
consts map[string]i64 // constant name -> integer value
|
consts map[string]i64 // constant name -> integer value
|
||||||
lines []obj.LineInfo // code offset -> source line (debug info)
|
lines []obj.LineInfo // code offset -> source line (debug info)
|
||||||
|
dbg_locals []obj.DbgLocal // per-function local name -> stack slot (debugger)
|
||||||
local_cnt int
|
local_cnt int
|
||||||
argc int
|
argc int
|
||||||
cur_fn string
|
cur_fn string
|
||||||
@@ -120,6 +121,11 @@ fn gen(prog Program) !obj.Obj {
|
|||||||
for l in imported.lines {
|
for l in imported.lines {
|
||||||
g.lines << obj.LineInfo{ off: u32(code_off) + l.off, line: l.line }
|
g.lines << obj.LineInfo{ off: u32(code_off) + l.off, line: l.line }
|
||||||
}
|
}
|
||||||
|
// merge debug locals: module-internal function names get the same
|
||||||
|
// prefix as their symbols so the debugger can resolve them
|
||||||
|
for l in imported.locals {
|
||||||
|
g.dbg_locals << obj.DbgLocal{ fn: prefix + l.fn, name: l.name, slot: l.slot }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for fd in prog.fns {
|
for fd in prog.fns {
|
||||||
g.captures = []string{} // top-level functions capture nothing
|
g.captures = []string{} // top-level functions capture nothing
|
||||||
@@ -131,6 +137,7 @@ fn gen(prog Program) !obj.Obj {
|
|||||||
code: g.code
|
code: g.code
|
||||||
relocs: g.relocs
|
relocs: g.relocs
|
||||||
lines: g.lines
|
lines: g.lines
|
||||||
|
locals: g.dbg_locals
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,6 +237,10 @@ fn (mut g Gen) gen_fn(fd FnDecl) ! {
|
|||||||
}
|
}
|
||||||
g.fixups.clear()
|
g.fixups.clear()
|
||||||
g.labels.clear()
|
g.labels.clear()
|
||||||
|
// snapshot the live locals as debug info for the debugger: name -> slot
|
||||||
|
for name, slot in g.locals {
|
||||||
|
g.dbg_locals << obj.DbgLocal{ fn: sym, name: name, slot: slot }
|
||||||
|
}
|
||||||
g.cur_fn = ''
|
g.cur_fn = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -528,6 +528,30 @@ fn test_dynamic_map_keys() {
|
|||||||
assert cfg[which]["host"] == "localhost"
|
assert cfg[which]["host"] == "localhost"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn test_sort_large() {
|
||||||
|
// merge sort must handle large arrays (O(n log n)) and stay sorted
|
||||||
|
let a = []
|
||||||
|
let n = 2000
|
||||||
|
let i = 0
|
||||||
|
while i < n {
|
||||||
|
a = push(a, (n - i) % 97)
|
||||||
|
i = i + 1
|
||||||
|
}
|
||||||
|
sort(a)
|
||||||
|
assert len(a) == n
|
||||||
|
let ok = 1
|
||||||
|
let j = 1
|
||||||
|
while j < n {
|
||||||
|
if a[j - 1] > a[j] {
|
||||||
|
ok = 0
|
||||||
|
}
|
||||||
|
j = j + 1
|
||||||
|
}
|
||||||
|
assert ok == 1
|
||||||
|
assert a[0] == 0
|
||||||
|
assert a[n - 1] == 96
|
||||||
|
}
|
||||||
|
|
||||||
fn test_failing() {
|
fn test_failing() {
|
||||||
// this one is meant to fail — shows up in `vr test` output
|
// this one is meant to fail — shows up in `vr test` output
|
||||||
assert 1 == 2
|
assert 1 == 2
|
||||||
|
|||||||
@@ -0,0 +1,509 @@
|
|||||||
|
// fuzz.v — grammar-driven fuzzer for the compiler and VM.
|
||||||
|
//
|
||||||
|
// vr fuzz [--seed N] [--iters N] [--max-ops N] [--save-dir DIR]
|
||||||
|
//
|
||||||
|
// Each iteration generates a random VuurRaaf program from a small typed
|
||||||
|
// grammar (ints, floats, strings, booleans, arrays, structs/maps, closures,
|
||||||
|
// if/while, functions, try/catch), compiles it, and runs it twice with an
|
||||||
|
// instruction budget — all inside a fresh `vr fuzz --child` subprocess, so a
|
||||||
|
// crash in the generator, the compiler, or the VM is isolated to that one
|
||||||
|
// case and the run continues.
|
||||||
|
//
|
||||||
|
// Findings (each saved as a .vr repro in --save-dir, default ./fuzz-repros/):
|
||||||
|
// * crash — the child died (segfault / V panic) in any phase
|
||||||
|
// * hang — caught by the --max-ops instruction budget
|
||||||
|
// * nondeterminism — the same program produced different output across runs
|
||||||
|
module main
|
||||||
|
|
||||||
|
import os
|
||||||
|
import compiler
|
||||||
|
import obj
|
||||||
|
import linker
|
||||||
|
import vm
|
||||||
|
|
||||||
|
struct Rng {
|
||||||
|
mut:
|
||||||
|
state u64
|
||||||
|
}
|
||||||
|
|
||||||
|
fn (mut r Rng) next() u64 {
|
||||||
|
r.state ^= r.state << 13
|
||||||
|
r.state ^= r.state >> 7
|
||||||
|
r.state ^= r.state << 17
|
||||||
|
return r.state
|
||||||
|
}
|
||||||
|
|
||||||
|
fn (mut r Rng) intn(n int) int {
|
||||||
|
if n <= 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return int(r.next() % u64(n))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn (mut r Rng) pick_str(items []string) string {
|
||||||
|
return items[r.intn(items.len)]
|
||||||
|
}
|
||||||
|
|
||||||
|
// FuzzCtx carries the random generator plus the helper functions the
|
||||||
|
// generator may reference from generated expressions.
|
||||||
|
struct FuzzCtx {
|
||||||
|
mut:
|
||||||
|
rng Rng
|
||||||
|
helper []string // helper function names
|
||||||
|
helper_ar []int // argument counts, parallel to helper
|
||||||
|
vars []string // names declared in the function being generated
|
||||||
|
}
|
||||||
|
|
||||||
|
const fuzz_int_ops = ['+', '-', '*', '/', '%']
|
||||||
|
const fuzz_cmp_ops = ['==', '!=', '<', '<=', '>', '>=']
|
||||||
|
const fuzz_bool_ops = ['and', 'or']
|
||||||
|
const fuzz_bools = ['true', 'false']
|
||||||
|
const fuzz_names = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']
|
||||||
|
|
||||||
|
fn toolchain_fuzz(args []string) ! {
|
||||||
|
if args.len > 0 && args[0] == '--child' {
|
||||||
|
// worker mode: generate + compile + run one seed, report via stderr
|
||||||
|
if args.len < 3 {
|
||||||
|
return error('fuzz --child needs <seed> <outdir> [max_ops]')
|
||||||
|
}
|
||||||
|
seed := args[1].u64()
|
||||||
|
outdir := args[2]
|
||||||
|
mut max_ops := i64(200000)
|
||||||
|
if args.len > 3 {
|
||||||
|
max_ops = args[3].i64()
|
||||||
|
}
|
||||||
|
fuzz_child(seed, outdir, max_ops)!
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mut seed := u64(1)
|
||||||
|
mut iters := 500
|
||||||
|
mut max_ops := i64(200000)
|
||||||
|
mut save_dir := 'fuzz-repros'
|
||||||
|
mut i := 0
|
||||||
|
for i < args.len {
|
||||||
|
a := args[i]
|
||||||
|
if a == '--seed' && i + 1 < args.len {
|
||||||
|
seed = args[i + 1].u64()
|
||||||
|
i += 2
|
||||||
|
} else if a == '--iters' && i + 1 < args.len {
|
||||||
|
iters = args[i + 1].int()
|
||||||
|
i += 2
|
||||||
|
} else if a == '--max-ops' && i + 1 < args.len {
|
||||||
|
max_ops = args[i + 1].i64()
|
||||||
|
i += 2
|
||||||
|
} else if a == '--save-dir' && i + 1 < args.len {
|
||||||
|
save_dir = args[i + 1]
|
||||||
|
i += 2
|
||||||
|
} else {
|
||||||
|
return error('unknown fuzz option "${a}" (supported: --seed N, --iters N, --max-ops N, --save-dir DIR)')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
os.mkdir_all(save_dir) or { return error('cannot create save dir ${save_dir}: ${err}') }
|
||||||
|
exe := os.executable()
|
||||||
|
tmpdir := os.join_path(os.temp_dir(), 'vr_fuzz_${os.getpid()}')
|
||||||
|
os.mkdir_all(tmpdir) or { return error('cannot create ${tmpdir}: ${err}') }
|
||||||
|
defer {
|
||||||
|
os.rmdir_all(tmpdir) or {}
|
||||||
|
}
|
||||||
|
println('fuzz: seed=${seed} iters=${iters} max-ops=${max_ops} save-dir=${save_dir}')
|
||||||
|
println('fuzz: each case runs in its own subprocess; findings are saved as .vr repros')
|
||||||
|
mut bugs := 0
|
||||||
|
mut compiled := 0
|
||||||
|
mut ran := 0
|
||||||
|
mut compile_fail := 0
|
||||||
|
mut runtime_err := 0
|
||||||
|
mut nondet := 0
|
||||||
|
for it in 0..iters {
|
||||||
|
if it % 50 == 0 {
|
||||||
|
println('fuzz: iter ${it}/${iters} (compiled=${compiled} bugs=${bugs})')
|
||||||
|
}
|
||||||
|
cur_seed := seed + u64(it)
|
||||||
|
os.rmdir_all(os.join_path(tmpdir, 'case')) or {}
|
||||||
|
outdir := os.join_path(tmpdir, 'case')
|
||||||
|
os.mkdir_all(outdir) or {}
|
||||||
|
run1 := os.execute('"${exe}" fuzz --child ${cur_seed} "${outdir}" ${max_ops}')
|
||||||
|
ran++
|
||||||
|
// classify crashes: anything that is not a clean exit-0 handshake
|
||||||
|
ok1 := run1.exit_code == 0 && (run1.output.contains('RUN_OK') || run1.output.contains('COMPILE_FAIL') || run1.output.contains('VM_ERR') || run1.output.contains('HANG'))
|
||||||
|
if !ok1 {
|
||||||
|
phase := crash_phase(outdir)
|
||||||
|
eprintln('fuzz: CRASH (exit ${run1.exit_code}, phase=${phase}) at iter ${it} seed ${cur_seed}')
|
||||||
|
bugs++
|
||||||
|
save_repro(save_dir, cur_seed, it, outdir, phase)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if run1.output.contains('COMPILE_FAIL') {
|
||||||
|
compile_fail++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if run1.output.contains('VM_ERR') || run1.output.contains('HANG') {
|
||||||
|
runtime_err++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
compiled++
|
||||||
|
// determinism: run the identical case again and compare output
|
||||||
|
run2 := os.execute('"${exe}" fuzz --child ${cur_seed} "${outdir}" ${max_ops}')
|
||||||
|
ran++
|
||||||
|
if run2.exit_code != 0 || run2.output != run1.output {
|
||||||
|
eprintln('fuzz: NONDETERMINISM at iter ${it} seed ${cur_seed}')
|
||||||
|
println('run1 (exit ${run1.exit_code}): ${run1.output}')
|
||||||
|
println('run2 (exit ${run2.exit_code}): ${run2.output}')
|
||||||
|
nondet++
|
||||||
|
bugs++
|
||||||
|
save_repro(save_dir, cur_seed, it, outdir, 'nondeterminism')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println('')
|
||||||
|
println('fuzz: done — ${iters} iterations, ${compiled} compiled, ${ran} runs')
|
||||||
|
println('fuzz: compile-fail=${compile_fail} runtime-errors=${runtime_err} bugs=${bugs} nondeterminism=${nondet}')
|
||||||
|
if bugs > 0 {
|
||||||
|
println('fuzz: BUGS FOUND — repros saved in ${save_dir}/')
|
||||||
|
exit(1)
|
||||||
|
}
|
||||||
|
println('fuzz: no bugs found')
|
||||||
|
}
|
||||||
|
|
||||||
|
// crash_phase inspects the worker's output dir to say where the crash happened.
|
||||||
|
fn crash_phase(outdir string) string {
|
||||||
|
case_file := os.join_path(outdir, 'case.vr')
|
||||||
|
if !os.exists(case_file) {
|
||||||
|
return 'generator'
|
||||||
|
}
|
||||||
|
if !os.exists(os.join_path(outdir, 'compiled.ok')) {
|
||||||
|
return 'compiler'
|
||||||
|
}
|
||||||
|
return 'vm'
|
||||||
|
}
|
||||||
|
|
||||||
|
// save_repro copies the failing case's source into the save dir.
|
||||||
|
fn save_repro(dir string, seed u64, iter int, outdir string, what string) {
|
||||||
|
src := os.read_file(os.join_path(outdir, 'case.vr')) or {
|
||||||
|
// generator crash: no source was produced; record the seed
|
||||||
|
note := os.join_path(dir, 'repro_seed${seed}_iter${iter}.seed')
|
||||||
|
os.write_file(note, 'seed=${seed}\nphase=${what}\n') or {}
|
||||||
|
eprintln('fuzz: saved seed note -> ${note}')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
repro_path := os.join_path(dir, 'repro_seed${seed}_iter${iter}.vr')
|
||||||
|
os.write_file(repro_path, '// repro: ${what}\n// seed=${seed}\n\n${src}') or {}
|
||||||
|
eprintln('fuzz: saved repro -> ${repro_path}')
|
||||||
|
}
|
||||||
|
|
||||||
|
// fuzz_child is the worker: generate one program, save it, compile it, and run
|
||||||
|
// it with an instruction budget. Contract with the parent:
|
||||||
|
// stderr 'COMPILE_FAIL' — program did not compile (expected)
|
||||||
|
// stderr 'VM_ERR' — program ran but raised a runtime error (expected)
|
||||||
|
// stderr 'HANG' — the max-ops budget fired (expected)
|
||||||
|
// stderr 'RUN_OK' — program finished; stdout is its output
|
||||||
|
// Any other termination (nonzero exit, panic, segfault) is a finding.
|
||||||
|
fn fuzz_child(seed u64, outdir string, max_ops i64) ! {
|
||||||
|
src := gen_program(seed)
|
||||||
|
os.write_file(os.join_path(outdir, 'case.vr'), src) or {
|
||||||
|
return error('cannot write case: ${err}')
|
||||||
|
}
|
||||||
|
o := compiler.compile(src) or {
|
||||||
|
eprintln('COMPILE_FAIL')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
os.write_file(os.join_path(outdir, 'compiled.ok'), 'ok') or {}
|
||||||
|
tmp_obj := os.join_path(outdir, 'case.vobj')
|
||||||
|
tmp_bin := os.join_path(outdir, 'case.vbin')
|
||||||
|
obj.write(tmp_obj, o) or { return error('obj write: ${err}') }
|
||||||
|
linker.link([tmp_obj], tmp_bin) or {
|
||||||
|
eprintln('LINK_FAIL: ${err}')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
bin := obj.read_bin(tmp_bin) or { return error('bin read: ${err}') }
|
||||||
|
vm.run_opts(bin, 'main', vm.RunOpts{ max_ops: max_ops }) or {
|
||||||
|
msg := err.msg()
|
||||||
|
if msg.contains('max ops exceeded') {
|
||||||
|
eprintln('HANG')
|
||||||
|
} else {
|
||||||
|
eprintln('VM_ERR')
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
eprintln('RUN_OK')
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// program generation
|
||||||
|
|
||||||
|
fn gen_program(seed u64) string {
|
||||||
|
mut ctx := FuzzCtx{ rng: Rng{ state: if seed == 0 { u64(1) } else { seed } } }
|
||||||
|
mut out := ''
|
||||||
|
// random helper functions (0..3)
|
||||||
|
n_helpers := ctx.rng.intn(4)
|
||||||
|
for h in 0..n_helpers {
|
||||||
|
fname := 'h${h}'
|
||||||
|
ctx.helper << fname
|
||||||
|
ctx.helper_ar << ctx.rng.intn(3)
|
||||||
|
out += gen_fn(mut ctx, fname, 2)
|
||||||
|
}
|
||||||
|
out += 'fn main() {\n'
|
||||||
|
out += gen_body(mut ctx, 3)
|
||||||
|
out += '\tprintln("done")\n}\n'
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// gen_fn generates a function declaration. depth bounds expression nesting.
|
||||||
|
fn gen_fn(mut ctx FuzzCtx, fname string, depth int) string {
|
||||||
|
params := ctx.rng.intn(4)
|
||||||
|
mut out := 'fn ${fname}('
|
||||||
|
mut param_names := []string{}
|
||||||
|
for i in 0..params {
|
||||||
|
p := fuzz_names[ctx.rng.intn(fuzz_names.len)]
|
||||||
|
param_names << p
|
||||||
|
if i > 0 {
|
||||||
|
out += ', '
|
||||||
|
}
|
||||||
|
out += p
|
||||||
|
}
|
||||||
|
out += ') {\n'
|
||||||
|
ctx.vars = []string{}
|
||||||
|
for p in param_names {
|
||||||
|
ctx.vars << p
|
||||||
|
out += '\tlet ${p} = ${p}\n' // re-bind so params are named locals
|
||||||
|
}
|
||||||
|
e := gen_expr(mut ctx, depth, 0)
|
||||||
|
out += '\tlet r = ${e}\n'
|
||||||
|
out += '\treturn r\n}\n'
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// gen_body generates statements for a function body.
|
||||||
|
fn gen_body(mut ctx FuzzCtx, depth int) string {
|
||||||
|
mut out := ''
|
||||||
|
n := ctx.rng.intn(6)
|
||||||
|
for i in 0..n {
|
||||||
|
out += gen_stmt(mut ctx, depth)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gen_stmt(mut ctx FuzzCtx, depth int) string {
|
||||||
|
// control-flow statements recurse into gen_body with depth-1 and are only
|
||||||
|
// allowed above depth 0, so nested if/while/try cannot grow unboundedly
|
||||||
|
// below depth 1 only leaf statements (let/assign/expr/push) are allowed,
|
||||||
|
// so nested control flow cannot grow unboundedly
|
||||||
|
mut t := ctx.rng.intn(7)
|
||||||
|
if depth <= 0 {
|
||||||
|
t = match ctx.rng.intn(4) {
|
||||||
|
0 { 0 }
|
||||||
|
1 { 1 }
|
||||||
|
2 { 4 }
|
||||||
|
else { 5 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match t {
|
||||||
|
0 { // let with expression (declares a fresh name)
|
||||||
|
v := fuzz_names[ctx.rng.intn(fuzz_names.len)]
|
||||||
|
ctx.vars << v
|
||||||
|
e := gen_expr(mut ctx, depth, 0)
|
||||||
|
return '\tlet ${v} = ${e}\n'
|
||||||
|
}
|
||||||
|
1 { // assignment to an existing var (or a let when none exist)
|
||||||
|
if ctx.vars.len > 0 {
|
||||||
|
v := ctx.vars[ctx.rng.intn(ctx.vars.len)]
|
||||||
|
e := gen_expr(mut ctx, depth, 0)
|
||||||
|
return '\t${v} = ${e}\n'
|
||||||
|
}
|
||||||
|
v := fuzz_names[ctx.rng.intn(fuzz_names.len)]
|
||||||
|
ctx.vars << v
|
||||||
|
e := gen_expr(mut ctx, depth, 0)
|
||||||
|
return '\tlet ${v} = ${e}\n'
|
||||||
|
}
|
||||||
|
2 { // if/else
|
||||||
|
cond := gen_expr(mut ctx, depth, 0)
|
||||||
|
b1 := gen_body(mut ctx, depth - 1)
|
||||||
|
b2 := gen_body(mut ctx, depth - 1)
|
||||||
|
return '\tif ${cond} {\n${b1}' + '\t} else {\n${b2}' + '\t}\n'
|
||||||
|
}
|
||||||
|
3 { // while with random condition (sometimes true: exercises max-ops)
|
||||||
|
cond := if ctx.rng.intn(4) == 0 { 'true' } else { gen_expr(mut ctx, depth, 0) }
|
||||||
|
return '\twhile ${cond} {\n${gen_body(mut ctx, depth - 1)}' + '\t}\n'
|
||||||
|
}
|
||||||
|
4 { // expression statement
|
||||||
|
e := gen_expr(mut ctx, depth, 0)
|
||||||
|
return '\t${e}\n'
|
||||||
|
}
|
||||||
|
5 { // push to array
|
||||||
|
if ctx.vars.len == 0 {
|
||||||
|
return '\tlet a = []\n'
|
||||||
|
}
|
||||||
|
v := ctx.vars[ctx.rng.intn(ctx.vars.len)]
|
||||||
|
e := gen_expr(mut ctx, depth, 0)
|
||||||
|
return '\t${v} = push(${v}, ${e})\n'
|
||||||
|
}
|
||||||
|
else { // try/catch
|
||||||
|
return '\ttry {\n${gen_body(mut ctx, depth - 1)}' + '\t} catch e {\n\t\tlet msg = e\n\t}\n'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// gen_expr generates a random expression; depth bounds nesting.
|
||||||
|
fn gen_expr(mut ctx FuzzCtx, depth int, ty int) string {
|
||||||
|
if depth <= 0 || ctx.rng.intn(3) == 0 {
|
||||||
|
return gen_atom(mut ctx, ty)
|
||||||
|
}
|
||||||
|
// pick a construct compatible with the requested type (0 = any)
|
||||||
|
mut choices := [0, 4, 6, 11] // variable, literal, len, call
|
||||||
|
if ty == 0 || ty == 1 {
|
||||||
|
choices << 0 // arithmetic
|
||||||
|
choices << 5 // array index
|
||||||
|
choices << 9 // struct field
|
||||||
|
}
|
||||||
|
if ty == 0 || ty == 3 {
|
||||||
|
choices << 1 // comparison -> bool
|
||||||
|
choices << 2 // boolean ops
|
||||||
|
choices << 3 // not
|
||||||
|
}
|
||||||
|
if ty == 0 || ty == 4 {
|
||||||
|
choices << 7 // string concat
|
||||||
|
}
|
||||||
|
if ty == 0 || ty == 5 {
|
||||||
|
choices << 8 // struct literal
|
||||||
|
}
|
||||||
|
t := choices[ctx.rng.intn(choices.len)]
|
||||||
|
match t {
|
||||||
|
0 { // arithmetic -> int
|
||||||
|
a := gen_expr(mut ctx, depth - 1, 1)
|
||||||
|
op := ctx.rng.pick_str(fuzz_int_ops)
|
||||||
|
b := gen_expr(mut ctx, depth - 1, 1)
|
||||||
|
return '(${a} ${op} ${b})'
|
||||||
|
}
|
||||||
|
1 { // comparison -> bool
|
||||||
|
a := gen_expr(mut ctx, depth - 1, 1)
|
||||||
|
op := ctx.rng.pick_str(fuzz_cmp_ops)
|
||||||
|
b := gen_expr(mut ctx, depth - 1, 1)
|
||||||
|
return '(${a} ${op} ${b})'
|
||||||
|
}
|
||||||
|
2 { // boolean ops -> bool
|
||||||
|
a := gen_expr(mut ctx, depth - 1, 3)
|
||||||
|
op := ctx.rng.pick_str(fuzz_bool_ops)
|
||||||
|
b := gen_expr(mut ctx, depth - 1, 3)
|
||||||
|
return '(${a} ${op} ${b})'
|
||||||
|
}
|
||||||
|
3 { // not -> bool
|
||||||
|
e := gen_expr(mut ctx, depth - 1, 3)
|
||||||
|
return 'not (${e})'
|
||||||
|
}
|
||||||
|
4 { // array literal of ints
|
||||||
|
n := ctx.rng.intn(4)
|
||||||
|
mut parts := []string{}
|
||||||
|
for i in 0..n {
|
||||||
|
parts << gen_expr(mut ctx, depth - 1, 1)
|
||||||
|
}
|
||||||
|
return '[${parts.join(', ')}]'
|
||||||
|
}
|
||||||
|
5 { // array index -> int
|
||||||
|
if ctx.vars.len == 0 {
|
||||||
|
n := ctx.rng.intn(21) - 10
|
||||||
|
return '${n}'
|
||||||
|
}
|
||||||
|
v := ctx.vars[ctx.rng.intn(ctx.vars.len)]
|
||||||
|
ix := ctx.rng.intn(5)
|
||||||
|
return '${v}[${ix}]'
|
||||||
|
}
|
||||||
|
6 { // len() -> int
|
||||||
|
if ctx.vars.len == 0 {
|
||||||
|
n := ctx.rng.intn(21) - 10
|
||||||
|
return '${n}'
|
||||||
|
}
|
||||||
|
v := ctx.vars[ctx.rng.intn(ctx.vars.len)]
|
||||||
|
return 'len(${v})'
|
||||||
|
}
|
||||||
|
7 { // string concat -> string
|
||||||
|
a := gen_expr(mut ctx, depth - 1, 4)
|
||||||
|
b := gen_expr(mut ctx, depth - 1, 4)
|
||||||
|
return '(${a} + ${b})'
|
||||||
|
}
|
||||||
|
8 { // struct literal
|
||||||
|
mut parts := []string{}
|
||||||
|
n := 1 + ctx.rng.intn(3)
|
||||||
|
for i in 0..n {
|
||||||
|
e := gen_expr(mut ctx, depth - 1, 1)
|
||||||
|
parts << '"f${i}": ${e}'
|
||||||
|
}
|
||||||
|
return '{${parts.join(', ')}}'
|
||||||
|
}
|
||||||
|
9 { // struct field access -> int
|
||||||
|
if ctx.vars.len == 0 {
|
||||||
|
n := ctx.rng.intn(21) - 10
|
||||||
|
return '${n}'
|
||||||
|
}
|
||||||
|
v := ctx.vars[ctx.rng.intn(ctx.vars.len)]
|
||||||
|
return '${v}.f${ctx.rng.intn(3)}'
|
||||||
|
}
|
||||||
|
11 { // call a helper
|
||||||
|
if ctx.helper.len > 0 {
|
||||||
|
h := ctx.rng.intn(ctx.helper.len)
|
||||||
|
np := ctx.helper_ar[h]
|
||||||
|
mut args := []string{}
|
||||||
|
for i in 0..np {
|
||||||
|
args << gen_expr(mut ctx, depth - 1, 1)
|
||||||
|
}
|
||||||
|
hname := ctx.helper[h]
|
||||||
|
return '${hname}(${args.join(', ')})'
|
||||||
|
}
|
||||||
|
return gen_atom(mut ctx, ty)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return gen_atom(mut ctx, ty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gen_params_decl(mut ctx FuzzCtx) string {
|
||||||
|
n := ctx.rng.intn(3)
|
||||||
|
mut parts := []string{}
|
||||||
|
for i in 0..n {
|
||||||
|
parts << fuzz_names[ctx.rng.intn(fuzz_names.len)]
|
||||||
|
}
|
||||||
|
return parts.join(', ')
|
||||||
|
}
|
||||||
|
|
||||||
|
// gen_atom generates a leaf expression of the requested type (0 = any).
|
||||||
|
fn gen_atom(mut ctx FuzzCtx, ty int) string {
|
||||||
|
match ty {
|
||||||
|
1 { // int
|
||||||
|
n := ctx.rng.intn(21) - 10
|
||||||
|
return '${n}'
|
||||||
|
}
|
||||||
|
2 { // float
|
||||||
|
n := ctx.rng.intn(101)
|
||||||
|
return '${f64(n) / 10.0}'
|
||||||
|
}
|
||||||
|
3 { // bool
|
||||||
|
return ctx.rng.pick_str(fuzz_bools)
|
||||||
|
}
|
||||||
|
4 { // string
|
||||||
|
n := ctx.rng.intn(100)
|
||||||
|
return '"s${n}"'
|
||||||
|
}
|
||||||
|
else { // any: mostly ints, sometimes a variable or a string
|
||||||
|
t := ctx.rng.intn(10)
|
||||||
|
if t < 5 {
|
||||||
|
n := ctx.rng.intn(21) - 10
|
||||||
|
return '${n}'
|
||||||
|
}
|
||||||
|
if t < 7 {
|
||||||
|
n := ctx.rng.intn(101)
|
||||||
|
return '${f64(n) / 10.0}'
|
||||||
|
}
|
||||||
|
if t < 8 {
|
||||||
|
return ctx.rng.pick_str(fuzz_bools)
|
||||||
|
}
|
||||||
|
if t < 9 {
|
||||||
|
n := ctx.rng.intn(100)
|
||||||
|
return '"s${n}"'
|
||||||
|
}
|
||||||
|
if ctx.vars.len > 0 {
|
||||||
|
return ctx.vars[ctx.rng.intn(ctx.vars.len)]
|
||||||
|
}
|
||||||
|
n := ctx.rng.intn(21) - 10
|
||||||
|
return '${n}'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-1
@@ -16,6 +16,7 @@ pub fn link(paths []string, out string) ! {
|
|||||||
mut symbols := map[string]int{}
|
mut symbols := map[string]int{}
|
||||||
mut relocs := []obj.Reloc{}
|
mut relocs := []obj.Reloc{}
|
||||||
mut lines := []obj.LineInfo{}
|
mut lines := []obj.LineInfo{}
|
||||||
|
mut locals := []obj.DbgLocal{}
|
||||||
for p in paths {
|
for p in paths {
|
||||||
o := obj.read(p)!
|
o := obj.read(p)!
|
||||||
base := code.len
|
base := code.len
|
||||||
@@ -32,6 +33,7 @@ pub fn link(paths []string, out string) ! {
|
|||||||
for l in o.lines {
|
for l in o.lines {
|
||||||
lines << obj.LineInfo{ off: l.off + u32(base), line: l.line }
|
lines << obj.LineInfo{ off: l.off + u32(base), line: l.line }
|
||||||
}
|
}
|
||||||
|
locals << o.locals
|
||||||
}
|
}
|
||||||
// resolve relocations
|
// resolve relocations
|
||||||
for r in relocs {
|
for r in relocs {
|
||||||
@@ -51,7 +53,7 @@ pub fn link(paths []string, out string) ! {
|
|||||||
for name, entry in symbols {
|
for name, entry in symbols {
|
||||||
fns << obj.BinFn{ name: name, entry: entry }
|
fns << obj.BinFn{ name: name, entry: entry }
|
||||||
}
|
}
|
||||||
obj.write_bin(out, obj.Bin{ fns: fns, strings: strings, code: code, lines: lines })!
|
obj.write_bin(out, obj.Bin{ fns: fns, strings: strings, code: code, lines: lines, locals: locals })!
|
||||||
}
|
}
|
||||||
|
|
||||||
fn intern_str(mut table []string, s string) int {
|
fn intern_str(mut table []string, s string) int {
|
||||||
|
|||||||
@@ -61,9 +61,15 @@ fn main() {
|
|||||||
'run', 'r' {
|
'run', 'r' {
|
||||||
toolchain_run(rest) or { die('run', err) }
|
toolchain_run(rest) or { die('run', err) }
|
||||||
}
|
}
|
||||||
'debug', 'd' {
|
'debug', 'd' {
|
||||||
toolchain_debug(rest) or { die('debug', err) }
|
toolchain_debug(rest) or { die('debug', err) }
|
||||||
}
|
}
|
||||||
|
'profile', 'p' {
|
||||||
|
toolchain_profile(rest) or { die('profile', err) }
|
||||||
|
}
|
||||||
|
'fuzz' {
|
||||||
|
toolchain_fuzz(rest) or { die('fuzz', err) }
|
||||||
|
}
|
||||||
'test', 't' {
|
'test', 't' {
|
||||||
toolchain_test(rest) or { die('test', err) }
|
toolchain_test(rest) or { die('test', err) }
|
||||||
}
|
}
|
||||||
@@ -189,8 +195,10 @@ fn toolchain_help() {
|
|||||||
println(' assemble <file.vasm> [-o out.vobj] assemble raw bytecode 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(' 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(' 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(' debug <file.vr|file.vbin> interactive debugger (breakpoints, step, locals)')
|
||||||
println(' test <file.vr> run every test_* function')
|
println(' profile <file.vr|file.vbin> run and report per-function instruction counts')
|
||||||
|
println(' fuzz [--seed N] [--iters N] fuzz the compiler and VM for crashes/hangs')
|
||||||
|
println(' test <file.vr|dir> run every test_* function')
|
||||||
println(' bench <file.vr> [iterations] benchmark main()')
|
println(' bench <file.vr> [iterations] benchmark main()')
|
||||||
println(' repl interactive session')
|
println(' repl interactive session')
|
||||||
println(' lsp language server (JSON-RPC over stdio)')
|
println(' lsp language server (JSON-RPC over stdio)')
|
||||||
@@ -362,16 +370,28 @@ fn toolchain_link(args []string) ! {
|
|||||||
|
|
||||||
fn toolchain_run(args []string) ! {
|
fn toolchain_run(args []string) ! {
|
||||||
if args.len == 0 {
|
if args.len == 0 {
|
||||||
return error('usage: vr run <file.vr|file.vbin> [program-args...] (add -w to watch for changes)')
|
return error('usage: vr run <file.vr|file.vbin> [program-args...] (add -w to watch, --profile to profile, --max-ops N to cap instructions)')
|
||||||
}
|
}
|
||||||
mut watch := false
|
mut watch := false
|
||||||
|
mut profile := false
|
||||||
|
mut max_ops := i64(0)
|
||||||
mut rest := args.clone()
|
mut rest := args.clone()
|
||||||
if rest[0] == '-w' || rest[0] == '--watch' {
|
for rest.len > 0 && (rest[0].starts_with('-') && rest[0] != '-') {
|
||||||
watch = true
|
if rest[0] == '-w' || rest[0] == '--watch' {
|
||||||
rest = rest[1..]
|
watch = true
|
||||||
|
rest = rest[1..]
|
||||||
|
} else if rest[0] == '--profile' {
|
||||||
|
profile = true
|
||||||
|
rest = rest[1..]
|
||||||
|
} else if rest[0] == '--max-ops' && rest.len > 1 {
|
||||||
|
max_ops = rest[1].i64()
|
||||||
|
rest = rest[2..]
|
||||||
|
} else {
|
||||||
|
return error('unknown flag "${rest[0]}" (supported: -w, --profile, --max-ops N)')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if rest.len == 0 {
|
if rest.len == 0 {
|
||||||
return error('usage: vr run <file.vr|file.vbin> [program-args...] (add -w to watch for changes)')
|
return error('usage: vr run <file.vr|file.vbin> [program-args...] (add -w to watch, --profile to profile, --max-ops N to cap instructions)')
|
||||||
}
|
}
|
||||||
f := rest[0]
|
f := rest[0]
|
||||||
prog_args := rest[1..]
|
prog_args := rest[1..]
|
||||||
@@ -381,16 +401,76 @@ fn toolchain_run(args []string) ! {
|
|||||||
}
|
}
|
||||||
if f.ends_with('.vbin') {
|
if f.ends_with('.vbin') {
|
||||||
bin := obj.read_bin(f)!
|
bin := obj.read_bin(f)!
|
||||||
vm.run_with_args(bin, 'main', false, prog_args)!
|
if profile {
|
||||||
|
print_profile(vm.run_profiled(bin, 'main', prog_args)!, f)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
vm.run_opts(bin, 'main', vm.RunOpts{ args: prog_args, max_ops: max_ops })!
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if f.ends_with('.vr') {
|
if f.ends_with('.vr') {
|
||||||
run_src_with_args(f, 'main', false, prog_args)!
|
if profile {
|
||||||
|
run_src_profiled(f, prog_args)!
|
||||||
|
return
|
||||||
|
}
|
||||||
|
run_src_with_args(f, 'main', false, prog_args, max_ops)!
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
return error('unsupported file type: ${f} (expected .vr or .vbin)')
|
return error('unsupported file type: ${f} (expected .vr or .vbin)')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// print_profile renders the profiled run's report as a table.
|
||||||
|
fn print_profile(rep vm.ProfileReport, f string) {
|
||||||
|
println('profile: ${f}')
|
||||||
|
println('${pad_right('function', 24)}${pad_left('calls', 8)}${pad_left('instr', 12)}${pad_left('%', 7)}')
|
||||||
|
for r in rep.rows {
|
||||||
|
if r.instr == 0 && r.calls == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
pct := if rep.total > 0 { 100.0 * f64(r.instr) / f64(rep.total) } else { 0.0 }
|
||||||
|
println('${pad_right(r.name, 24)}${pad_left(r.calls.str(), 8)}${pad_left(r.instr.str(), 12)}${pad_left(pct_fmt(pct) + '%', 7)}')
|
||||||
|
}
|
||||||
|
println('${pad_right('total', 24)}${pad_left(rep.total.str(), 12)}')
|
||||||
|
}
|
||||||
|
|
||||||
|
// pad_left pads s with spaces on the left to reach width w.
|
||||||
|
fn pad_left(s string, w int) string {
|
||||||
|
mut out := s
|
||||||
|
for out.len < w {
|
||||||
|
out = ' ' + out
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// pad_right pads s with spaces on the right to reach width w.
|
||||||
|
fn pad_right(s string, w int) string {
|
||||||
|
mut out := s
|
||||||
|
for out.len < w {
|
||||||
|
out += ' '
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// pct_fmt renders a percentage with one decimal.
|
||||||
|
fn pct_fmt(p f64) string {
|
||||||
|
return '${p:.1f}'
|
||||||
|
}
|
||||||
|
|
||||||
|
// run_src_profiled compiles+links a source file and runs it with profiling.
|
||||||
|
fn run_src_profiled(src string, prog_args []string) ! {
|
||||||
|
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)!
|
||||||
|
print_profile(vm.run_profiled(bin, 'main', prog_args)!, src)
|
||||||
|
}
|
||||||
|
|
||||||
// run_watch recompiles and reruns the program whenever the source file (or
|
// run_watch recompiles and reruns the program whenever the source file (or
|
||||||
// anything it imports) changes — the classic develop-run-edit loop.
|
// anything it imports) changes — the classic develop-run-edit loop.
|
||||||
fn run_watch(f string, prog_args []string) ! {
|
fn run_watch(f string, prog_args []string) ! {
|
||||||
@@ -403,7 +483,7 @@ fn run_watch(f string, prog_args []string) ! {
|
|||||||
// clear the screen between runs for a clean diff of output
|
// clear the screen between runs for a clean diff of output
|
||||||
print('\x1b[2J\x1b[H')
|
print('\x1b[2J\x1b[H')
|
||||||
println('== ${os.file_name(f)} — ${time.now().custom_format('HH:mm:ss')} ==')
|
println('== ${os.file_name(f)} — ${time.now().custom_format('HH:mm:ss')} ==')
|
||||||
run_src_with_args(f, 'main', false, prog_args) or {
|
run_src_with_args(f, 'main', false, prog_args, 0) or {
|
||||||
eprintln('${err.msg()}')
|
eprintln('${err.msg()}')
|
||||||
}
|
}
|
||||||
for {
|
for {
|
||||||
@@ -417,24 +497,80 @@ fn run_watch(f string, prog_args []string) ! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// toolchain_debug starts the interactive debugger: run to the first
|
||||||
|
// --break <line>, then accept commands (continue/step/next/finish/print/...).
|
||||||
|
// With no breakpoints it stops at program entry so breakpoints can be set
|
||||||
|
// before anything runs. --trace keeps the old full instruction trace.
|
||||||
fn toolchain_debug(args []string) ! {
|
fn toolchain_debug(args []string) ! {
|
||||||
if args.len == 0 {
|
mut f := ''
|
||||||
return error('usage: vr debug <file.vr|file.vbin>')
|
mut bps := []int{}
|
||||||
|
mut trace := false
|
||||||
|
mut i := 0
|
||||||
|
for i < args.len {
|
||||||
|
a := args[i]
|
||||||
|
if a == '--break' && i + 1 < args.len {
|
||||||
|
bps << args[i + 1].int()
|
||||||
|
i += 2
|
||||||
|
} else if a == '--trace' {
|
||||||
|
trace = true
|
||||||
|
i++
|
||||||
|
} else if f == '' {
|
||||||
|
f = a
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
return error('unexpected argument "${a}" (usage: vr debug <file> [--break N]... [--trace])')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if f == '' {
|
||||||
|
return error('usage: vr debug <file.vr|file.vbin> [--break N]... [--trace]')
|
||||||
}
|
}
|
||||||
f := args[0]
|
|
||||||
println('debug: tracing execution of ${f}')
|
|
||||||
if f.ends_with('.vbin') {
|
if f.ends_with('.vbin') {
|
||||||
bin := obj.read_bin(f)!
|
bin := obj.read_bin(f)!
|
||||||
vm.run(bin, 'main', true)!
|
vm.run_opts(bin, 'main', vm.RunOpts{ debug: true, breakpoints: bps, trace: trace })!
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if f.ends_with('.vr') {
|
if f.ends_with('.vr') {
|
||||||
run_src(f, 'main', true)!
|
run_src_debug(f, bps, trace)!
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
return error('unsupported file type: ${f} (expected .vr or .vbin)')
|
return error('unsupported file type: ${f} (expected .vr or .vbin)')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// toolchain_profile is `vr profile <file> [args...]` — run with per-function
|
||||||
|
// instruction/call counting and print the hot-function report.
|
||||||
|
fn toolchain_profile(args []string) ! {
|
||||||
|
if args.len == 0 {
|
||||||
|
return error('usage: vr profile <file.vr|file.vbin> [program-args...]')
|
||||||
|
}
|
||||||
|
f := args[0]
|
||||||
|
prog_args := args[1..]
|
||||||
|
if f.ends_with('.vbin') {
|
||||||
|
bin := obj.read_bin(f)!
|
||||||
|
print_profile(vm.run_profiled(bin, 'main', prog_args)!, f)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if f.ends_with('.vr') {
|
||||||
|
run_src_profiled(f, prog_args)!
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return error('unsupported file type: ${f} (expected .vr or .vbin)')
|
||||||
|
}
|
||||||
|
|
||||||
|
// run_src_debug compiles+links a source file and runs it under the debugger.
|
||||||
|
fn run_src_debug(src string, bps []int, 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_opts(bin, 'main', vm.RunOpts{ debug: true, breakpoints: bps, trace: trace })!
|
||||||
|
}
|
||||||
|
|
||||||
fn toolchain_test(args []string) ! {
|
fn toolchain_test(args []string) ! {
|
||||||
if args.len == 0 {
|
if args.len == 0 {
|
||||||
return error('usage: vr test <file.vr|dir>')
|
return error('usage: vr test <file.vr|dir>')
|
||||||
@@ -573,10 +709,10 @@ fn toolchain_bench(args []string) ! {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn run_src(src string, entry string, trace bool) ! {
|
fn run_src(src string, entry string, trace bool) ! {
|
||||||
run_src_with_args(src, entry, trace, []string{})!
|
run_src_with_args(src, entry, trace, []string{}, 0)!
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_src_with_args(src string, entry string, trace bool, args []string) ! {
|
fn run_src_with_args(src string, entry string, trace bool, args []string, max_ops i64) ! {
|
||||||
tmp_obj := os.join_path(os.temp_dir(), 'vr_${os.getpid()}.vobj')
|
tmp_obj := os.join_path(os.temp_dir(), 'vr_${os.getpid()}.vobj')
|
||||||
tmp_bin := os.join_path(os.temp_dir(), 'vr_${os.getpid()}.vbin')
|
tmp_bin := os.join_path(os.temp_dir(), 'vr_${os.getpid()}.vbin')
|
||||||
defer {
|
defer {
|
||||||
@@ -587,7 +723,7 @@ fn run_src_with_args(src string, entry string, trace bool, args []string) ! {
|
|||||||
obj.write(tmp_obj, o)!
|
obj.write(tmp_obj, o)!
|
||||||
linker.link([tmp_obj], tmp_bin)!
|
linker.link([tmp_obj], tmp_bin)!
|
||||||
bin := obj.read_bin(tmp_bin)!
|
bin := obj.read_bin(tmp_bin)!
|
||||||
vm.run_with_args(bin, entry, trace, args)!
|
vm.run_opts(bin, entry, vm.RunOpts{ trace: trace, args: args, max_ops: max_ops })!
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ pub mut:
|
|||||||
code []u8
|
code []u8
|
||||||
relocs []Reloc
|
relocs []Reloc
|
||||||
lines []LineInfo
|
lines []LineInfo
|
||||||
|
locals []DbgLocal
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct BinFn {
|
pub struct BinFn {
|
||||||
@@ -54,12 +55,23 @@ pub mut:
|
|||||||
line int
|
line int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DbgLocal is one named local variable of a function (debug info for the
|
||||||
|
// interactive debugger): the VM can resolve `print <name>` to the value at
|
||||||
|
// stack slot bp+slot while stopped inside `fn`.
|
||||||
|
pub struct DbgLocal {
|
||||||
|
pub mut:
|
||||||
|
fn string
|
||||||
|
name string
|
||||||
|
slot int
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Bin {
|
pub struct Bin {
|
||||||
pub mut:
|
pub mut:
|
||||||
fns []BinFn
|
fns []BinFn
|
||||||
strings []string
|
strings []string
|
||||||
code []u8
|
code []u8
|
||||||
lines []LineInfo
|
lines []LineInfo
|
||||||
|
locals []DbgLocal
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -143,10 +155,14 @@ fn (mut r Reader) read_str() !string {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// VROBJ
|
// VROBJ
|
||||||
|
|
||||||
|
// format_version is 2 since v0.2: a debug-locals section was appended after
|
||||||
|
// the line table. Version 1 files (no locals) still read fine.
|
||||||
|
const format_version = u8(2)
|
||||||
|
|
||||||
pub fn write(path string, o Obj) ! {
|
pub fn write(path string, o Obj) ! {
|
||||||
mut b := []u8{}
|
mut b := []u8{}
|
||||||
b << magic.bytes()
|
b << magic.bytes()
|
||||||
b << u8(1) // format version
|
b << format_version
|
||||||
b << encode_u32(u32(o.symbols.len))
|
b << encode_u32(u32(o.symbols.len))
|
||||||
for s in o.symbols {
|
for s in o.symbols {
|
||||||
b << encode_u32(u32(s.name.len))
|
b << encode_u32(u32(s.name.len))
|
||||||
@@ -172,6 +188,14 @@ pub fn write(path string, o Obj) ! {
|
|||||||
b << encode_u32(l.off)
|
b << encode_u32(l.off)
|
||||||
b << encode_i64(i64(l.line))
|
b << encode_i64(i64(l.line))
|
||||||
}
|
}
|
||||||
|
b << encode_u32(u32(o.locals.len))
|
||||||
|
for l in o.locals {
|
||||||
|
b << encode_u32(u32(l.fn.len))
|
||||||
|
b << l.fn.bytes()
|
||||||
|
b << encode_u32(u32(l.name.len))
|
||||||
|
b << l.name.bytes()
|
||||||
|
b << encode_i64(i64(l.slot))
|
||||||
|
}
|
||||||
os.write_bytes(path, b)!
|
os.write_bytes(path, b)!
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,7 +205,7 @@ pub fn read(path string) !Obj {
|
|||||||
return error('not a VROBJ file: ${path}')
|
return error('not a VROBJ file: ${path}')
|
||||||
}
|
}
|
||||||
mut r := Reader{ b: b, pos: magic.len }
|
mut r := Reader{ b: b, pos: magic.len }
|
||||||
_ := r.u8_()! // version
|
ver := r.u8_()! // format version
|
||||||
mut o := Obj{}
|
mut o := Obj{}
|
||||||
nsym := int(r.u32_()!)
|
nsym := int(r.u32_()!)
|
||||||
for _ in 0..nsym {
|
for _ in 0..nsym {
|
||||||
@@ -212,6 +236,15 @@ pub fn read(path string) !Obj {
|
|||||||
line := int(r.i64_()!)
|
line := int(r.i64_()!)
|
||||||
o.lines << LineInfo{ off: off, line: line }
|
o.lines << LineInfo{ off: off, line: line }
|
||||||
}
|
}
|
||||||
|
if ver >= 2 {
|
||||||
|
nlocals := int(r.u32_()!)
|
||||||
|
for _ in 0..nlocals {
|
||||||
|
fn_name := r.read_str()!
|
||||||
|
name := r.read_str()!
|
||||||
|
slot := int(r.i64_()!)
|
||||||
|
o.locals << DbgLocal{ fn: fn_name, name: name, slot: slot }
|
||||||
|
}
|
||||||
|
}
|
||||||
return o
|
return o
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,7 +254,7 @@ pub fn read(path string) !Obj {
|
|||||||
pub fn write_bin(path string, bin Bin) ! {
|
pub fn write_bin(path string, bin Bin) ! {
|
||||||
mut b := []u8{}
|
mut b := []u8{}
|
||||||
b << bin_magic.bytes()
|
b << bin_magic.bytes()
|
||||||
b << u8(1) // format version
|
b << format_version
|
||||||
b << encode_u32(u32(bin.fns.len))
|
b << encode_u32(u32(bin.fns.len))
|
||||||
for f in bin.fns {
|
for f in bin.fns {
|
||||||
b << encode_u32(u32(f.name.len))
|
b << encode_u32(u32(f.name.len))
|
||||||
@@ -240,6 +273,14 @@ pub fn write_bin(path string, bin Bin) ! {
|
|||||||
b << encode_u32(l.off)
|
b << encode_u32(l.off)
|
||||||
b << encode_i64(i64(l.line))
|
b << encode_i64(i64(l.line))
|
||||||
}
|
}
|
||||||
|
b << encode_u32(u32(bin.locals.len))
|
||||||
|
for l in bin.locals {
|
||||||
|
b << encode_u32(u32(l.fn.len))
|
||||||
|
b << l.fn.bytes()
|
||||||
|
b << encode_u32(u32(l.name.len))
|
||||||
|
b << l.name.bytes()
|
||||||
|
b << encode_i64(i64(l.slot))
|
||||||
|
}
|
||||||
os.write_bytes(path, b)!
|
os.write_bytes(path, b)!
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,7 +290,7 @@ pub fn read_bin(path string) !Bin {
|
|||||||
return error('not a VRBIN file: ${path}')
|
return error('not a VRBIN file: ${path}')
|
||||||
}
|
}
|
||||||
mut r := Reader{ b: b, pos: bin_magic.len }
|
mut r := Reader{ b: b, pos: bin_magic.len }
|
||||||
_ := r.u8_()! // version
|
ver := r.u8_()! // format version
|
||||||
mut bin := Bin{}
|
mut bin := Bin{}
|
||||||
nfn := int(r.u32_()!)
|
nfn := int(r.u32_()!)
|
||||||
for _ in 0..nfn {
|
for _ in 0..nfn {
|
||||||
@@ -273,5 +314,14 @@ pub fn read_bin(path string) !Bin {
|
|||||||
line := int(r.i64_()!)
|
line := int(r.i64_()!)
|
||||||
bin.lines << LineInfo{ off: off, line: line }
|
bin.lines << LineInfo{ off: off, line: line }
|
||||||
}
|
}
|
||||||
|
if ver >= 2 {
|
||||||
|
nlocals := int(r.u32_()!)
|
||||||
|
for _ in 0..nlocals {
|
||||||
|
fn_name := r.read_str()!
|
||||||
|
name := r.read_str()!
|
||||||
|
slot := int(r.i64_()!)
|
||||||
|
bin.locals << DbgLocal{ fn: fn_name, name: name, slot: slot }
|
||||||
|
}
|
||||||
|
}
|
||||||
return bin
|
return bin
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,178 @@
|
|||||||
|
// dbg.v — the interactive debugger, enabled by `vr debug`.
|
||||||
|
//
|
||||||
|
// While enabled the VM checks dbg_tick before every instruction. When a
|
||||||
|
// breakpoint is hit (or a step/next/finish condition is met) it drops into
|
||||||
|
// dbg_session, a small command loop over stdin:
|
||||||
|
//
|
||||||
|
// c | continue resume until the next breakpoint (or the end)
|
||||||
|
// s | step execute one instruction
|
||||||
|
// n | next run to the next line of the current frame (skip calls)
|
||||||
|
// f | finish run until the current function returns
|
||||||
|
// b [line] set a breakpoint on a source line (no arg: list them)
|
||||||
|
// d <n> delete breakpoint n (1-based)
|
||||||
|
// p <name> print a local variable's value
|
||||||
|
// l | locals list the current function's locals and their values
|
||||||
|
// bt | stack print the call chain
|
||||||
|
// q | quit abort the program
|
||||||
|
//
|
||||||
|
// Reading EOF (e.g. piped input) is treated as `continue`, so scripts can
|
||||||
|
// drive a session non-interactively.
|
||||||
|
module vm
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
// dbg_tick runs once per instruction while the debugger is enabled and
|
||||||
|
// decides whether to stop and open an interactive session.
|
||||||
|
fn (mut v Vm) dbg_tick() ! {
|
||||||
|
line := v.line_at(v.ip)
|
||||||
|
mut stop := false
|
||||||
|
match v.dbg.mode {
|
||||||
|
.step {
|
||||||
|
stop = true
|
||||||
|
}
|
||||||
|
.next {
|
||||||
|
// run until the line changes while still at (or above) the frame
|
||||||
|
// level where next began — calls push deeper frames, which we skip
|
||||||
|
stop = v.bp <= v.dbg.start_bp && line != v.dbg.last_line
|
||||||
|
}
|
||||||
|
.finish {
|
||||||
|
// run until the current frame returns to its caller
|
||||||
|
stop = v.bp < v.dbg.start_bp
|
||||||
|
}
|
||||||
|
.run {
|
||||||
|
// stop at the first instruction of a breakpoint line only, so a
|
||||||
|
// multi-instruction line does not re-trigger mid-line
|
||||||
|
if line in v.dbg.breakpoints {
|
||||||
|
first := v.ip == 0 || v.line_at(v.ip - 1) != line
|
||||||
|
stop = first
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if stop {
|
||||||
|
v.dbg_session()!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// dbg_session is the interactive command loop. It returns when the user
|
||||||
|
// chooses a resume mode (continue/step/next/finish) or quits.
|
||||||
|
fn (mut v Vm) dbg_session() ! {
|
||||||
|
v.dbg.last_line = v.line_at(v.ip)
|
||||||
|
println('')
|
||||||
|
println('== stopped at ${v.func_at(v.ip)} (line ${v.dbg.last_line}, ip ${v.ip}) — help: h')
|
||||||
|
for {
|
||||||
|
input := os.input_opt('(vr-dbg) ') or { 'c' } // EOF → continue
|
||||||
|
parts := input.trim_space().split(' ')
|
||||||
|
cmd := parts[0]
|
||||||
|
arg := if parts.len > 1 { parts[1] } else { '' }
|
||||||
|
match cmd {
|
||||||
|
'c', 'continue', '' {
|
||||||
|
v.dbg.mode = .run
|
||||||
|
return
|
||||||
|
}
|
||||||
|
's', 'step' {
|
||||||
|
v.dbg.mode = .step
|
||||||
|
return
|
||||||
|
}
|
||||||
|
'n', 'next' {
|
||||||
|
v.dbg.mode = .next
|
||||||
|
v.dbg.start_bp = v.bp
|
||||||
|
return
|
||||||
|
}
|
||||||
|
'f', 'finish' {
|
||||||
|
v.dbg.mode = .finish
|
||||||
|
v.dbg.start_bp = v.bp
|
||||||
|
return
|
||||||
|
}
|
||||||
|
'b', 'break' {
|
||||||
|
if arg == '' {
|
||||||
|
if v.dbg.breakpoints.len == 0 {
|
||||||
|
println(' no breakpoints set')
|
||||||
|
} else {
|
||||||
|
for i, bp in v.dbg.breakpoints {
|
||||||
|
println(' ${i + 1}: line ${bp}')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
line := arg.int()
|
||||||
|
if line <= 0 {
|
||||||
|
println(' usage: b <line>')
|
||||||
|
} else if line !in v.dbg.breakpoints {
|
||||||
|
v.dbg.breakpoints << line
|
||||||
|
println(' breakpoint set at line ${line}')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'd', 'delete' {
|
||||||
|
n := arg.int()
|
||||||
|
if n >= 1 && n <= v.dbg.breakpoints.len {
|
||||||
|
v.dbg.breakpoints.delete(n - 1)
|
||||||
|
println(' breakpoint ${n} deleted')
|
||||||
|
} else {
|
||||||
|
println(' usage: d <n> (see `b` for the list)')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'p', 'print' {
|
||||||
|
if arg == '' {
|
||||||
|
println(' usage: p <name>')
|
||||||
|
} else {
|
||||||
|
v.dbg_print_local(arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'l', 'locals' {
|
||||||
|
v.dbg_list_locals()
|
||||||
|
}
|
||||||
|
'bt', 'stack', 'backtrace' {
|
||||||
|
println(v.stack_trace())
|
||||||
|
}
|
||||||
|
'h', 'help' {
|
||||||
|
println(' c continue · s step · n next · f finish · b [line] · d <n>')
|
||||||
|
println(' p <name> · l locals · bt stack · q quit')
|
||||||
|
}
|
||||||
|
'q', 'quit', 'exit' {
|
||||||
|
v.halted = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
println(' unknown command "${cmd}" — h for help')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// dbg_print_local prints the value of one local variable of the current
|
||||||
|
// function, resolving its slot from the debug locals table.
|
||||||
|
fn (mut v Vm) dbg_print_local(name string) {
|
||||||
|
fn_name := v.func_at(v.ip)
|
||||||
|
slot := v.dbg_slot(fn_name, name)
|
||||||
|
if slot < 0 {
|
||||||
|
println(' no local "${name}" in ${fn_name}')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
println(' ${name} = ${v.val_str(v.stack[v.bp + slot], 0)}')
|
||||||
|
}
|
||||||
|
|
||||||
|
// dbg_list_locals prints every named local of the current function with its
|
||||||
|
// current value.
|
||||||
|
fn (mut v Vm) dbg_list_locals() {
|
||||||
|
fn_name := v.func_at(v.ip)
|
||||||
|
mut found := false
|
||||||
|
for l in v.dbg_locals {
|
||||||
|
if l.fn == fn_name {
|
||||||
|
found = true
|
||||||
|
println(' ${l.name} = ${v.val_str(v.stack[v.bp + l.slot], 0)} (slot ${l.slot})')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
println(' (no named locals for ${fn_name})')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// dbg_slot finds the stack slot of a local by (function, name), or -1.
|
||||||
|
fn (v Vm) dbg_slot(fn_name string, name string) int {
|
||||||
|
for l in v.dbg_locals {
|
||||||
|
if l.fn == fn_name && l.name == name {
|
||||||
|
return l.slot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
+44
-10
@@ -236,16 +236,11 @@ fn (mut v Vm) native(id int, _argc int) ! {
|
|||||||
return error('sort() expects an array')
|
return error('sort() expects an array')
|
||||||
}
|
}
|
||||||
mut a := v.arrays[v.hand(h)]
|
mut a := v.arrays[v.hand(h)]
|
||||||
// insertion sort by numeric value
|
// merge sort: guaranteed O(n log n) (insertion sort was O(n^2) on
|
||||||
for i in 1..a.len {
|
// large or reversed inputs) and stable, so equal elements keep
|
||||||
key := a[i]
|
// their original order
|
||||||
mut j := i - 1
|
mut tmp := []i64{len: a.len}
|
||||||
for j >= 0 && v.num_gt(a[j], key) {
|
v.merge_sort(mut a, mut tmp, 0, a.len)
|
||||||
a[j + 1] = a[j]
|
|
||||||
j--
|
|
||||||
}
|
|
||||||
a[j + 1] = key
|
|
||||||
}
|
|
||||||
v.push(h)!
|
v.push(h)!
|
||||||
}
|
}
|
||||||
native_clone {
|
native_clone {
|
||||||
@@ -1099,6 +1094,45 @@ fn (mut v Vm) str_method(name string, argc int) ! {
|
|||||||
return error('unknown string method "${name}"')
|
return error('unknown string method "${name}"')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// merge_sort sorts a[lo..hi) ascending by numeric value, using tmp as the
|
||||||
|
// scratch buffer (must be at least hi long). It is stable: equal elements
|
||||||
|
// keep their relative order.
|
||||||
|
fn (mut v Vm) merge_sort(mut a []i64, mut tmp []i64, lo int, hi int) {
|
||||||
|
if hi - lo <= 1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mid := lo + (hi - lo) / 2
|
||||||
|
v.merge_sort(mut a, mut tmp, lo, mid)
|
||||||
|
v.merge_sort(mut a, mut tmp, mid, hi)
|
||||||
|
mut i := lo
|
||||||
|
mut j := mid
|
||||||
|
mut k := lo
|
||||||
|
for i < mid && j < hi {
|
||||||
|
if !v.num_gt(a[i], a[j]) {
|
||||||
|
// a[i] <= a[j]: take from the left half (equal -> left, so stable)
|
||||||
|
tmp[k] = a[i]
|
||||||
|
i++
|
||||||
|
} else {
|
||||||
|
tmp[k] = a[j]
|
||||||
|
j++
|
||||||
|
}
|
||||||
|
k++
|
||||||
|
}
|
||||||
|
for i < mid {
|
||||||
|
tmp[k] = a[i]
|
||||||
|
i++
|
||||||
|
k++
|
||||||
|
}
|
||||||
|
for j < hi {
|
||||||
|
tmp[k] = a[j]
|
||||||
|
j++
|
||||||
|
k++
|
||||||
|
}
|
||||||
|
for x in lo..hi {
|
||||||
|
a[x] = tmp[x]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// num_gt compares two values by their numeric value (int or float).
|
// num_gt compares two values by their numeric value (int or float).
|
||||||
fn (mut v Vm) num_gt(x i64, y i64) bool {
|
fn (mut v Vm) num_gt(x i64, y i64) bool {
|
||||||
if v.is_float(x) || v.is_float(y) {
|
if v.is_float(x) || v.is_float(y) {
|
||||||
|
|||||||
+32
@@ -34,6 +34,30 @@ struct Handler {
|
|||||||
sp int // stack pointer right after the handler record
|
sp int // stack pointer right after the handler record
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DbgMode says what the debugger should do after an interactive session ends.
|
||||||
|
enum DbgMode {
|
||||||
|
run // keep going until the next breakpoint (or the end)
|
||||||
|
step // stop at the very next instruction
|
||||||
|
next // stop after the current line returns to this frame level
|
||||||
|
finish // stop when the current function returns
|
||||||
|
}
|
||||||
|
|
||||||
|
// DbgState is the interactive debugger's runtime state, checked once per
|
||||||
|
// instruction while enabled.
|
||||||
|
struct DbgState {
|
||||||
|
mut:
|
||||||
|
enabled bool
|
||||||
|
breakpoints []int // source lines to stop at (first instruction of the line)
|
||||||
|
mode DbgMode
|
||||||
|
start_bp int // frame base captured when next/finish began
|
||||||
|
last_line int // line at the moment the session stopped
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FnEntry {
|
||||||
|
idx int
|
||||||
|
entry int
|
||||||
|
}
|
||||||
|
|
||||||
struct Vm {
|
struct Vm {
|
||||||
mut:
|
mut:
|
||||||
code []u8
|
code []u8
|
||||||
@@ -57,6 +81,14 @@ mut:
|
|||||||
const_strs int // strings[0..const_strs] are bytecode constants, never collected
|
const_strs int // strings[0..const_strs] are bytecode constants, never collected
|
||||||
last_heap int // heap size at the last GC check (allocation trigger)
|
last_heap int // heap size at the last GC check (allocation trigger)
|
||||||
build_root string // directory of the .vrmm build module (build_root() builtin)
|
build_root string // directory of the .vrmm build module (build_root() builtin)
|
||||||
|
dbg DbgState // interactive debugger state (vr debug)
|
||||||
|
dbg_locals []obj.DbgLocal // local name -> slot per function (debugger)
|
||||||
|
profiling bool // instruction/call counting (vr run --profile)
|
||||||
|
prof_instr []u64 // instructions executed per function index
|
||||||
|
prof_calls []u64 // calls made per function index
|
||||||
|
fn_of_ip []int // code offset -> function index (for profiling)
|
||||||
|
max_ops i64 // instruction budget; 0 = unlimited (fuzzing safety)
|
||||||
|
ops i64 // instructions executed so far
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bool_i64(b bool) i64 {
|
fn bool_i64(b bool) i64 {
|
||||||
|
|||||||
@@ -8,6 +8,19 @@ module vm
|
|||||||
import obj
|
import obj
|
||||||
import math
|
import math
|
||||||
|
|
||||||
|
// RunOpts configures a VM run: tracing, program arguments, the build root,
|
||||||
|
// the interactive debugger, an instruction budget, and profiling.
|
||||||
|
pub struct RunOpts {
|
||||||
|
pub:
|
||||||
|
trace bool
|
||||||
|
args []string = []
|
||||||
|
root string
|
||||||
|
debug bool // start the interactive debugger (vr debug)
|
||||||
|
breakpoints []int // source lines to stop at; empty + debug = stop at entry
|
||||||
|
max_ops i64 // instruction budget; 0 = unlimited
|
||||||
|
profile bool // count instructions/calls per function
|
||||||
|
}
|
||||||
|
|
||||||
// run executes the function named `entry` from the executable `bin` and
|
// run executes the function named `entry` from the executable `bin` and
|
||||||
// returns its return value (0 if it never returns one).
|
// returns its return value (0 if it never returns one).
|
||||||
pub fn run(bin obj.Bin, entry string, trace bool) !i64 {
|
pub fn run(bin obj.Bin, entry string, trace bool) !i64 {
|
||||||
@@ -17,27 +30,95 @@ pub fn run(bin obj.Bin, entry string, trace bool) !i64 {
|
|||||||
// run_with_args is run() with command-line arguments exposed to the program
|
// run_with_args is run() with command-line arguments exposed to the program
|
||||||
// via the `args()` builtin.
|
// via the `args()` builtin.
|
||||||
pub fn run_with_args(bin obj.Bin, entry string, trace bool, args []string) !i64 {
|
pub fn run_with_args(bin obj.Bin, entry string, trace bool, args []string) !i64 {
|
||||||
return run_internal(bin, entry, trace, args, '')!
|
return run_opts(bin, entry, RunOpts{ trace: trace, args: args })!
|
||||||
}
|
}
|
||||||
|
|
||||||
// run_build executes a .vrmm build module: the entry target receives the
|
// run_build executes a .vrmm build module: the entry target receives the
|
||||||
// extra CLI arguments via `args()`, and `build_root()` reports the module's
|
// extra CLI arguments via `args()`, and `build_root()` reports the module's
|
||||||
// own directory so scripts can find files regardless of the working directory.
|
// own directory so scripts can find files regardless of the working directory.
|
||||||
pub fn run_build(bin obj.Bin, entry string, args []string, root string) !i64 {
|
pub fn run_build(bin obj.Bin, entry string, args []string, root string) !i64 {
|
||||||
return run_internal(bin, entry, false, args, root)!
|
return run_opts(bin, entry, RunOpts{ args: args, root: root })!
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_internal(bin obj.Bin, entry string, trace bool, args []string, root string) !i64 {
|
// run_opts runs the program with full control over the runtime options.
|
||||||
|
pub fn run_opts(bin obj.Bin, entry string, opts RunOpts) !i64 {
|
||||||
|
mut v := new_vm(bin, entry, opts)!
|
||||||
|
return v.run_result()!
|
||||||
|
}
|
||||||
|
|
||||||
|
// run_debug runs the program under the interactive debugger, stopping at the
|
||||||
|
// given source-line breakpoints (or at entry when none are given).
|
||||||
|
pub fn run_debug(bin obj.Bin, entry string, breakpoints []int, args []string) !i64 {
|
||||||
|
return run_opts(bin, entry, RunOpts{ args: args, debug: true, breakpoints: breakpoints })!
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProfileRow is one function's profile totals.
|
||||||
|
pub struct ProfileRow {
|
||||||
|
pub:
|
||||||
|
name string
|
||||||
|
calls u64
|
||||||
|
instr u64
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProfileReport is the result of a profiled run: per-function instruction
|
||||||
|
// and call counts, sorted by instructions executed (hot first).
|
||||||
|
pub struct ProfileReport {
|
||||||
|
pub:
|
||||||
|
rows []ProfileRow
|
||||||
|
total u64 // instructions executed across all functions
|
||||||
|
}
|
||||||
|
|
||||||
|
// run_profiled executes the program counting instructions and calls per
|
||||||
|
// function, and returns the report.
|
||||||
|
pub fn run_profiled(bin obj.Bin, entry string, args []string) !ProfileReport {
|
||||||
|
mut v := new_vm(bin, entry, RunOpts{ args: args, profile: true })!
|
||||||
|
_ = v.run_result()!
|
||||||
|
mut rows := []ProfileRow{}
|
||||||
|
for i in 0..v.fns.len {
|
||||||
|
rows << ProfileRow{ name: v.fns[i].name, calls: v.prof_calls[i], instr: v.prof_instr[i] }
|
||||||
|
}
|
||||||
|
rows.sort_with_compare(fn (a &ProfileRow, b &ProfileRow) int {
|
||||||
|
if a.instr > b.instr {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if a.instr < b.instr {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
mut total := u64(0)
|
||||||
|
for r in rows {
|
||||||
|
total += r.instr
|
||||||
|
}
|
||||||
|
return ProfileReport{ rows: rows, total: total }
|
||||||
|
}
|
||||||
|
|
||||||
|
// new_vm builds a configured Vm for the entry function, pushing the synthetic
|
||||||
|
// entry frame and pointing ip at the entry point.
|
||||||
|
fn new_vm(bin obj.Bin, entry string, opts RunOpts) !Vm {
|
||||||
mut v := Vm{
|
mut v := Vm{
|
||||||
code: bin.code
|
code: bin.code
|
||||||
strings: bin.strings.clone()
|
strings: bin.strings.clone()
|
||||||
stack: []i64{len: stack_cap}
|
stack: []i64{len: stack_cap}
|
||||||
trace: trace
|
trace: opts.trace
|
||||||
prog_args: args
|
prog_args: opts.args
|
||||||
lines: bin.lines
|
lines: bin.lines
|
||||||
fns: bin.fns
|
fns: bin.fns
|
||||||
const_strs: bin.strings.len
|
const_strs: bin.strings.len
|
||||||
build_root: root
|
build_root: opts.root
|
||||||
|
dbg_locals: bin.locals
|
||||||
|
max_ops: opts.max_ops
|
||||||
|
}
|
||||||
|
if opts.profile {
|
||||||
|
v.profiling = true
|
||||||
|
v.prof_instr = []u64{len: v.fns.len}
|
||||||
|
v.prof_calls = []u64{len: v.fns.len}
|
||||||
|
v.fn_of_ip = v.build_fn_of_ip()
|
||||||
|
}
|
||||||
|
if opts.debug {
|
||||||
|
v.dbg.enabled = true
|
||||||
|
v.dbg.breakpoints = opts.breakpoints.clone()
|
||||||
|
v.dbg.mode = if opts.breakpoints.len > 0 { DbgMode.run } else { DbgMode.step }
|
||||||
}
|
}
|
||||||
mut entry_ip := -1
|
mut entry_ip := -1
|
||||||
for f in bin.fns {
|
for f in bin.fns {
|
||||||
@@ -61,6 +142,11 @@ fn run_internal(bin obj.Bin, entry string, trace bool, args []string, root strin
|
|||||||
v.sp++
|
v.sp++
|
||||||
v.bp = v.sp
|
v.bp = v.sp
|
||||||
v.ip = entry_ip
|
v.ip = entry_ip
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// run_result executes until halt/error and extracts the program's result.
|
||||||
|
fn (mut v Vm) run_result() !i64 {
|
||||||
v.exec() or {
|
v.exec() or {
|
||||||
return error('${err.msg()} at ${v.where()}\n${v.stack_trace()}')
|
return error('${err.msg()} at ${v.where()}\n${v.stack_trace()}')
|
||||||
}
|
}
|
||||||
@@ -73,6 +159,29 @@ fn run_internal(bin obj.Bin, entry string, trace bool, args []string, root strin
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// build_fn_of_ip precomputes, for every code offset, the index of the
|
||||||
|
// function that contains it, so profiling adds one array lookup per opcode.
|
||||||
|
// Function tables are not guaranteed to be in entry order (the linker builds
|
||||||
|
// them from a map), so entries are sorted by offset first.
|
||||||
|
fn (v Vm) build_fn_of_ip() []int {
|
||||||
|
mut out := []int{len: v.code.len}
|
||||||
|
mut fes := []FnEntry{}
|
||||||
|
for i, f in v.fns {
|
||||||
|
fes << FnEntry{ idx: i, entry: f.entry }
|
||||||
|
}
|
||||||
|
fes.sort_with_compare(fn (a &FnEntry, b &FnEntry) int {
|
||||||
|
return a.entry - b.entry
|
||||||
|
})
|
||||||
|
mut fi := 0
|
||||||
|
for ip in 0..v.code.len {
|
||||||
|
for fi + 1 < fes.len && fes[fi + 1].entry <= ip {
|
||||||
|
fi++
|
||||||
|
}
|
||||||
|
out[ip] = fes[fi].idx
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// where returns a source-level location for the current instruction pointer:
|
// where returns a source-level location for the current instruction pointer:
|
||||||
// `line 12 (ip 345)` when debug info is available, otherwise just `(ip 345)`.
|
// `line 12 (ip 345)` when debug info is available, otherwise just `(ip 345)`.
|
||||||
fn (v Vm) where() string {
|
fn (v Vm) where() string {
|
||||||
@@ -145,6 +254,18 @@ fn (mut v Vm) exec() ! {
|
|||||||
if v.trace {
|
if v.trace {
|
||||||
v.trace_op(op)
|
v.trace_op(op)
|
||||||
}
|
}
|
||||||
|
if v.profiling {
|
||||||
|
v.prof_instr[v.fn_of_ip[v.ip]]++
|
||||||
|
}
|
||||||
|
if v.max_ops > 0 {
|
||||||
|
v.ops++
|
||||||
|
if v.ops > v.max_ops {
|
||||||
|
return error('max ops exceeded (${v.max_ops}) — possible infinite loop')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if v.dbg.enabled {
|
||||||
|
v.dbg_tick()!
|
||||||
|
}
|
||||||
match op {
|
match op {
|
||||||
op_halt {
|
op_halt {
|
||||||
v.halted = true
|
v.halted = true
|
||||||
@@ -305,6 +426,9 @@ fn (mut v Vm) exec() ! {
|
|||||||
v.ip++
|
v.ip++
|
||||||
target := int(v.read_i64())
|
target := int(v.read_i64())
|
||||||
argc := int(v.read_i64())
|
argc := int(v.read_i64())
|
||||||
|
if v.profiling {
|
||||||
|
v.prof_calls[v.fn_of_ip[target]]++
|
||||||
|
}
|
||||||
v.call(target, argc)
|
v.call(target, argc)
|
||||||
}
|
}
|
||||||
op_ret {
|
op_ret {
|
||||||
@@ -633,6 +757,9 @@ fn (mut v Vm) exec() ! {
|
|||||||
v.stack[c + i] = cl.captured[i]
|
v.stack[c + i] = cl.captured[i]
|
||||||
}
|
}
|
||||||
v.sp = c + n + argc
|
v.sp = c + n + argc
|
||||||
|
if v.profiling {
|
||||||
|
v.prof_calls[v.fn_of_ip[cl.entry]]++
|
||||||
|
}
|
||||||
v.call(cl.entry, argc + n)
|
v.call(cl.entry, argc + n)
|
||||||
}
|
}
|
||||||
op_argc {
|
op_argc {
|
||||||
|
|||||||
Reference in New Issue
Block a user