Tier Three: Added threads/jobs

This commit is contained in:
allexanderbergmns
2026-08-25 17:26:07 +02:00
parent d11f9c8d37
commit 415dec40a2
24 changed files with 495 additions and 47 deletions
+13 -1
View File
@@ -326,7 +326,19 @@ fn main() {
- generics: `fn first[T](arr) { return arr[0] }` with checked call sites - generics: `fn first[T](arr) { return arr[0] }` with checked call sites
`first[int](arr)` — the VM is dynamically typed, so type parameters erase `first[int](arr)` — the VM is dynamically typed, so type parameters erase
to a single function but arity and duplicates are validated to a single function but arity and duplicates are validated
- variables: `let name = expr`, reassignment `name = expr` - variables: `let name = expr` binds **immutably** (reassignment is a
compile-time error); declare `mut name = expr` to opt in to reassignment
`name = expr`. Loop variables, function parameters, and the receiver are
always reassignable, as are element/field writes (`a[i] = v`, `s.f = v`)
- string builders: `strings.builder()` / `strings.build_add(sb, piece)` /
`strings.build_str(sb)` (also unqualified `sb_new` / `sb_add` / `sb_str`)
accumulate many pieces and materialize once — O(n) instead of the O(n²)
of a long `+` chain
- concurrency: `spawn(fn, args...)` runs the closure on its own OS thread in
an isolated VM (captured values and args are deep-copied) and returns a job
id; `spawn_join(id)` blocks until the job finishes and returns its result
(int/float/string, or a flat array/struct rendered as its string form).
Jobs run truly in parallel and their failures propagate as catchable errors
- floats: `3.14`, `0.5`, `1e3` — float literals and `float(x)`; arithmetic - floats: `3.14`, `0.5`, `1e3` — float literals and `float(x)`; arithmetic
promotes to float; `floor` / `ceil` / `round` / `sqrt` / `pow` / `abs` / promotes to float; `floor` / `ceil` / `round` / `sqrt` / `pow` / `abs` /
`min` / `max` / `rand` / `rand_int` `min` / `max` / `rand` / `rand_int`
Executable
BIN
View File
Binary file not shown.
+1
View File
@@ -80,6 +80,7 @@ pub mut:
pub struct Stmt { pub struct Stmt {
pub mut: pub mut:
kind StmtKind kind StmtKind
mutable bool // let_stmt/destruct_stmt: bound with `mut` (reassignable)
target string target string
idx_target string // for_in_stmt: index variable name (empty when unused) idx_target string // for_in_stmt: index variable name (empty when unused)
destruct_targets []string // destruct_stmt: names to bind destruct_targets []string // destruct_stmt: names to bind
+37 -7
View File
@@ -40,14 +40,15 @@ struct FnSig {
struct Checker { struct Checker {
mut: mut:
types map[string]TypeInfo // current scope: local name -> type types map[string]TypeInfo // current scope: local name -> type
fns map[string]FnSig mutable map[string]bool // local names bound with `mut`, params, loop vars; only these may be reassigned
structs map[string][]string fns map[string]FnSig
enums map[string][]string structs map[string][]string
consts map[string]TypeInfo enums map[string][]string
consts map[string]TypeInfo
loop_depth int loop_depth int
checked map[string]bool // imported files already checked checked map[string]bool // imported files already checked
modules map[string]bool // imported module names (bare `import os`) modules map[string]bool // imported module names (bare `import os`)
} }
// check validates a parsed program and returns an error on the first problem. // check validates a parsed program and returns an error on the first problem.
@@ -148,9 +149,12 @@ fn (mut c Checker) check_fn(fd FnDecl) ! {
// receiver and parameters are untyped (unknown) — the runtime is dynamic // receiver and parameters are untyped (unknown) — the runtime is dynamic
if fd.recv_name.len > 0 { if fd.recv_name.len > 0 {
c.types[fd.recv_name] = TypeInfo{ kind: .struct_t } c.types[fd.recv_name] = TypeInfo{ kind: .struct_t }
c.mutable[fd.recv_name] = true
} }
for p in fd.params { for p in fd.params {
c.types[p] = TypeInfo{ kind: .unknown } c.types[p] = TypeInfo{ kind: .unknown }
// parameters are reassignable: callers pass values, bodies may rewrite
c.mutable[p] = true
} }
for st in fd.body { for st in fd.body {
c.check_stmt(st)! c.check_stmt(st)!
@@ -165,11 +169,13 @@ fn (mut c Checker) check_stmt(st Stmt) ! {
.let_stmt { .let_stmt {
t := c.check_expr(st.expr)! t := c.check_expr(st.expr)!
c.types[st.target] = t c.types[st.target] = t
c.mutable[st.target] = st.mutable // false for `let`, true for `mut`
} }
.destruct_stmt { .destruct_stmt {
base := c.check_expr(st.expr)! base := c.check_expr(st.expr)!
for name in st.destruct_targets { for name in st.destruct_targets {
c.types[name] = if st.destruct_field { TypeInfo{ kind: .unknown } } else { TypeInfo{ kind: .unknown } } c.types[name] = if st.destruct_field { TypeInfo{ kind: .unknown } } else { TypeInfo{ kind: .unknown } }
c.mutable[name] = false
} }
_ = base _ = base
} }
@@ -177,6 +183,9 @@ fn (mut c Checker) check_stmt(st Stmt) ! {
if st.target !in c.types { if st.target !in c.types {
return error('unknown variable "${st.target}" (line ${st.line})') return error('unknown variable "${st.target}" (line ${st.line})')
} }
if !(st.target in c.mutable) || !c.mutable[st.target] {
return error('cannot reassign immutable "${st.target}" declare it with `mut` (line ${st.line})')
}
_ = c.check_expr(st.expr)! _ = c.check_expr(st.expr)!
} }
.index_assign { .index_assign {
@@ -225,6 +234,7 @@ fn (mut c Checker) check_stmt(st Stmt) ! {
c.expect_numeric(start_t, 'range start', st.line)! c.expect_numeric(start_t, 'range start', st.line)!
c.expect_numeric(end_t, 'range end', st.line)! c.expect_numeric(end_t, 'range end', st.line)!
c.types[st.target] = TypeInfo{ kind: .int_t } c.types[st.target] = TypeInfo{ kind: .int_t }
c.mutable[st.target] = true // the loop advances it; bodies may too
c.loop_depth++ c.loop_depth++
for s in st.body { for s in st.body {
c.check_stmt(s)! c.check_stmt(s)!
@@ -243,8 +253,10 @@ fn (mut c Checker) check_stmt(st Stmt) ! {
} }
c.types[st.target] = TypeInfo{ kind: .unknown } c.types[st.target] = TypeInfo{ kind: .unknown }
} }
c.mutable[st.target] = true // loop vars are reassigned by iteration
if st.idx_target.len > 0 { if st.idx_target.len > 0 {
c.types[st.idx_target] = TypeInfo{ kind: .int_t } c.types[st.idx_target] = TypeInfo{ kind: .int_t }
c.mutable[st.idx_target] = true
} }
c.loop_depth++ c.loop_depth++
for s in st.body { for s in st.body {
@@ -295,6 +307,10 @@ fn (mut c Checker) check_expr(e Expr) !TypeInfo {
c.types[e.name] c.types[e.name]
} else if e.name in c.consts { } else if e.name in c.consts {
c.consts[e.name] c.consts[e.name]
} else if e.name in c.fns {
// a bare function name used as a value (e.g. the first argument
// of `spawn`) evaluates to a closure
TypeInfo{ kind: .unknown }
} else { } else {
return error('unknown variable "${e.name}" (line ${e.line})') return error('unknown variable "${e.name}" (line ${e.line})')
} }
@@ -539,6 +555,16 @@ fn (mut c Checker) check_call(e Expr) !TypeInfo {
// host builtins (native) — validate arity from the spec table // host builtins (native) — validate arity from the spec table
bid, bargc := builtin_spec(e.name) bid, bargc := builtin_spec(e.name)
if bid >= 0 { if bid >= 0 {
if e.name == 'spawn' {
// variadic: a function followed by zero or more arguments
if e.args.len < 1 {
return error('spawn() expects a function plus zero or more arguments (line ${e.line})')
}
for a in e.args {
_ = c.check_expr(a)!
}
return builtin_result_type(e.name)
}
if e.args.len != bargc { if e.args.len != bargc {
return error('${e.name}() takes exactly ${bargc} argument(s) (line ${e.line})') return error('${e.name}() takes exactly ${bargc} argument(s) (line ${e.line})')
} }
@@ -601,6 +627,10 @@ fn builtin_result_type(name string) TypeInfo {
// stdlib: JSON + string formatting // stdlib: JSON + string formatting
'json_encode', 'format', 'replace', 'pad', 'pad_left', 'repeat' { TypeInfo{ kind: .string_t } } 'json_encode', 'format', 'replace', 'pad', 'pad_left', 'repeat' { TypeInfo{ kind: .string_t } }
'json_decode', 'split_lines' { TypeInfo{ kind: .unknown } } 'json_decode', 'split_lines' { TypeInfo{ kind: .unknown } }
'sb_new', 'sb_add', 'sb_str' { TypeInfo{ kind: .string_t } }
'sb_len' { TypeInfo{ kind: .int_t } }
'spawn' { TypeInfo{ kind: .int_t } }
'spawn_join' { TypeInfo{ kind: .unknown } }
'cwd', 'json_pretty' { TypeInfo{ kind: .string_t } } 'cwd', 'json_pretty' { TypeInfo{ kind: .string_t } }
'build_is_dir' { TypeInfo{ kind: .int_t } } 'build_is_dir' { TypeInfo{ kind: .int_t } }
// build-module builtins (.vrmm) // build-module builtins (.vrmm)
+44 -5
View File
@@ -45,6 +45,7 @@ mut:
enter_off u32 enter_off u32
next_lbl int next_lbl int
modules map[string]bool // imported module names (bare `import os`) modules map[string]bool // imported module names (bare `import os`)
fn_names map[string]bool // top-level function names usable as closure values
captures []string // enclosing locals captured by the closure being compiled captures []string // enclosing locals captured by the closure being compiled
} }
@@ -117,6 +118,10 @@ fn gen(prog Program) !obj.Obj {
} }
g.relocs << obj.Reloc{ offset: u32(code_off) + r.offset, name: rname, kind: r.kind } g.relocs << obj.Reloc{ offset: u32(code_off) + r.offset, name: rname, kind: r.kind }
} }
// record imported function names so a bare name can be used as a value
for s in imported.symbols {
g.fn_names[s.name] = true
}
// merge debug info, rebasing offsets into this object's code space // merge debug info, rebasing offsets into this object's code space
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 }
@@ -127,6 +132,9 @@ fn gen(prog Program) !obj.Obj {
g.dbg_locals << obj.DbgLocal{ fn: prefix + l.fn, name: l.name, slot: l.slot } g.dbg_locals << obj.DbgLocal{ fn: prefix + l.fn, name: l.name, slot: l.slot }
} }
} }
for fd in prog.fns {
g.fn_names[fd.name] = true
}
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
g.gen_fn(fd)! g.gen_fn(fd)!
@@ -800,12 +808,20 @@ fn (mut g Gen) gen_expr(e Expr) ! {
// check if it's an enum variant (e.g., Color.red) // check if it's an enum variant (e.g., Color.red)
g.code << op_push_i g.code << op_push_i
g.code << obj.encode_i64(i64(g.enum_vals[e.name])) g.code << obj.encode_i64(i64(g.enum_vals[e.name]))
} else { } else if e.name in g.locals {
idx := g.locals[e.name] or { idx := g.locals[e.name]
return error('unknown variable "${e.name}" at line ${e.line}')
}
g.code << op_load g.code << op_load
g.code << obj.encode_i64(i64(idx)) g.code << obj.encode_i64(i64(idx))
} else if e.name in g.fn_names {
// a bare top-level function name used as a value (e.g. the first
// argument of `spawn`) — emit a zero-capture closure whose entry
// is resolved by the linker.
g.code << op_closure
g.code << obj.encode_i64(0)
g.relocs << obj.Reloc{ offset: u32(g.code.len) - 8, name: e.name, kind: 0 }
g.code << obj.encode_i64(0) // no captured locals
} else {
return error('unknown variable "${e.name}" at line ${e.line}')
} }
} }
.unary { .unary {
@@ -911,6 +927,20 @@ fn (mut g Gen) gen_call(e Expr) ! {
// host builtins (file I/O, OS, math, collections) go through op_native // host builtins (file I/O, OS, math, collections) go through op_native
bid, bargc := builtin_spec(e.name) bid, bargc := builtin_spec(e.name)
if bid >= 0 { if bid >= 0 {
// spawn takes a variable number of arguments (the function followed by
// its call arguments), so emit the real argc rather than the spec's
if e.name == 'spawn' {
if e.args.len < 1 {
return error('spawn expects a function plus zero or more arguments (line ${e.line})')
}
for a in e.args {
g.gen_expr(a)!
}
g.code << op_native
g.code << obj.encode_i64(i64(bid))
g.code << obj.encode_i64(i64(e.args.len))
return
}
if e.args.len != bargc { if e.args.len != bargc {
return error('${e.name}() takes exactly ${bargc} argument(s) (line ${e.line})') return error('${e.name}() takes exactly ${bargc} argument(s) (line ${e.line})')
} }
@@ -1002,6 +1032,14 @@ fn builtin_spec(name string) (int, int) {
'pad' { native_pad, 2 } 'pad' { native_pad, 2 }
'pad_left' { native_pad_left, 2 } 'pad_left' { native_pad_left, 2 }
'repeat' { native_repeat, 2 } 'repeat' { native_repeat, 2 }
// string builder
'sb_new' { native_sb_new, 0 }
'sb_add' { native_sb_add, 2 }
'sb_str' { native_sb_str, 1 }
'sb_len' { native_sb_len, 1 }
// concurrency
'spawn' { native_spawn, 1 }
'spawn_join' { native_spawn_join, 1 }
'build_is_dir' { native_build_is_dir, 1 } 'build_is_dir' { native_build_is_dir, 1 }
'cwd' { native_cwd, 0 } 'cwd' { native_cwd, 0 }
'json_pretty' { native_json_pretty, 1 } 'json_pretty' { native_json_pretty, 1 }
@@ -1261,7 +1299,8 @@ fn (mut g Gen) expr_type(e Expr) string {
'upper', 'lower', 'trim', 'str', 'getenv', 'read_file', 'join' { 'string' } 'upper', 'lower', 'trim', 'str', 'getenv', 'read_file', 'join' { 'string' }
'build_compile', 'build_assemble', 'build_link', 'build_exec', 'build_base', 'build_compile', 'build_assemble', 'build_link', 'build_exec', 'build_base',
'build_dir', 'build_join', 'build_root' { 'string' } 'build_dir', 'build_join', 'build_root' { 'string' }
'json_encode', 'format', 'replace', 'pad', 'pad_left', 'repeat' { 'string' } 'json_encode', 'format', 'replace', 'pad', 'pad_left', 'repeat', 'sb_new', 'sb_add', 'sb_str' { 'string' }
'sb_len' { 'int' }
'cwd', 'json_pretty' { 'string' } 'cwd', 'json_pretty' { 'string' }
else { '' } else { '' }
} }
+1
View File
@@ -285,6 +285,7 @@ fn (mut l Lexer) lex_ident(line int, col int) Tok {
'fn' { TokKind.kw_fn } 'fn' { TokKind.kw_fn }
'struct' { TokKind.kw_struct } 'struct' { TokKind.kw_struct }
'let' { TokKind.kw_let } 'let' { TokKind.kw_let }
'mut' { TokKind.kw_mut }
'if' { TokKind.kw_if } 'if' { TokKind.kw_if }
'else' { TokKind.kw_else } 'else' { TokKind.kw_else }
'while' { TokKind.kw_while } 'while' { TokKind.kw_while }
+10
View File
@@ -172,3 +172,13 @@ const native_path_abs = 185
const native_path_rel = 186 const native_path_rel = 186
const native_exec_full = 187 const native_exec_full = 187
const native_weekday = 188 const native_weekday = 188
// string builder (efficient repeated concatenation)
const native_sb_new = 189
const native_sb_add = 190
const native_sb_str = 191
const native_sb_len = 192
// concurrency: spawn/join threads
const native_spawn = 193
const native_spawn_join = 194
+5 -3
View File
@@ -315,9 +315,11 @@ fn (mut p Parser) parse_block() ![]Stmt {
fn (mut p Parser) parse_stmt() !Stmt { fn (mut p Parser) parse_stmt() !Stmt {
t := p.cur() t := p.cur()
match t.kind { match t.kind {
.kw_let { .kw_let, .kw_mut {
mutable := t.kind == .kw_mut
p.advance() p.advance()
// destructuring let: `let { a, b } = expr` or `let [a, b] = expr` // mutable with destructuring is not supported (kept simple); `let {
// a, b } = x` and `let [a, b] = x` stay immutable-style bindings
if p.cur().kind == .lbrace || p.cur().kind == .lbracket { if p.cur().kind == .lbrace || p.cur().kind == .lbracket {
is_field := p.cur().kind == .lbrace is_field := p.cur().kind == .lbrace
p.advance() p.advance()
@@ -344,7 +346,7 @@ fn (mut p Parser) parse_stmt() !Stmt {
name := p.expect(.ident, 'variable name')! name := p.expect(.ident, 'variable name')!
p.expect(.assign, "'='")! p.expect(.assign, "'='")!
e := p.parse_expr()! e := p.parse_expr()!
return Stmt{ kind: .let_stmt, target: name.lit, expr: e, line: t.line } return Stmt{ kind: .let_stmt, target: name.lit, expr: e, mutable: mutable, line: t.line }
} .kw_if { } .kw_if {
return p.parse_if(t)! return p.parse_if(t)!
} }
+1
View File
@@ -44,6 +44,7 @@ pub enum TokKind {
kw_fn kw_fn
kw_struct kw_struct
kw_let kw_let
kw_mut
kw_if kw_if
kw_else kw_else
kw_while kw_while
+2 -2
View File
@@ -2,7 +2,7 @@
// Run with: vr run examples/arrays.vr // Run with: vr run examples/arrays.vr
fn sum(items) { fn sum(items) {
let total = 0 mut total = 0
for x in items { for x in items {
total = total + x total = total + x
} }
@@ -79,7 +79,7 @@ fn main() {
println("") println("")
// find the first even number in an array // find the first even number in an array
let found = -1 mut found = -1
for x in a { for x in a {
if x % 2 == 0 { if x % 2 == 0 {
found = x found = x
+1 -1
View File
@@ -1,7 +1,7 @@
// compound assignment operators: +=, -=, *=, /= // compound assignment operators: +=, -=, *=, /=
fn main() { fn main() {
let x = 10 mut x = 10
println(x) println(x)
x += 5 x += 5
+2 -2
View File
@@ -25,7 +25,7 @@ fn main() {
} }
// iterate and use count // iterate and use count
let total = 0 mut total = 0
for c in Color { for c in Color {
total = total + c.count() total = total + c.count()
} }
@@ -40,7 +40,7 @@ fn main() {
} }
// break and continue work // break and continue work
let weekdays = 0 mut weekdays = 0
for d in Day { for d in Day {
if d == Day.saturday { if d == Day.saturday {
break break
+1 -1
View File
@@ -10,7 +10,7 @@ fn main() {
// use the index for computation // use the index for computation
let nums = [10, 20, 30, 40, 50] let nums = [10, 20, 30, 40, 50]
let sum = 0 mut sum = 0
for i, v in nums { for i, v in nums {
if i % 2 == 0 { if i % 2 == 0 {
sum += v sum += v
+3 -3
View File
@@ -15,8 +15,8 @@ fn fib(n) {
// returns n! via a loop // returns n! via a loop
fn fact(n) { fn fact(n) {
let result = 1 mut result = 1
let i = 1 mut i = 1
while i <= n { while i <= n {
result = result * i result = result * i
i = i + 1 i = i + 1
@@ -54,7 +54,7 @@ fn main() {
} }
// while loop // while loop
let i = 0 mut i = 0
while i < 3 { while i < 3 {
println("counting " + i) println("counting " + i)
i = i + 1 i = i + 1
+1 -1
View File
@@ -64,7 +64,7 @@ fn main() {
} }
// match inside a loop (break/continue still work) // match inside a loop (break/continue still work)
let found = 0 mut found = 0
for i in 0..10 { for i in 0..10 {
match i { match i {
4 { 4 {
+89 -20
View File
@@ -33,8 +33,8 @@ fn test_strings() {
} }
fn test_loops() { fn test_loops() {
let total = 0 mut total = 0
let i = 1 mut i = 1
while i <= 10 { while i <= 10 {
total = total + i total = total + i
i = i + 1 i = i + 1
@@ -64,12 +64,12 @@ fn test_arrays() {
} }
fn test_for_range() { fn test_for_range() {
let total = 0 mut total = 0
for i in 0..5 { for i in 0..5 {
total = total + i total = total + i
} }
assert total == 10 // 0+1+2+3+4 assert total == 10 // 0+1+2+3+4
let inc = 0 mut inc = 0
for i in 1...3 { for i in 1...3 {
inc = inc + i inc = inc + i
} }
@@ -78,7 +78,7 @@ fn test_for_range() {
fn test_for_in() { fn test_for_in() {
let a = [5, 6, 7] let a = [5, 6, 7]
let total = 0 mut total = 0
for x in a { for x in a {
total = total + x total = total + x
} }
@@ -94,7 +94,7 @@ fn test_for_in() {
fn test_break_continue() { fn test_break_continue() {
// break exits the loop early // break exits the loop early
let total = 0 mut total = 0
for i in 0..100 { for i in 0..100 {
if i == 5 { if i == 5 {
break break
@@ -104,7 +104,7 @@ fn test_break_continue() {
assert total == 10 // 0+1+2+3+4 assert total == 10 // 0+1+2+3+4
// continue skips to the next iteration // continue skips to the next iteration
let sum = 0 mut sum = 0
for i in 0..6 { for i in 0..6 {
if i == 3 { if i == 3 {
continue continue
@@ -114,7 +114,7 @@ fn test_break_continue() {
assert sum == 12 // 0+1+2+4+5 (3 skipped) assert sum == 12 // 0+1+2+4+5 (3 skipped)
// break and continue in while loops // break and continue in while loops
let n = 0 mut n = 0
while true { while true {
n = n + 1 n = n + 1
if n == 3 { if n == 3 {
@@ -141,7 +141,7 @@ fn test_break_continue() {
fn test_else_if() { fn test_else_if() {
let score = 85 let score = 85
let grade = "" mut grade = ""
if score >= 90 { if score >= 90 {
grade = "A" grade = "A"
} else if score >= 80 { } else if score >= 80 {
@@ -156,7 +156,7 @@ fn test_else_if() {
} }
fn test_match() { fn test_match() {
let total = 0 mut total = 0
match 2 { match 2 {
1 { 1 {
total = total + 1 total = total + 1
@@ -172,7 +172,7 @@ fn test_match() {
// match on strings // match on strings
let word = "cat" let word = "cat"
let sound = "" mut sound = ""
match word { match word {
"dog" { "dog" {
sound = "woof" sound = "woof"
@@ -187,7 +187,7 @@ fn test_match() {
assert sound == "meow" assert sound == "meow"
// no else arm: nothing runs when nothing matches // no else arm: nothing runs when nothing matches
let hit = 0 mut hit = 0
match 99 { match 99 {
1 { 1 {
hit = 1 hit = 1
@@ -376,7 +376,7 @@ fn test_closures() {
assert "v=" + str(dbl(4)) == "v=8" assert "v=" + str(dbl(4)) == "v=8"
// closure containing a loop // closure containing a loop
let sum = fn () { let sum = fn () {
let total = 0 mut total = 0
for i in 0..5 { for i in 0..5 {
total = total + i total = total + i
} }
@@ -402,7 +402,7 @@ fn test_enums() {
let next = Suit.hearts + 1 let next = Suit.hearts + 1
assert next == 1 assert next == 1
// enum iteration // enum iteration
let names = [] mut names = []
for s in Suit { for s in Suit {
names = push(names, s.to_string()) names = push(names, s.to_string())
} }
@@ -471,7 +471,7 @@ fn test_closure_capture() {
assert greet() == "hi alice age=30" assert greet() == "hi alice age=30"
// capture by value: later changes to the outer var don't affect the closure // capture by value: later changes to the outer var don't affect the closure
let counter = 10 mut counter = 10
let read = fn() { let read = fn() {
return counter return counter
} }
@@ -491,7 +491,7 @@ fn test_closure_capture() {
assert outer() == 106 assert outer() == 106
// assignment to a captured variable writes the closure's own copy // assignment to a captured variable writes the closure's own copy
let n = 1 mut n = 1
let bump = fn() { let bump = fn() {
n = n + 1 n = n + 1
return n return n
@@ -530,17 +530,17 @@ fn test_dynamic_map_keys() {
fn test_sort_large() { fn test_sort_large() {
// merge sort must handle large arrays (O(n log n)) and stay sorted // merge sort must handle large arrays (O(n log n)) and stay sorted
let a = [] mut a = []
let n = 2000 let n = 2000
let i = 0 mut i = 0
while i < n { while i < n {
a = push(a, (n - i) % 97) a = push(a, (n - i) % 97)
i = i + 1 i = i + 1
} }
sort(a) sort(a)
assert len(a) == n assert len(a) == n
let ok = 1 mut ok = 1
let j = 1 mut j = 1
while j < n { while j < n {
if a[j - 1] > a[j] { if a[j - 1] > a[j] {
ok = 0 ok = 0
@@ -552,6 +552,75 @@ fn test_sort_large() {
assert a[n - 1] == 96 assert a[n - 1] == 96
} }
fn test_mutable_bindings() {
// `let` is immutable: reassignment is a compile-time error (tested in CI)
// `mut` opts into reassignment
mut counter = 0
counter = counter + 1
counter = counter + 2
assert counter == 3
// `let` still reads fine and does not accept reassignment
let fixed = 42
assert fixed == 42
}
fn test_string_builder() {
// string builders avoid O(n^2) reallocation of repeated `+`
mut sb = strings.builder()
for i in 0..10 {
sb = strings.build_add(sb, "x" + str(i) + ",")
}
let out = strings.build_str(sb)
assert out == "x0,x1,x2,x3,x4,x5,x6,x7,x8,x9,"
}
fn test_spawn_join() {
// spawn runs a closure on an OS thread; spawn_join waits and returns
let j1 = spawn(worker_sum, 100)
let j2 = spawn(worker_sum, 200)
let r1 = spawn_join(j1)
let r2 = spawn_join(j2)
assert r1 == 4950
assert r2 == 19900
}
fn worker_sum(n) {
mut s = 0
mut i = 0
while i < n {
s = s + i
i = i + 1
}
return s
}
fn test_spawn_string_result() {
let j = spawn(worker_greet, "world")
let res = spawn_join(j)
assert res == "hello world"
}
fn worker_greet(name) {
return "hello " + name
}
fn test_spawn_error_propagation() {
mut caught = false
try {
let j = spawn(worker_boom)
let r = spawn_join(j)
assert r == 0
} catch e {
caught = true
}
assert caught
}
fn worker_boom() {
let x = 1 / 0
return x
}
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
+1 -1
View File
@@ -20,7 +20,7 @@ fn main() {
// milliseconds for timing // milliseconds for timing
let start = time.ms() let start = time.ms()
let x = 0 mut x = 0
for i in 0..100000 { for i in 0..100000 {
x = x + i x = x + i
} }
+16
View File
@@ -70,3 +70,19 @@ fn capitalize(s) {
} }
return upper(s[0]) + s[1..] return upper(s[0]) + s[1..]
} }
// builder returns a fresh string builder. Append pieces with build_add and
// materialize once with build_str — O(n) total instead of O(n^2) for a long
// chain of `+`. The same functions are available unqualified as sb_new,
// sb_add, sb_str (used internally by this module's builtin-backed helpers).
fn builder() {
return sb_new()
}
fn build_add(sb, piece) {
return sb_add(sb, piece)
}
fn build_str(sb) {
return sb_str(sb)
}
+44
View File
@@ -640,6 +640,50 @@ fn (mut v Vm) native(id int, _argc int) ! {
} }
v.push(v.alloc_str(s.repeat(n)))! v.push(v.alloc_str(s.repeat(n)))!
} }
// -------------------------------------------------------------------
// string builder — accumulate pieces and join once (linear, not
// quadratic like repeated `+` concatenation)
native_sb_new {
v.builders << StrBuilder{}
v.push(v.mkbuilder(v.builders.len - 1))!
}
native_sb_add {
x := v.pop()!
sb := v.pop()!
if !v.is_builder(sb) || !v.valid_builder_handle(sb) {
return error('sb_add expects a string builder as its first argument')
}
if v.is_str(x) && v.valid_handle(x) {
v.builders[v.hand(sb)].parts << v.strings[v.hand(x)]
} else {
v.builders[v.hand(sb)].parts << v.val_str(x, 0)
}
v.push(sb)!
}
native_sb_str {
sb := v.pop()!
if !v.is_builder(sb) || !v.valid_builder_handle(sb) {
return error('sb_str expects a string builder')
}
v.push(v.alloc_str(v.builders[v.hand(sb)].parts.join('')))!
}
native_sb_len {
sb := v.pop()!
if !v.is_builder(sb) || !v.valid_builder_handle(sb) {
return error('sb_len expects a string builder')
}
mut n := 0
for p in v.builders[v.hand(sb)].parts {
n += p.len
}
v.push(v.enc_int(i64(n)))!
}
native_spawn {
v.push(v.native_spawn(_argc)!)!
}
native_spawn_join {
v.push(v.native_spawn_join()!)!
}
native_cwd { native_cwd {
v.push(v.alloc_str(os.getwd()))! v.push(v.alloc_str(os.getwd()))!
} }
+10
View File
@@ -171,3 +171,13 @@ const native_path_abs = 185
const native_path_rel = 186 const native_path_rel = 186
const native_exec_full = 187 const native_exec_full = 187
const native_weekday = 188 const native_weekday = 188
// string builder (efficient repeated concatenation)
const native_sb_new = 189
const native_sb_add = 190
const native_sb_str = 191
const native_sb_len = 192
// concurrency: spawn/join threads
const native_spawn = 193
const native_spawn_join = 194
+187
View File
@@ -0,0 +1,187 @@
// spawn.v — real OS-thread concurrency for the VuurRaaf VM.
//
// let job = spawn(fn, arg1, arg2, ...) // start, returns a job id
// let result = spawn_join(job) // wait for the result
//
// Each spawned closure runs on its own OS thread in a *fresh, isolated* VM.
// The child heap is built on the parent thread (so the deep-copy of captured
// values and arguments happens while the parent heap is quiescent), then the
// child VM is handed to a worker thread which owns it exclusively. The only
// state shared between threads is the immutable bytecode; the result is
// handed back through a per-job channel.
//
// The child Vm is passed to the worker *by value*: V copies the struct, and
// because the child grows its own slice pools (reallocating the backing on
// append), the worker's mutations never touch the parent's memory.
//
// Supported argument/result shapes: ints, floats, strings, `none`, and flat
// arrays/structs of those, deep-copied into the child heap. Composite
// results are rendered to their string form so the parent never holds a
// dangling child handle.
module vm
// Job records one in-flight spawn; the result arrives on the channel.
struct Job {
mut:
entry int
ready Vm // the isolated child VM, exclusively owned by the worker thread
ch chan JobResult
}
// JobResult is the outcome of a spawned closure, delivered via channel.
struct JobResult {
mut:
ok bool
err string
// exactly one of str / val is set: strings come back as `str` (always
// isomorphic across heaps); other scalar results as `val`; composite
// results are delivered as their string rendering in `str`.
str string
val i64
}
const max_jobs = 256
// spawn is wired to the `spawn` builtin: launch a closure on an OS thread.
fn (mut v Vm) native_spawn(argc int) !i64 {
if argc < 1 {
return error('spawn expects a function followed by zero or more arguments')
}
// stack (top first): arg_{argc-1} ... arg_0, closure
mut args := []i64{len: argc - 1}
for i in 0..argc - 1 {
args[argc - 2 - i] = v.pop()!
}
ch := v.pop()!
if !v.is_closure(ch) || !v.valid_closure_handle(ch) {
return error('spawn expects a function as its first argument')
}
cl := v.closures[v.hand(ch)]
if v.jobs.len >= max_jobs {
return error('too many concurrent spawn jobs (limit ${max_jobs})')
}
child := v.build_job_vm(cl.entry, cl.captured, args)!
idx := v.jobs.len
v.jobs << Job{ entry: cl.entry, ready: child, ch: chan JobResult{cap: 1} }
// pass the child Vm by value: the worker owns this copy exclusively
job_child := v.jobs[idx].ready
ch_result := v.jobs[idx].ch
go run_spawned_job(job_child, ch_result)
return v.enc_int(i64(idx))
}
// native_spawn_join is wired to the `spawn_join` builtin: block until the job
// finished, then return its result (deep-read back into the parent heap).
fn (mut v Vm) native_spawn_join() !i64 {
id := int(v.dec_int(v.pop()!))
if id < 0 || id >= v.jobs.len {
return error('spawn_join: unknown job id ${id}')
}
res := <-v.jobs[id].ch
if !res.ok {
return error('spawned job failed: ${res.err}')
}
if res.str != '' {
return v.alloc_str(res.str)
}
return res.val
}
// build_job_vm constructs the isolated child Vm and pre-loads the closure's
// captured values + call arguments as leading locals. Runs on the parent
// thread while the parent heap is quiescent.
fn (mut v Vm) build_job_vm(entry int, captured []i64, args []i64) !Vm {
mut child := Vm{
code: v.code
strings: v.strings.clone()
stack: []i64{len: stack_cap}
lines: v.lines
fns: v.fns
const_strs: v.const_strs
max_ops: v.max_ops
}
// Synthetic entry frame laid out exactly like Vm.call: [ip, old_bp, argc,
// local_0..local_{argc-1}] with bp at local_0 (so stack[bp-3]=ip,
// stack[bp-2]=old_bp, stack[bp-1]=argc). The synthetic caller is placed at
// bp = 3 + n so that the closure's final `ret` — which reads ip(bp-3),
// old_bp(bp-2), argc(bp-1), unwinds `sp -= argc`, then halts (ip == -1) and
// pushes the return value — lands back at slot (3+n)-3-n = 0, mirroring
// how a top-level `main` returns.
n := captured.len + args.len
child.bp = 3 + n
child.sp = 3 + n
base := child.bp - 3 // ip slot
child.stack[base] = child.enc_int(-1) // return ip: signals the synthetic frame
child.stack[base + 1] = child.enc_int(0) // old_bp
child.stack[base + 2] = child.enc_int(i64(n)) // argc (captured + args)
for i, c in captured {
child.stack[child.bp + i] = v.copy_into(mut child, c)!
}
for i, a in args {
child.stack[child.bp + captured.len + i] = v.copy_into(mut child, a)!
}
child.sp = child.bp + n
child.ip = entry
return child
}
// run_spawned_job is the worker thread entry: it drives the exclusively-owned
// child VM to completion and sends the result back. The child is passed by
// value — V copies the struct (including the freshly-allocated slice backings
// built on the parent thread), so this worker's mutations never alias the
// parent's memory.
fn run_spawned_job(child Vm, ch chan JobResult) {
mut c := child
c.exec() or {
ch <- JobResult{ ok: false, err: err.msg() }
return
}
res := c.stack[0] // left by ret from the synthetic entry frame
if c.is_str(res) && c.valid_handle(res) {
ch <- JobResult{ ok: true, str: c.strings[c.hand(res)] }
return
}
ch <- JobResult{ ok: true, val: res }
}
// copy_into deep-copies a parent value into the child heap, re-interned so
// the child's handles are valid in its own pools. Runs on the parent thread.
fn (mut v Vm) copy_into(mut child Vm, x i64) !i64 {
if v.is_int(x) {
return x
}
if v.is_none(x) {
return x
}
if v.is_float(x) && v.valid_float_handle(x) {
child.floats << v.fval(x)
return child.mkfloat(child.floats.len - 1)
}
if v.is_str(x) && v.valid_handle(x) {
child.strings << v.strings[v.hand(x)]
return child.mkstr(child.strings.len - 1)
}
if v.is_arr(x) && v.valid_arr_handle(x) {
src := v.arrays[v.hand(x)]
mut na := []i64{len: src.len}
for i, el in src {
na[i] = v.copy_into(mut child, el)!
}
child.arrays << na
return child.mkarr(child.arrays.len - 1)
}
if v.is_struct(x) && v.valid_struct_handle(x) {
s := v.structs[v.hand(x)]
mut nf := []Field{len: s.fields.len}
for i, fld in s.fields {
nf[i] = Field{ name: fld.name, val: v.copy_into(mut child, fld.val)! }
}
child.structs << StructVal{ fields: nf, by_name: child.index_fields(nf) }
return child.mkstruct_handle(child.structs.len - 1)
}
if v.is_closure(x) {
return error('spawn: closures capturing composite/closure values are not supported')
}
return error('spawn: unsupported value type for cross-thread copy')
}
+10
View File
@@ -34,6 +34,13 @@ struct Handler {
sp int // stack pointer right after the handler record sp int // stack pointer right after the handler record
} }
// StrBuilder accumulates string parts so repeated concatenation stays linear
// (a `+ cat a + b + c` chain reallocates on every step; a builder joins once).
struct StrBuilder {
mut:
parts []string
}
// DbgMode says what the debugger should do after an interactive session ends. // DbgMode says what the debugger should do after an interactive session ends.
enum DbgMode { enum DbgMode {
run // keep going until the next breakpoint (or the end) run // keep going until the next breakpoint (or the end)
@@ -66,6 +73,8 @@ mut:
structs []StructVal structs []StructVal
floats []f64 floats []f64
closures []Closure closures []Closure
builders []StrBuilder
jobs []Job
stack []i64 stack []i64
sp int sp int
bp int bp int
@@ -79,6 +88,7 @@ mut:
lines []obj.LineInfo // debug info: code offset -> source line lines []obj.LineInfo // debug info: code offset -> source line
fns []obj.BinFn // function table (for stack traces) fns []obj.BinFn // function table (for stack traces)
const_strs int // strings[0..const_strs] are bytecode constants, never collected const_strs int // strings[0..const_strs] are bytecode constants, never collected
bin obj.Bin // the linked program (kept so spawn() can build isolated child VMs)
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 DbgState // interactive debugger state (vr debug)
+15
View File
@@ -15,6 +15,7 @@ const tag_struct = u64(2)
const tag_arr = u64(3) const tag_arr = u64(3)
const tag_float = u64(4) const tag_float = u64(4)
const tag_closure = u64(5) const tag_closure = u64(5)
const tag_builder = u64(7)
// none_val is the sentinel for the `none` literal (and JSON null). Tag 110 // none_val is the sentinel for the `none` literal (and JSON null). Tag 110
// is not a valid encoded integer (those are multiples of 8) nor any handle, // is not a valid encoded integer (those are multiples of 8) nor any handle,
@@ -50,6 +51,10 @@ fn (mut v Vm) is_closure(x i64) bool {
return u64(x) & tag_mask == tag_closure return u64(x) & tag_mask == tag_closure
} }
fn (mut v Vm) is_builder(x i64) bool {
return u64(x) & tag_mask == tag_builder
}
fn (mut v Vm) is_none(x i64) bool { fn (mut v Vm) is_none(x i64) bool {
return x == none_val return x == none_val
} }
@@ -91,6 +96,10 @@ fn (mut v Vm) mkclosure(idx int) i64 {
return i64((u64(idx) << 3) | tag_closure) return i64((u64(idx) << 3) | tag_closure)
} }
fn (mut v Vm) mkbuilder(idx int) i64 {
return i64((u64(idx) << 3) | tag_builder)
}
// push_float interns a float into the pool and returns its tagged handle. // push_float interns a float into the pool and returns its tagged handle.
fn (mut v Vm) push_float(f f64) i64 { fn (mut v Vm) push_float(f f64) i64 {
v.floats << f v.floats << f
@@ -137,6 +146,11 @@ fn (mut v Vm) valid_closure_handle(x i64) bool {
return h >= 0 && h < v.closures.len return h >= 0 && h < v.closures.len
} }
fn (mut v Vm) valid_builder_handle(x i64) bool {
h := v.hand(x)
return h >= 0 && h < v.builders.len
}
// valid_handle_for bounds-checks a handle against the pool matching its tag. // valid_handle_for bounds-checks a handle against the pool matching its tag.
fn (mut v Vm) valid_handle_for(x i64) bool { fn (mut v Vm) valid_handle_for(x i64) bool {
return match v.tag(x) { return match v.tag(x) {
@@ -145,6 +159,7 @@ fn (mut v Vm) valid_handle_for(x i64) bool {
tag_arr { v.valid_arr_handle(x) } tag_arr { v.valid_arr_handle(x) }
tag_float { v.valid_float_handle(x) } tag_float { v.valid_float_handle(x) }
tag_closure { v.valid_closure_handle(x) } tag_closure { v.valid_closure_handle(x) }
tag_builder { v.valid_builder_handle(x) }
else { true } else { true }
} }
} }
+1
View File
@@ -105,6 +105,7 @@ fn new_vm(bin obj.Bin, entry string, opts RunOpts) !Vm {
lines: bin.lines lines: bin.lines
fns: bin.fns fns: bin.fns
const_strs: bin.strings.len const_strs: bin.strings.len
bin: bin
build_root: opts.root build_root: opts.root
dbg_locals: bin.locals dbg_locals: bin.locals
max_ops: opts.max_ops max_ops: opts.max_ops