diff --git a/README.MD b/README.MD index a6b2538..ead4add 100644 --- a/README.MD +++ b/README.MD @@ -326,7 +326,19 @@ fn main() { - generics: `fn first[T](arr) { return arr[0] }` with checked call sites `first[int](arr)` — the VM is dynamically typed, so type parameters erase to a single function but arity and duplicates are validated -- variables: `let name = expr`, reassignment `name = expr` +- 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 promotes to float; `floor` / `ceil` / `round` / `sqrt` / `pow` / `abs` / `min` / `max` / `rand` / `rand_int` diff --git a/bin/vc b/bin/vc new file mode 100755 index 0000000..f241fb6 Binary files /dev/null and b/bin/vc differ diff --git a/compiler/ast.v b/compiler/ast.v index f04fc58..bc23670 100644 --- a/compiler/ast.v +++ b/compiler/ast.v @@ -80,6 +80,7 @@ pub mut: pub struct Stmt { pub mut: kind StmtKind + mutable bool // let_stmt/destruct_stmt: bound with `mut` (reassignable) target string idx_target string // for_in_stmt: index variable name (empty when unused) destruct_targets []string // destruct_stmt: names to bind diff --git a/compiler/check.v b/compiler/check.v index 7c30b32..5dcbc97 100644 --- a/compiler/check.v +++ b/compiler/check.v @@ -40,14 +40,15 @@ struct FnSig { struct Checker { mut: - types map[string]TypeInfo // current scope: local name -> type - fns map[string]FnSig - structs map[string][]string - enums map[string][]string - consts map[string]TypeInfo + types map[string]TypeInfo // current scope: local name -> type + mutable map[string]bool // local names bound with `mut`, params, loop vars; only these may be reassigned + fns map[string]FnSig + structs map[string][]string + enums map[string][]string + consts map[string]TypeInfo loop_depth int - checked map[string]bool // imported files already checked - modules map[string]bool // imported module names (bare `import os`) + checked map[string]bool // imported files already checked + modules map[string]bool // imported module names (bare `import os`) } // 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 if fd.recv_name.len > 0 { c.types[fd.recv_name] = TypeInfo{ kind: .struct_t } + c.mutable[fd.recv_name] = true } for p in fd.params { c.types[p] = TypeInfo{ kind: .unknown } + // parameters are reassignable: callers pass values, bodies may rewrite + c.mutable[p] = true } for st in fd.body { c.check_stmt(st)! @@ -165,11 +169,13 @@ fn (mut c Checker) check_stmt(st Stmt) ! { .let_stmt { t := c.check_expr(st.expr)! c.types[st.target] = t + c.mutable[st.target] = st.mutable // false for `let`, true for `mut` } .destruct_stmt { base := c.check_expr(st.expr)! for name in st.destruct_targets { c.types[name] = if st.destruct_field { TypeInfo{ kind: .unknown } } else { TypeInfo{ kind: .unknown } } + c.mutable[name] = false } _ = base } @@ -177,6 +183,9 @@ fn (mut c Checker) check_stmt(st Stmt) ! { if st.target !in c.types { 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)! } .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(end_t, 'range end', st.line)! c.types[st.target] = TypeInfo{ kind: .int_t } + c.mutable[st.target] = true // the loop advances it; bodies may too c.loop_depth++ for s in st.body { c.check_stmt(s)! @@ -243,8 +253,10 @@ fn (mut c Checker) check_stmt(st Stmt) ! { } c.types[st.target] = TypeInfo{ kind: .unknown } } + c.mutable[st.target] = true // loop vars are reassigned by iteration if st.idx_target.len > 0 { c.types[st.idx_target] = TypeInfo{ kind: .int_t } + c.mutable[st.idx_target] = true } c.loop_depth++ for s in st.body { @@ -295,6 +307,10 @@ fn (mut c Checker) check_expr(e Expr) !TypeInfo { c.types[e.name] } else if e.name in c.consts { 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 { 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 bid, bargc := builtin_spec(e.name) 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 { 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 'json_encode', 'format', 'replace', 'pad', 'pad_left', 'repeat' { TypeInfo{ kind: .string_t } } '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 } } 'build_is_dir' { TypeInfo{ kind: .int_t } } // build-module builtins (.vrmm) diff --git a/compiler/codegen.v b/compiler/codegen.v index 20254d0..139ffc0 100644 --- a/compiler/codegen.v +++ b/compiler/codegen.v @@ -45,6 +45,7 @@ mut: enter_off u32 next_lbl int 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 } @@ -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 } } + // 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 for l in imported.lines { 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 } } } + for fd in prog.fns { + g.fn_names[fd.name] = true + } for fd in prog.fns { g.captures = []string{} // top-level functions capture nothing 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) g.code << op_push_i g.code << obj.encode_i64(i64(g.enum_vals[e.name])) - } else { - idx := g.locals[e.name] or { - return error('unknown variable "${e.name}" at line ${e.line}') - } + } else if e.name in g.locals { + idx := g.locals[e.name] g.code << op_load 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 { @@ -911,6 +927,20 @@ fn (mut g Gen) gen_call(e Expr) ! { // host builtins (file I/O, OS, math, collections) go through op_native bid, bargc := builtin_spec(e.name) 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 { 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_left' { native_pad_left, 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 } 'cwd' { native_cwd, 0 } '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' } 'build_compile', 'build_assemble', 'build_link', 'build_exec', 'build_base', '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' } else { '' } } diff --git a/compiler/lexer.v b/compiler/lexer.v index 93389db..a476061 100644 --- a/compiler/lexer.v +++ b/compiler/lexer.v @@ -285,6 +285,7 @@ fn (mut l Lexer) lex_ident(line int, col int) Tok { 'fn' { TokKind.kw_fn } 'struct' { TokKind.kw_struct } 'let' { TokKind.kw_let } + 'mut' { TokKind.kw_mut } 'if' { TokKind.kw_if } 'else' { TokKind.kw_else } 'while' { TokKind.kw_while } diff --git a/compiler/opcodes.v b/compiler/opcodes.v index ff9ce08..32ab24e 100644 --- a/compiler/opcodes.v +++ b/compiler/opcodes.v @@ -172,3 +172,13 @@ const native_path_abs = 185 const native_path_rel = 186 const native_exec_full = 187 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 diff --git a/compiler/parser.v b/compiler/parser.v index 699dab5..b8960e3 100644 --- a/compiler/parser.v +++ b/compiler/parser.v @@ -315,9 +315,11 @@ fn (mut p Parser) parse_block() ![]Stmt { fn (mut p Parser) parse_stmt() !Stmt { t := p.cur() match t.kind { - .kw_let { + .kw_let, .kw_mut { + mutable := t.kind == .kw_mut 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 { is_field := p.cur().kind == .lbrace p.advance() @@ -344,7 +346,7 @@ fn (mut p Parser) parse_stmt() !Stmt { name := p.expect(.ident, 'variable name')! p.expect(.assign, "'='")! e := p.parse_expr()! - return Stmt{ kind: .let_stmt, target: name.lit, expr: e, line: t.line } + return Stmt{ kind: .let_stmt, target: name.lit, expr: e, mutable: mutable, line: t.line } } .kw_if { return p.parse_if(t)! } diff --git a/compiler/tokens.v b/compiler/tokens.v index f0ab8a5..40938b3 100644 --- a/compiler/tokens.v +++ b/compiler/tokens.v @@ -44,6 +44,7 @@ pub enum TokKind { kw_fn kw_struct kw_let + kw_mut kw_if kw_else kw_while diff --git a/examples/arrays.vr b/examples/arrays.vr index d8598cc..d4f5be8 100644 --- a/examples/arrays.vr +++ b/examples/arrays.vr @@ -2,7 +2,7 @@ // Run with: vr run examples/arrays.vr fn sum(items) { - let total = 0 + mut total = 0 for x in items { total = total + x } @@ -79,7 +79,7 @@ fn main() { println("") // find the first even number in an array - let found = -1 + mut found = -1 for x in a { if x % 2 == 0 { found = x diff --git a/examples/compound_assign.vr b/examples/compound_assign.vr index 5d98986..042ad2c 100644 --- a/examples/compound_assign.vr +++ b/examples/compound_assign.vr @@ -1,7 +1,7 @@ // compound assignment operators: +=, -=, *=, /= fn main() { - let x = 10 + mut x = 10 println(x) x += 5 diff --git a/examples/enum_iter.vr b/examples/enum_iter.vr index 7558673..185e5b8 100644 --- a/examples/enum_iter.vr +++ b/examples/enum_iter.vr @@ -25,7 +25,7 @@ fn main() { } // iterate and use count - let total = 0 + mut total = 0 for c in Color { total = total + c.count() } @@ -40,7 +40,7 @@ fn main() { } // break and continue work - let weekdays = 0 + mut weekdays = 0 for d in Day { if d == Day.saturday { break diff --git a/examples/for_index.vr b/examples/for_index.vr index e034e4a..eb88dd3 100644 --- a/examples/for_index.vr +++ b/examples/for_index.vr @@ -10,7 +10,7 @@ fn main() { // use the index for computation let nums = [10, 20, 30, 40, 50] - let sum = 0 + mut sum = 0 for i, v in nums { if i % 2 == 0 { sum += v diff --git a/examples/hello.vr b/examples/hello.vr index 78ad3f9..ebd7e50 100644 --- a/examples/hello.vr +++ b/examples/hello.vr @@ -15,8 +15,8 @@ fn fib(n) { // returns n! via a loop fn fact(n) { - let result = 1 - let i = 1 + mut result = 1 + mut i = 1 while i <= n { result = result * i i = i + 1 @@ -54,7 +54,7 @@ fn main() { } // while loop - let i = 0 + mut i = 0 while i < 3 { println("counting " + i) i = i + 1 diff --git a/examples/match.vr b/examples/match.vr index e17216f..8e100dd 100644 --- a/examples/match.vr +++ b/examples/match.vr @@ -64,7 +64,7 @@ fn main() { } // match inside a loop (break/continue still work) - let found = 0 + mut found = 0 for i in 0..10 { match i { 4 { diff --git a/examples/tests.vr b/examples/tests.vr index 6b4ddbc..1a1199c 100644 --- a/examples/tests.vr +++ b/examples/tests.vr @@ -33,8 +33,8 @@ fn test_strings() { } fn test_loops() { - let total = 0 - let i = 1 + mut total = 0 + mut i = 1 while i <= 10 { total = total + i i = i + 1 @@ -64,12 +64,12 @@ fn test_arrays() { } fn test_for_range() { - let total = 0 + mut total = 0 for i in 0..5 { total = total + i } assert total == 10 // 0+1+2+3+4 - let inc = 0 + mut inc = 0 for i in 1...3 { inc = inc + i } @@ -78,7 +78,7 @@ fn test_for_range() { fn test_for_in() { let a = [5, 6, 7] - let total = 0 + mut total = 0 for x in a { total = total + x } @@ -94,7 +94,7 @@ fn test_for_in() { fn test_break_continue() { // break exits the loop early - let total = 0 + mut total = 0 for i in 0..100 { if i == 5 { break @@ -104,7 +104,7 @@ fn test_break_continue() { assert total == 10 // 0+1+2+3+4 // continue skips to the next iteration - let sum = 0 + mut sum = 0 for i in 0..6 { if i == 3 { continue @@ -114,7 +114,7 @@ fn test_break_continue() { assert sum == 12 // 0+1+2+4+5 (3 skipped) // break and continue in while loops - let n = 0 + mut n = 0 while true { n = n + 1 if n == 3 { @@ -141,7 +141,7 @@ fn test_break_continue() { fn test_else_if() { let score = 85 - let grade = "" + mut grade = "" if score >= 90 { grade = "A" } else if score >= 80 { @@ -156,7 +156,7 @@ fn test_else_if() { } fn test_match() { - let total = 0 + mut total = 0 match 2 { 1 { total = total + 1 @@ -172,7 +172,7 @@ fn test_match() { // match on strings let word = "cat" - let sound = "" + mut sound = "" match word { "dog" { sound = "woof" @@ -187,7 +187,7 @@ fn test_match() { assert sound == "meow" // no else arm: nothing runs when nothing matches - let hit = 0 + mut hit = 0 match 99 { 1 { hit = 1 @@ -376,7 +376,7 @@ fn test_closures() { assert "v=" + str(dbl(4)) == "v=8" // closure containing a loop let sum = fn () { - let total = 0 + mut total = 0 for i in 0..5 { total = total + i } @@ -402,7 +402,7 @@ fn test_enums() { let next = Suit.hearts + 1 assert next == 1 // enum iteration - let names = [] + mut names = [] for s in Suit { names = push(names, s.to_string()) } @@ -471,7 +471,7 @@ fn test_closure_capture() { assert greet() == "hi alice age=30" // capture by value: later changes to the outer var don't affect the closure - let counter = 10 + mut counter = 10 let read = fn() { return counter } @@ -491,7 +491,7 @@ fn test_closure_capture() { assert outer() == 106 // assignment to a captured variable writes the closure's own copy - let n = 1 + mut n = 1 let bump = fn() { n = n + 1 return n @@ -530,17 +530,17 @@ fn test_dynamic_map_keys() { fn test_sort_large() { // merge sort must handle large arrays (O(n log n)) and stay sorted - let a = [] + mut a = [] let n = 2000 - let i = 0 + mut 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 + mut ok = 1 + mut j = 1 while j < n { if a[j - 1] > a[j] { ok = 0 @@ -552,6 +552,75 @@ fn test_sort_large() { 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() { // this one is meant to fail — shows up in `vr test` output assert 1 == 2 diff --git a/examples/time.vr b/examples/time.vr index fbd2a79..aecb9f9 100644 --- a/examples/time.vr +++ b/examples/time.vr @@ -20,7 +20,7 @@ fn main() { // milliseconds for timing let start = time.ms() - let x = 0 + mut x = 0 for i in 0..100000 { x = x + i } diff --git a/lib/strings.vr b/lib/strings.vr index 38e8d9e..37f18f4 100644 --- a/lib/strings.vr +++ b/lib/strings.vr @@ -70,3 +70,19 @@ fn capitalize(s) { } 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) +} diff --git a/vm/native.v b/vm/native.v index d067880..a0cca38 100644 --- a/vm/native.v +++ b/vm/native.v @@ -640,6 +640,50 @@ fn (mut v Vm) native(id int, _argc int) ! { } 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 { v.push(v.alloc_str(os.getwd()))! } diff --git a/vm/opcodes.v b/vm/opcodes.v index ff01edd..208737c 100644 --- a/vm/opcodes.v +++ b/vm/opcodes.v @@ -171,3 +171,13 @@ const native_path_abs = 185 const native_path_rel = 186 const native_exec_full = 187 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 diff --git a/vm/spawn.v b/vm/spawn.v new file mode 100644 index 0000000..366c524 --- /dev/null +++ b/vm/spawn.v @@ -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') +} \ No newline at end of file diff --git a/vm/types.v b/vm/types.v index 3b86b59..5e5b843 100644 --- a/vm/types.v +++ b/vm/types.v @@ -34,6 +34,13 @@ struct Handler { 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. enum DbgMode { run // keep going until the next breakpoint (or the end) @@ -66,6 +73,8 @@ mut: structs []StructVal floats []f64 closures []Closure + builders []StrBuilder + jobs []Job stack []i64 sp int bp int @@ -79,6 +88,7 @@ mut: lines []obj.LineInfo // debug info: code offset -> source line fns []obj.BinFn // function table (for stack traces) 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) build_root string // directory of the .vrmm build module (build_root() builtin) dbg DbgState // interactive debugger state (vr debug) diff --git a/vm/value.v b/vm/value.v index 660f02e..2a2b6ab 100644 --- a/vm/value.v +++ b/vm/value.v @@ -15,6 +15,7 @@ const tag_struct = u64(2) const tag_arr = u64(3) const tag_float = u64(4) const tag_closure = u64(5) +const tag_builder = u64(7) // 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, @@ -50,6 +51,10 @@ fn (mut v Vm) is_closure(x i64) bool { 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 { return x == none_val } @@ -91,6 +96,10 @@ fn (mut v Vm) mkclosure(idx int) i64 { 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. fn (mut v Vm) push_float(f f64) i64 { v.floats << f @@ -137,6 +146,11 @@ fn (mut v Vm) valid_closure_handle(x i64) bool { 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. fn (mut v Vm) valid_handle_for(x i64) bool { 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_float { v.valid_float_handle(x) } tag_closure { v.valid_closure_handle(x) } + tag_builder { v.valid_builder_handle(x) } else { true } } } diff --git a/vm/vm.v b/vm/vm.v index 1ef11b4..6517395 100644 --- a/vm/vm.v +++ b/vm/vm.v @@ -105,6 +105,7 @@ fn new_vm(bin obj.Bin, entry string, opts RunOpts) !Vm { lines: bin.lines fns: bin.fns const_strs: bin.strings.len + bin: bin build_root: opts.root dbg_locals: bin.locals max_ops: opts.max_ops