diff --git a/README.MD b/README.MD index 21df361..5ebca81 100644 --- a/README.MD +++ b/README.MD @@ -37,9 +37,21 @@ vr clean remove .vobj/.vbin artifacts vr up rebuild bin/vr vr symlink link bin/vr into your PATH vr config [set ] toolchain config (outdir, verbose) +vr repl interactive session +vr fmt [-w] format source (keeps comments) +vr init [name] scaffold a project (vr.mod + main.vr) +vr get fetch a package into vendor/ +vr install install dependencies from vr.mod +vr list show the project manifest vr info | loader | alloc | version | help ``` +```bash +./bin/vr repl # try expressions and functions interactively +./bin/vr fmt -w f.vr # normalize a file's indentation/spacing in place +./bin/vr init myproj # start a project; vr get owner/repo fetches packages +``` + Quick start: ```bash @@ -61,9 +73,10 @@ Quick start: ## The VuurRaaf language -A small, V-flavored language. Values are 64-bit integers, strings, arrays, or -structs (strings concatenate with `+` and compare with `==`/`!=`; arrays and -structs are mutable references that compare by identity). +A small, V-flavored language. Values are 64-bit integers, 64-bit floats, +strings, arrays, structs, enums, and closures (strings concatenate with `+` +and compare with `==`/`!=`; arrays and structs are mutable references that +compare by identity). ``` fn sum(items) { @@ -138,10 +151,29 @@ fn main() { } ``` -- functions: `fn name(a, b) { ... }` with `return expr` +- functions: `fn name(a, b) { ... }` with `return expr`; default parameter + values `fn f(a, b = 10)`, variadic params `fn f(nums...)`, destructuring + `let { a, b } = rec` and `let [x, y] = arr`, and anonymous closures + `let f = fn(x) { return x * 2 }` stored in variables and arrays +- generics: `fn first[T](arr) { return arr[0] }` with checked call sites + `first[int](arr)` — the VM is dynamically typed, so type parameters erase + to a single function but arity and duplicates are validated - variables: `let name = expr`, reassignment `name = expr` +- floats: `3.14`, `0.5`, `1e3` — float literals and `float(x)`; arithmetic + promotes to float; `floor` / `ceil` / `round` / `sqrt` / `pow` / `abs` / + `min` / `max` / `rand` / `rand_int` +- strings are UTF-8: `len(s)` counts characters, `s[i]` and `s[a..b]` index + and slice by character (runes), and methods like `s.to_upper()`, + `s.contains(x)`, `s.split(d)`, `s.index_of(x)`, `s.to_int()`, `s.len()` + work on any string-valued expression - arrays: `[e1, e2, ...]`, indexing `a[i]` (read and write), `len(a)`, - `push(a, v)`; array literals may nest + `push/insert/remove/pop/sort/reverse/clone/index_of/join`; array literals + may nest +- error handling: `try { ... } catch e { ... }` and `throw "message"` — the + runtime unwinds to the nearest catch +- bitwise operators: `& | ^ ~ << >>` +- host builtins: `read_file` / `write_file`, `args()`, `getenv` / `setenv`, + `exit`, `sleep`, `time()`, `type(x)`, `str(x)`, `int(x)`, `split` / `join` - for loops: `for x in arr { }` and ranges `for i in 0..10 { }` / `for i in 0...10 { }`; loop variables are scoped to the loop body - `break` / `continue` inside `while` and `for` loops (in `for` loops @@ -153,9 +185,14 @@ fn main() { field access `a.b` and assignment `a.b = v` (chained: `a[i].b`, `a.b[i]`); structs are mutable references (identity `==`/`!=`), and setting a missing field adds it, so records can be built incrementally -- operators: `+ - * / %`, `== != < <= > >=`, `and or not`, unary `-` +- enums: `enum Color { red green blue }` with `Color.red`, `e.to_string()`, + `e.count()`, and iteration in `for` +- constants: `const NAME = 42` (compile-time integer/bool values) +- operators: `+ - * / %`, `== != < <= > >=`, `and or not`, `& | ^ ~ << >>`, + unary `-`; constant expressions fold at compile time - statements: `let`, assignment, `if/else`, `match`, `while`, `for`, - `break`, `continue`, `return`, `assert`, calls, `print(...)` / `println(...)` + `break`, `continue`, `return`, `assert`, `try/catch`/`throw`, calls, + `print(...)` / `println(...)` - comments: `//` ## Assembly @@ -198,25 +235,32 @@ field name as a string first, exactly like the compiler does). | module | role | |--------------|-------------------------------------------------------------| -| `compiler/` | lexer, recursive-descent parser, bytecode codegen (VROBJ) | +| `compiler/` | lexer, parser, type checker, bytecode codegen (VROBJ) | | `assembler/` | `.vasm` -> VROBJ | | `linker/` | resolves relocations, rebases strings, emits VRBIN | | `vm/` | stack VM: tagged values, call frames, string/array heaps | | `obj/` | VROBJ/VRBIN binary formats | | `bin/` | small standalone tools: `tl_alloc.v`, `tl_loader.v` | -The VM is a stack machine with 64-bit tagged values using two tag bits: -numbers are stored shifted left by two, string handles end in `01`, array -handles in `11` — so no integer ever collides with a heap handle. Arrays live -in a growable heap (`mkarray`/`aget`/`aset`/`alen`/`apush`). Calls push a -frame (return address, base pointer, argc), copy arguments into local slots, -and reserve extra locals with `enter n`. `vr debug` prints every instruction -with the stack contents (arrays rendered as `[1, 2, ...]`). +The VM is a stack machine with 64-bit tagged values using three tag bits: +ints, string/array/struct/float/closure handles — so no integer ever +collides with a heap handle. A mark-and-sweep garbage collector runs between +opcodes when the heap grows past a threshold, tracing the stack (which holds +every frame's locals) and compacting the pools; string constants baked into +bytecode are never collected. Bytecode carries a line table, so runtime +errors report the source line. A conservative compile-time type checker +(`compiler/check.v`) rejects provably wrong programs (unknown variables, +field access on numbers, arithmetic on strings, wrong arity) while leaving +dynamic programs alone. `vr debug` prints every instruction with the stack +contents (arrays rendered as `[1, 2, ...]`). ## Repository layout ``` main.v CLI entry point (vr ...) +repl.v interactive REPL +fmt.v source formatter +pkg.v package manager (init/get/install/list) v.mod module definition compiler/ assembler/ linker/ vm/ obj/ the toolchain itself bin/ built binary + standalone tools diff --git a/compiler/ast.v b/compiler/ast.v index 085080b..e1bdd3b 100644 --- a/compiler/ast.v +++ b/compiler/ast.v @@ -3,6 +3,7 @@ module compiler pub enum ExprKind { int_lit + float_lit str_lit bool_lit ident @@ -11,7 +12,9 @@ pub enum ExprKind { index field method_call + slice unary + anon_fn binary call } @@ -25,22 +28,31 @@ pub mut: pub struct Expr { pub mut: - kind ExprKind - int_v i64 - str_v string + kind ExprKind + int_v i64 + float_v f64 + str_v string name string // ident/call name, or the field name of a `.field` access op TokKind left &Expr = unsafe { nil } right &Expr = unsafe { nil } + extra &Expr = unsafe { nil } // slice: end index expression elems []Expr fields []StructField // struct_lit: the named fields - args []Expr - line int + args []Expr + type_args []string // call: explicit generic type arguments (first[int](...)) + fparams []string // anon_fn: parameter names + fdefaults []Expr // anon_fn: default values (parallel to fparams) + fhas_defs []bool // anon_fn: which params have defaults + fvariadic bool // anon_fn: last param is variadic + fn_body []Stmt // anon_fn: function body + line int } pub enum StmtKind { expr_stmt let_stmt + destruct_stmt assign_stmt index_assign field_assign @@ -53,6 +65,8 @@ pub enum StmtKind { continue_stmt ret_stmt assert_stmt + try_stmt + throw_stmt } // MatchArm is a single `value { body }` arm of a match statement. @@ -66,6 +80,9 @@ pub struct Stmt { pub mut: kind StmtKind target string + idx_target string // for_in_stmt: index variable name (empty when unused) + destruct_targets []string // destruct_stmt: names to bind + destruct_field bool // destruct_stmt: struct ({ a, b }) vs array ([a, b]) expr Expr cond Expr base Expr // index_assign: the indexed expression @@ -90,10 +107,14 @@ pub mut: pub struct FnDecl { pub mut: - name string - recv_name string // method receiver local name ('' for plain functions) - recv_type string // method receiver struct type ('' for plain functions) + name string + type_params []string // generic type parameters (fn first[T, U](...) { ... }) + recv_name string // method receiver local name ('' for plain functions) + recv_type string // method receiver struct type ('' for plain functions) params []string + defaults []Expr // parallel to params; empty Expr{} when no default + has_defs []bool // parallel to params: whether a default exists + variadic bool // last param is variadic (nums...) body []Stmt line int } @@ -120,11 +141,28 @@ pub mut: line int } +// InterfaceDecl is an `interface Name { method1(); method2() type }` declaration. +// Methods are stored as (name, return_type) pairs. The interface is satisfied +// by any struct that implements all listed methods (structural/duck typing). +pub struct InterfaceDecl { +pub mut: + name string + methods []InterfaceMethod + line int +} + +pub struct InterfaceMethod { +pub mut: + name string + line int +} + pub struct Program { pub mut: - fns []FnDecl - structs []StructDecl - enums []EnumDecl - imports []ImportDecl - consts []ConstDecl + fns []FnDecl + structs []StructDecl + enums []EnumDecl + imports []ImportDecl + consts []ConstDecl + interfaces []InterfaceDecl } diff --git a/compiler/check.v b/compiler/check.v new file mode 100644 index 0000000..6d22f5d --- /dev/null +++ b/compiler/check.v @@ -0,0 +1,599 @@ +// check.v — a compile-time type checker for VuurRaaf. +// +// Runs after parsing and before codegen. The language is dynamically typed at +// runtime, so this pass is deliberately conservative: it rejects programs that +// are *provably* wrong (unknown variables, field access on numbers, arithmetic +// on strings, wrong arity on known functions) while leaving genuinely dynamic +// programs (untyped parameters, unknown receiver types, mixed containers, and +// calls to functions defined in other objects) alone. Unresolved function +// names are deferred to the linker, matching the toolchain's separate +// compilation model. +module compiler + +import os + +enum CType { + unknown + int_t + float_t + string_t + bool_t + array_t + struct_t + enum_t + closure_t +} + +struct TypeInfo { + kind CType + name string // struct/enum type name when statically known +} + +struct FnSig { + min_args int + has_defs []bool + variadic bool + n_type_params int // generic type parameters declared on the function + returns TypeInfo +} + +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 + loop_depth int + checked map[string]bool // imported files already checked +} + +// check validates a parsed program and returns an error on the first problem. +fn check(prog Program) ! { + mut c := Checker{} + // register declarations + for sd in prog.structs { + if sd.name in c.structs { + return error('duplicate struct declaration "${sd.name}" (line ${sd.line})') + } + c.structs[sd.name] = sd.fields + } + for ed in prog.enums { + if ed.name in c.enums { + return error('duplicate enum declaration "${ed.name}" (line ${ed.line})') + } + c.enums[ed.name] = ed.variants + } + for cd in prog.consts { + c.consts[cd.name] = TypeInfo{ kind: .int_t } + } + for fd in prog.fns { + if fd.name in c.fns { + return error('duplicate function "${fd.name}" (line ${fd.line})') + } + c.fns[fd.name] = FnSig{ min_args: fd.params.len - def_count(fd), has_defs: fd.has_defs, variadic: fd.variadic, n_type_params: fd.type_params.len } + } + // imported files are checked (and their symbols merged) recursively + for imp in prog.imports { + c.check_import(imp.path)! + } + for fd in prog.fns { + c.check_fn(fd)! + } +} + +fn def_count(fd FnDecl) int { + mut n := 0 + for has in fd.has_defs { + if has { + n++ + } + } + return n +} + +fn (mut c Checker) check_import(path string) ! { + if path in c.checked { + return + } + c.checked[path] = true + resolved := resolve_import(path) or { return error('cannot read import "${path}"') } + src := os.read_file(resolved) or { return error('cannot read import "${path}"') } + prog := parse(tokenize(src)!)! + // merge declarations from the import + for sd in prog.structs { + if sd.name !in c.structs { + c.structs[sd.name] = sd.fields + } + } + for ed in prog.enums { + if ed.name !in c.enums { + c.enums[ed.name] = ed.variants + } + } + for cd in prog.consts { + if cd.name !in c.consts { + c.consts[cd.name] = TypeInfo{ kind: .int_t } + } + } + for fd in prog.fns { + if fd.name !in c.fns { + c.fns[fd.name] = FnSig{ min_args: fd.params.len - def_count(fd), has_defs: fd.has_defs, variadic: fd.variadic, n_type_params: fd.type_params.len } + } + } + for imp in prog.imports { + c.check_import(imp.path)! + } + for fd in prog.fns { + c.check_fn(fd)! + } +} + +fn (mut c Checker) check_fn(fd FnDecl) ! { + c.types.clear() + c.loop_depth = 0 + // 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 } + } + for p in fd.params { + c.types[p] = TypeInfo{ kind: .unknown } + } + for st in fd.body { + c.check_stmt(st)! + } +} + +fn (mut c Checker) check_stmt(st Stmt) ! { + match st.kind { + .expr_stmt { + _ = c.check_expr(st.expr)! + } + .let_stmt { + t := c.check_expr(st.expr)! + c.types[st.target] = t + } + .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 } } + } + _ = base + } + .assign_stmt { + if st.target !in c.types { + return error('unknown variable "${st.target}" (line ${st.line})') + } + _ = c.check_expr(st.expr)! + } + .index_assign { + base := c.check_expr(st.base)! + _ = c.check_expr(st.idx)! + c.expect_container(base, 'index assignment', st.line)! + _ = c.check_expr(st.expr)! + } + .field_assign { + base := c.check_expr(st.base)! + c.expect_struct_like(base, 'field assignment', st.line)! + _ = c.check_expr(st.expr)! + } + .if_stmt { + _ = c.check_expr(st.cond)! + for s in st.body { + c.check_stmt(s)! + } + for s in st.els { + c.check_stmt(s)! + } + } + .match_stmt { + _ = c.check_expr(st.expr)! + for arm in st.arms { + _ = c.check_expr(arm.val)! + for s in arm.body { + c.check_stmt(s)! + } + } + for s in st.els_body { + c.check_stmt(s)! + } + } + .while_stmt { + _ = c.check_expr(st.cond)! + c.loop_depth++ + for s in st.body { + c.check_stmt(s)! + } + c.loop_depth-- + } + .for_range_stmt { + start_t := c.check_expr(st.expr)! + end_t := c.check_expr(st.cond)! + 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.loop_depth++ + for s in st.body { + c.check_stmt(s)! + } + c.loop_depth-- + } + .for_in_stmt { + seq := c.check_expr(st.expr)! + // iterate enums and arrays; unknown is allowed (dynamic) + if seq.kind == .int_t || seq.kind == .float_t || seq.kind == .bool_t { + return error('cannot iterate a ${type_name(seq.kind)} (line ${st.line})') + } + c.types[st.target] = TypeInfo{ kind: .unknown } + if st.idx_target.len > 0 { + c.types[st.idx_target] = TypeInfo{ kind: .int_t } + } + c.loop_depth++ + for s in st.body { + c.check_stmt(s)! + } + c.loop_depth-- + } + .break_stmt, .continue_stmt { + if c.loop_depth == 0 { + what := if st.kind == .break_stmt { 'break' } else { 'continue' } + return error('${what} outside of a loop (line ${st.line})') + } + } + .ret_stmt { + if st.has_val { + _ = c.check_expr(st.expr)! + } + } + .assert_stmt { + _ = c.check_expr(st.expr)! + } + .try_stmt { + c.loop_depth++ // errors unwind through loops; keep depth permissive + c.loop_depth-- + for s in st.body { + c.check_stmt(s)! + } + c.types[st.target] = TypeInfo{ kind: .string_t } + for s in st.els { + c.check_stmt(s)! + } + } + .throw_stmt { + _ = c.check_expr(st.expr)! + } + } +} + +fn (mut c Checker) check_expr(e Expr) !TypeInfo { + return match e.kind { + .int_lit { TypeInfo{ kind: .int_t } } + .float_lit { TypeInfo{ kind: .float_t } } + .str_lit { TypeInfo{ kind: .string_t } } + .bool_lit { TypeInfo{ kind: .bool_t } } + .ident { + if e.name in c.types { + c.types[e.name] + } else if e.name in c.consts { + c.consts[e.name] + } else { + return error('unknown variable "${e.name}" (line ${e.line})') + } + } + .array_lit { + for el in e.elems { + _ = c.check_expr(el)! + } + TypeInfo{ kind: .array_t } + } + .struct_lit { + if e.name.len > 0 && e.name in c.structs { + fields := c.structs[e.name] + mut seen := map[string]bool{} + for f in e.fields { + if f.name !in fields { + return error('unknown field "${f.name}" for struct ${e.name} (line ${e.line})') + } + if f.name in seen { + return error('duplicate field "${f.name}" in struct literal (line ${e.line})') + } + seen[f.name] = true + _ = c.check_expr(f.val)! + } + return TypeInfo{ kind: .struct_t, name: e.name } + } + for f in e.fields { + _ = c.check_expr(f.val)! + } + TypeInfo{ kind: .struct_t } + } + .index { + base := c.check_expr(*e.left)! + _ = c.check_expr(*e.right)! + c.expect_container(base, 'indexing', e.line)! + TypeInfo{ kind: .unknown } + } + .field { + base := c.check_expr(*e.left)! + c.expect_struct_like(base, 'field access', e.line)! + // enum variant: Color.red → enum_t + if base.kind == .enum_t { + return TypeInfo{ kind: .enum_t, name: base.name } + } + TypeInfo{ kind: .unknown } + } + .method_call { + recv := c.check_expr(*e.left)! + if recv.kind == .int_t || recv.kind == .float_t || recv.kind == .bool_t { + return error('cannot call a method on a ${type_name(recv.kind)} (line ${e.line})') + } + for a in e.args { + _ = c.check_expr(a)! + } + TypeInfo{ kind: .unknown } + } + .slice { + base := c.check_expr(*e.left)! + _ = c.check_expr(*e.right)! + _ = c.check_expr(*e.extra)! + if base.kind == .int_t || base.kind == .float_t || base.kind == .bool_t { + return error('cannot slice a ${type_name(base.kind)} (line ${e.line})') + } + if base.kind == .string_t { + TypeInfo{ kind: .string_t } + } else { + TypeInfo{ kind: .unknown } + } + } + .unary { + op := c.check_expr(*e.right)! + match e.op { + .kw_not { TypeInfo{ kind: .bool_t } } + .tilde { + c.expect_int(op, 'bitwise not', e.line)! + TypeInfo{ kind: .int_t } + } + else { + c.expect_numeric(op, 'unary minus', e.line)! + op + } + } + } + .binary { + c.check_binary(e)! + } + .call { + c.check_call(e)! + } + .anon_fn { + for p in e.fparams { + c.types[p] = TypeInfo{ kind: .unknown } + } + for s in e.fn_body { + c.check_stmt(s)! + } + TypeInfo{ kind: .closure_t } + } + } +} + +fn (mut c Checker) check_binary(e Expr) !TypeInfo { + l := c.check_expr(*e.left)! + r := c.check_expr(*e.right)! + return match e.op { + .plus { + if l.kind == .string_t || r.kind == .string_t { + return TypeInfo{ kind: .string_t } + } + if l.kind == .array_t || r.kind == .array_t { + return error('cannot add arrays with + (line ${e.line})') + } + if l.kind == .struct_t || r.kind == .struct_t { + return error('cannot add structs with + (line ${e.line})') + } + if l.kind == .bool_t || r.kind == .bool_t { + return error('cannot add a bool with + (line ${e.line})') + } + if l.kind == .unknown || r.kind == .unknown { + return TypeInfo{ kind: .unknown } + } + if l.kind == .float_t || r.kind == .float_t { + return TypeInfo{ kind: .float_t } + } + return TypeInfo{ kind: .int_t } + } + .minus, .star, .slash, .percent { + c.expect_numeric(l, 'arithmetic', e.line)! + c.expect_numeric(r, 'arithmetic', e.line)! + if l.kind == .float_t || r.kind == .float_t { + TypeInfo{ kind: .float_t } + } else { + TypeInfo{ kind: .int_t } + } + } + .eq_eq, .not_eq { + TypeInfo{ kind: .bool_t } + } + .lt, .le, .gt, .ge { + if l.kind == .array_t || r.kind == .array_t { + return error('cannot order arrays (line ${e.line})') + } + if l.kind == .struct_t || r.kind == .struct_t { + return error('cannot order structs (line ${e.line})') + } + if l.kind == .bool_t && r.kind == .bool_t { + return error('cannot order booleans (line ${e.line})') + } + if l.kind != .unknown && r.kind != .unknown && l.kind != r.kind && !(is_num_kind(l.kind) && is_num_kind(r.kind)) { + return error('cannot compare a ${type_name(l.kind)} and a ${type_name(r.kind)} (line ${e.line})') + } + TypeInfo{ kind: .bool_t } + } + .kw_and, .kw_or { + TypeInfo{ kind: .bool_t } + } + .amp, .pipe, .caret, .lt_lt, .gt_gt { + c.expect_int(l, 'bitwise operator', e.line)! + c.expect_int(r, 'bitwise operator', e.line)! + TypeInfo{ kind: .int_t } + } + else { + return error('unsupported operator at line ${e.line}') + } + } +} + +fn (mut c Checker) check_call(e Expr) !TypeInfo { + // builtin calls + if e.name == 'len' { + if e.args.len != 1 { + return error('len() takes exactly one argument (line ${e.line})') + } + t := c.check_expr(e.args[0])! + if t.kind == .int_t || t.kind == .float_t || t.kind == .bool_t || t.kind == .closure_t { + return error('len() on a ${type_name(t.kind)} (line ${e.line})') + } + return TypeInfo{ kind: .int_t } + } + if e.name == 'push' || e.name == 'insert' || e.name == 'remove' { + if e.args.len == 0 { + return error('${e.name}() expects arguments (line ${e.line})') + } + seq := c.check_expr(e.args[0])! + c.expect_container(seq, '${e.name}()', e.line)! + for a in e.args[1..] { + _ = c.check_expr(a)! + } + return TypeInfo{ kind: .unknown } + } + if e.name == 'has' || e.name == 'delete' { + if e.args.len != 2 { + return error('${e.name}() takes exactly two arguments (line ${e.line})') + } + seq := c.check_expr(e.args[0])! + c.expect_struct_like(seq, '${e.name}()', e.line)! + _ = c.check_expr(e.args[1])! + return TypeInfo{ kind: .unknown } + } + if e.name == 'keys' { + if e.args.len != 1 { + return error('keys() takes exactly one argument (line ${e.line})') + } + seq := c.check_expr(e.args[0])! + c.expect_struct_like(seq, 'keys()', e.line)! + return TypeInfo{ kind: .array_t } + } + if e.name == 'print' || e.name == 'println' { + if e.args.len != 1 { + return error('${e.name}() takes exactly one argument (line ${e.line})') + } + _ = c.check_expr(e.args[0])! + return TypeInfo{ kind: .unknown } + } + // host builtins (native) — validate arity from the spec table + bid, bargc := builtin_spec(e.name) + if bid >= 0 { + if e.args.len != bargc { + return error('${e.name}() takes exactly ${bargc} argument(s) (line ${e.line})') + } + for a in e.args { + _ = c.check_expr(a)! + } + return builtin_result_type(e.name) + } + // closure call: a local holding a function value + if e.name in c.types && c.types[e.name].kind == .closure_t { + for a in e.args { + _ = c.check_expr(a)! + } + return TypeInfo{ kind: .unknown } + } + // user function — arity is checked only when the signature is known. + // Unknown names are allowed: this toolchain supports separate compilation, + // so a call may resolve to a function in another object at link time. + // Truly missing functions are reported by the linker, not the checker. + if e.name in c.fns { + sig := c.fns[e.name] + // generic type arguments must match the declared type parameters + if e.type_args.len > 0 && e.type_args.len != sig.n_type_params { + return error('${e.name}() takes ${sig.n_type_params} type argument(s), got ${e.type_args.len} (line ${e.line})') + } + if e.type_args.len == 0 && sig.n_type_params > 0 { + // calling a generic function without explicit type args is fine — + // the VM infers from the values at runtime + } + if !sig.variadic { + if e.args.len < sig.min_args { + return error('${e.name}() expects at least ${sig.min_args} argument(s), got ${e.args.len} (line ${e.line})') + } + if e.args.len > sig.has_defs.len { + return error('${e.name}() expects at most ${sig.has_defs.len} argument(s), got ${e.args.len} (line ${e.line})') + } + } + } + for a in e.args { + _ = c.check_expr(a)! + } + return TypeInfo{ kind: .unknown } +} + +fn builtin_result_type(name string) TypeInfo { + return match name { + 'abs', 'min', 'max', 'floor', 'ceil', 'round', 'rand_int' { TypeInfo{ kind: .int_t } } + 'pow', 'sqrt', 'rand', 'float', 'time' { TypeInfo{ kind: .float_t } } + 'int' { TypeInfo{ kind: .int_t } } + 'str', 'type', 'lower', 'upper', 'trim', 'getenv', 'read_file' { TypeInfo{ kind: .string_t } } + 'contains', 'starts_with', 'ends_with' { TypeInfo{ kind: .bool_t } } + 'split' { TypeInfo{ kind: .array_t } } + 'join' { TypeInfo{ kind: .string_t } } + 'sort', 'reverse' { TypeInfo{ kind: .array_t } } + 'pop' { TypeInfo{ kind: .unknown } } + 'clone', 'index_of' { TypeInfo{ kind: .unknown } } + 'args', 'keys' { TypeInfo{ kind: .array_t } } + 'len' { TypeInfo{ kind: .int_t } } + 'write_file', 'setenv', 'exit', 'sleep', 'eprint' { TypeInfo{ kind: .unknown } } + else { TypeInfo{ kind: .unknown } } + } +} + +fn is_num_kind(k CType) bool { + return k == .int_t || k == .float_t +} + +fn type_name(k CType) string { + return match k { + .int_t { 'int' } + .float_t { 'float' } + .string_t { 'string' } + .bool_t { 'bool' } + .array_t { 'array' } + .struct_t { 'struct' } + .enum_t { 'enum' } + .closure_t { 'function' } + else { 'value' } + } +} + +fn (mut c Checker) expect_numeric(t TypeInfo, what string, line int) ! { + if t.kind == .string_t || t.kind == .array_t || t.kind == .struct_t || t.kind == .bool_t || t.kind == .closure_t { + return error('${what} on a ${type_name(t.kind)} (line ${line})') + } +} + +fn (mut c Checker) expect_int(t TypeInfo, what string, line int) ! { + if t.kind == .string_t || t.kind == .array_t || t.kind == .struct_t || t.kind == .bool_t || t.kind == .closure_t || t.kind == .float_t { + return error('${what} requires an int, got a ${type_name(t.kind)} (line ${line})') + } +} + +fn (mut c Checker) expect_container(t TypeInfo, what string, line int) ! { + if t.kind == .int_t || t.kind == .float_t || t.kind == .bool_t || t.kind == .closure_t { + return error('${what} on a ${type_name(t.kind)} (line ${line})') + } +} + +fn (mut c Checker) expect_struct_like(t TypeInfo, what string, line int) ! { + if t.kind == .int_t || t.kind == .float_t || t.kind == .bool_t || t.kind == .string_t || t.kind == .closure_t { + return error('${what} on a ${type_name(t.kind)} (line ${line})') + } +} diff --git a/compiler/codegen.v b/compiler/codegen.v index 6f33786..4653267 100644 --- a/compiler/codegen.v +++ b/compiler/codegen.v @@ -31,8 +31,10 @@ mut: types map[string]string // local name -> declared struct type ('' = unknown) structs map[string][]string // declared struct name -> field list enums map[string][]string // enum name -> variant list + lam_counter int // anonymous function counter enum_vals map[string]int // 'Enum.variant' -> integer value consts map[string]i64 // constant name -> integer value + lines []obj.LineInfo // code offset -> source line (debug info) local_cnt int argc int cur_fn string @@ -78,7 +80,7 @@ fn gen(prog Program) !obj.Obj { } // compile imported files and merge their objects for imp in prog.imports { - imported := compile_file(imp.path)! + imported := compile_file(resolve_import(imp.path)!)! // merge symbols from the imported object for s in imported.symbols { g.symbols << s @@ -93,6 +95,10 @@ fn gen(prog Program) !obj.Obj { for r in imported.relocs { g.relocs << obj.Reloc{ offset: u32(code_off) + r.offset, name: r.name, kind: r.kind } } + // 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 } + } } for fd in prog.fns { g.gen_fn(fd)! @@ -102,6 +108,7 @@ fn gen(prog Program) !obj.Obj { strings: g.strings code: g.code relocs: g.relocs + lines: g.lines } } @@ -111,6 +118,7 @@ fn (mut g Gen) gen_fn(fd FnDecl) ! { sym := if fd.recv_type.len > 0 { '${fd.recv_type}.${fd.name}' } else { fd.name } g.cur_fn = sym g.symbols << obj.Symbol{ name: sym, entry: g.code.len } + g.lines << obj.LineInfo{ off: u32(g.code.len), line: fd.line } g.locals.clear() g.types.clear() g.local_cnt = 0 @@ -121,20 +129,67 @@ fn (mut g Gen) gen_fn(fd FnDecl) ! { g.types[fd.recv_name] = fd.recv_type next = 1 } + // a variadic parameter does not occupy an argument slot; it gets a fresh + // local that the prologue fills with the collected vararg array + if fd.variadic { + g.argc-- + } for i, p in fd.params { + if fd.variadic && i == fd.params.len - 1 { + continue + } g.locals[p] = i + next } g.local_cnt = g.argc + if fd.variadic { + vidx := g.local_cnt + g.local_cnt++ + g.locals[fd.params[fd.params.len - 1]] = vidx + } // `enter n` reserves the non-parameter locals; n is patched once the body // has been scanned. g.code << op_enter g.enter_off = u32(g.code.len) g.code << obj.encode_i64(0) + // default parameter values: if the caller passed fewer args than this + // param's slot, evaluate the default and store it + for i, p in fd.params { + if fd.variadic && i == fd.params.len - 1 { + continue + } + if i >= fd.has_defs.len || !fd.has_defs[i] { + continue + } + slot := i + next + skip_l := g.new_label() + g.code << op_argc + g.code << op_push_i + g.code << obj.encode_i64(i64(slot)) + g.code << op_le + g.code << op_jz + g.code << obj.encode_i64(0) + g.fixups << Fixup{ name: skip_l, off: u32(g.code.len) - 8 } + g.gen_expr(fd.defaults[i])! + g.emit_store(slot) + g.emit_label(skip_l) + } + // variadic collection: build an array from args[argc..actual-1] + if fd.variadic { + vidx := g.locals[fd.params[fd.params.len - 1]] or { + return error('internal: variadic param missing') + } + g.code << op_varargs + g.code << obj.encode_i64(i64(g.argc)) + g.code << obj.encode_i64(i64(vidx)) + } for st in fd.body { g.gen_stmt(st)! } g.code << op_ret // trailing return for fall-through - obj.patch_i64(mut g.code, g.enter_off, i64(g.local_cnt - g.argc)) + // reserve all local slots: the callee may be called with fewer arguments + // than declared (default parameters) or more (variadic), so the frame must + // always cover slots 0..local_cnt-1 + obj.patch_i64(mut g.code, g.enter_off, i64(g.local_cnt)) // resolve intra-function jump targets for f in g.fixups { target := g.labels[f.name] or { @@ -148,6 +203,7 @@ fn (mut g Gen) gen_fn(fd FnDecl) ! { } fn (mut g Gen) gen_stmt(st Stmt) ! { + g.lines << obj.LineInfo{ off: u32(g.code.len), line: st.line } match st.kind { .expr_stmt { g.gen_expr(st.expr)! @@ -168,6 +224,28 @@ fn (mut g Gen) gen_stmt(st Stmt) ! { g.code << op_store g.code << obj.encode_i64(i64(idx)) } + .destruct_stmt { + // let { a, b } = e → tmp := e; a := tmp.a; b := tmp.b + // let [a, b] = e → tmp := e; a := tmp[0]; b := tmp[1] + tmp_idx := g.new_local() + g.gen_expr(st.expr)! + g.emit_store(tmp_idx) + for i, name in st.destruct_targets { + g.emit_load(tmp_idx) + if st.destruct_field { + g.emit_field_name(name) + g.code << op_sget + } else { + g.code << op_push_i + g.code << obj.encode_i64(i64(i)) + g.code << op_aget + } + idx := g.new_local() + g.locals[name] = idx + g.types.delete(name) + g.emit_store(idx) + } + } .assign_stmt { idx := g.locals[st.target] or { return error('unknown variable "${st.target}" at line ${st.line}') @@ -350,9 +428,26 @@ fn (mut g Gen) gen_stmt(st Stmt) ! { prev_t := g.types[st.target] or { '' } g.locals[st.target] = elem_idx g.types.delete(st.target) + // bind the index variable if present (for i, v in arr) + prev_idx := if st.idx_target.len > 0 { g.locals[st.idx_target] or { -1 } } else { -1 } + prev_idx_t := if st.idx_target.len > 0 { g.types[st.idx_target] or { '' } } else { '' } + if st.idx_target.len > 0 { + g.locals[st.idx_target] = idx_idx + g.types.delete(st.idx_target) + } for s in st.body { g.gen_stmt(s)! } + if st.idx_target.len > 0 { + if prev_idx >= 0 { + g.locals[st.idx_target] = prev_idx + } else { + g.locals.delete(st.idx_target) + } + if prev_idx_t.len > 0 { + g.types[st.idx_target] = prev_idx_t + } + } if prev >= 0 { g.locals[st.target] = prev } else { @@ -403,6 +498,44 @@ fn (mut g Gen) gen_stmt(st Stmt) ! { g.code << obj.encode_i64(0) g.fixups << Fixup{ name: ctx.continue_l, off: u32(g.code.len) - 8 } } + .throw_stmt { + g.gen_expr(st.expr)! + g.code << op_throw + } + .try_stmt { + catch_l := g.new_label() + end_l := g.new_label() + err_idx := g.new_local() + g.code << op_try + g.code << obj.encode_i64(0) + g.fixups << Fixup{ name: catch_l, off: u32(g.code.len) - 8 } + for s in st.body { + g.gen_stmt(s)! + } + g.code << op_catch_done + g.code << op_jmp + g.code << obj.encode_i64(0) + g.fixups << Fixup{ name: end_l, off: u32(g.code.len) - 8 } + g.emit_label(catch_l) + g.code << op_store + g.code << obj.encode_i64(i64(err_idx)) + prev := g.locals[st.target] or { -1 } + prev_t := g.types[st.target] or { '' } + g.locals[st.target] = err_idx + g.types.delete(st.target) + for s in st.els { + g.gen_stmt(s)! + } + if prev >= 0 { + g.locals[st.target] = prev + } else { + g.locals.delete(st.target) + } + if prev_t.len > 0 { + g.types[st.target] = prev_t + } + g.emit_label(end_l) + } } } @@ -412,6 +545,10 @@ fn (mut g Gen) gen_expr(e Expr) ! { g.code << op_push_i g.code << obj.encode_i64(e.int_v) } + .float_lit { + g.code << op_push_f + g.code << obj.encode_f64(e.float_v) + } .str_lit { // the index is a placeholder; the linker rebases it via a string // relocation so multi-file links keep working @@ -468,7 +605,21 @@ fn (mut g Gen) gen_expr(e Expr) ! { } .method_call { // p.dist(x) → call .dist p, x - recv_t := g.method_receiver_type(e)! + recv_t := g.method_receiver_type(e) + // string methods: s.len(), s.to_upper(), s.contains(x), ... — + // the receiver type is known when it is a literal or a local that + // was assigned a string literal + if recv_t == 'string' || e.left.kind == .str_lit { + g.gen_expr(*e.left)! + for a in e.args { + g.gen_expr(a)! + } + g.code << op_str_method + g.code << obj.encode_i64(0) // name placeholder — rebased by the linker + g.relocs << obj.Reloc{ offset: u32(g.code.len) - 8, name: e.name, kind: 1 } + g.code << obj.encode_i64(i64(e.args.len)) + return + } // built-in: enum.to_string() generates a match on the integer value if e.name == 'to_string' && recv_t in g.enums && e.args.len == 0 { g.gen_enum_to_string(recv_t, *e.left, e.line)! @@ -487,11 +638,24 @@ fn (mut g Gen) gen_expr(e Expr) ! { for a in e.args { g.gen_expr(a)! } - g.code << op_call - g.code << obj.encode_i64(0) // placeholder — patched by the linker - g.relocs << obj.Reloc{ offset: u32(g.code.len) - 8, name: '${recv_t}.${e.name}', kind: 0 } - g.code << obj.encode_i64(i64(e.args.len + 1)) // receiver + args + // if receiver type is known, emit a static method call + if recv_t.len > 0 { + g.code << op_call + g.code << obj.encode_i64(0) // placeholder — patched by the linker + g.relocs << obj.Reloc{ offset: u32(g.code.len) - 8, name: '${recv_t}.${e.name}', kind: 0 } + g.code << obj.encode_i64(i64(e.args.len + 1)) // receiver + args + } else { + // unknown type: treat as closure call on a struct field + g.emit_field_name(e.name) + g.code << op_sget + for a in e.args { + g.gen_expr(a)! + } + g.code << op_call_closure + g.code << obj.encode_i64(i64(e.args.len)) } + return + } .index { // if the index is a string literal, use struct field access (map style) if e.right.kind == .str_lit { @@ -504,6 +668,57 @@ fn (mut g Gen) gen_expr(e Expr) ! { g.code << op_aget } } + .slice { + // arr[start..end] → push value, start, end; slice + g.gen_expr(*e.left)! + g.gen_expr(*e.right)! + g.gen_expr(*e.extra)! + g.code << op_slice + } + .anon_fn { + g.lam_counter++ + name := '__lam_${g.lam_counter}' + // jump over the lambda body so callers don't fall through + g.code << op_jmp + g.code << obj.encode_i64(0) + skip_fix_off := u32(g.code.len) - 8 + fd := FnDecl{ + name: name + params: e.fparams + defaults: e.fdefaults + has_defs: e.fhas_defs + variadic: e.fvariadic + body: e.fn_body + line: e.line + } + // Save enclosing fixup/label/locals/type state; gen_fn clears them. + // enter_off and argc are also per-function, so they must be restored + // or the enclosing function's `enter n` patch is lost (locals would + // then collide with the stack top). + saved_fixups := g.fixups.clone() + saved_labels := g.labels.clone() + saved_locals := g.locals.clone() + saved_types := g.types.clone() + saved_local_cnt := g.local_cnt + saved_enter_off := g.enter_off + saved_argc := g.argc + g.labels.clear() + g.fixups = []Fixup{} + g.gen_fn(fd)! + // Restore the enclosing state. + g.fixups = saved_fixups + g.labels = saved_labels.clone() + g.locals = saved_locals.clone() + g.types = saved_types.clone() + g.local_cnt = saved_local_cnt + g.enter_off = saved_enter_off + g.argc = saved_argc + // Patch the skip jump to land at the closure opcode we emit next. + obj.patch_i64(mut g.code, skip_fix_off, i64(g.code.len)) + g.code << op_closure + g.code << obj.encode_i64(0) + g.relocs << obj.Reloc{ offset: u32(g.code.len) - 8, name: name, kind: 0 } + } .bool_lit { g.code << op_push_i g.code << obj.encode_i64(e.int_v) @@ -526,11 +741,29 @@ fn (mut g Gen) gen_expr(e Expr) ! { } } .unary { + // constant-fold unary ops on literals: -5, -2.5, not true, ~7 + if e.right.kind == .int_lit && (e.op == .minus || e.op == .tilde) { + v := e.right.int_v + res := if e.op == .minus { -v } else { ~v } + g.code << op_push_i + g.code << obj.encode_i64(res) + return + } + if e.right.kind == .float_lit && e.op == .minus { + g.code << op_push_f + g.code << obj.encode_f64(-e.right.float_v) + return + } + if e.right.kind == .bool_lit && e.op == .kw_not { + g.code << op_push_i + g.code << obj.encode_i64(if e.right.int_v == 0 { 1 } else { 0 }) + return + } g.gen_expr(*e.right)! - if e.op == .kw_not { - g.code << op_not - } else { - g.code << op_neg + match e.op { + .kw_not { g.code << op_not } + .tilde { g.code << op_not_b } + else { g.code << op_neg } } } .binary { @@ -594,6 +827,31 @@ fn (mut g Gen) gen_call(e Expr) ! { g.code << op_skeys return } + // closure call: ident(args) where ident is a local holding a closure + if e.name in g.locals { + g.gen_expr(Expr{ kind: .ident, name: e.name, line: e.line })! + g.code << op_dup // separate the closure copy from the local slot + for a in e.args { + g.gen_expr(a)! + } + g.code << op_call_closure + g.code << obj.encode_i64(i64(e.args.len)) + return + } + // host builtins (file I/O, OS, math, collections) go through op_native + bid, bargc := builtin_spec(e.name) + if bid >= 0 { + if e.args.len != bargc { + return error('${e.name}() takes exactly ${bargc} argument(s) (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(bargc)) + return + } for a in e.args { g.gen_expr(a)! } @@ -603,7 +861,169 @@ fn (mut g Gen) gen_call(e Expr) ! { g.code << obj.encode_i64(i64(e.args.len)) // argc } +// builtin_spec maps a builtin function name to its (native id, arg count). +// A negative id means the name is not a builtin (it is a user function). +fn builtin_spec(name string) (int, int) { + return match name { + 'abs' { native_abs, 1 } + 'min' { native_min, 2 } + 'max' { native_max, 2 } + 'pow' { native_pow, 2 } + 'sqrt' { native_sqrt, 1 } + 'floor' { native_floor, 1 } + 'ceil' { native_ceil, 1 } + 'round' { native_round, 1 } + 'rand' { native_rand, 0 } + 'rand_int' { native_rand_int, 1 } + 'int' { native_int, 1 } + 'str' { native_str, 1 } + 'float' { native_float, 1 } + 'type' { native_type, 1 } + 'split' { native_split, 2 } + 'join' { native_join, 2 } + 'contains' { native_contains, 2 } + 'starts_with' { native_starts_with, 2 } + 'ends_with' { native_ends_with, 2 } + 'trim' { native_trim, 1 } + 'lower' { native_lower, 1 } + 'upper' { native_upper, 1 } + 'pop' { native_pop, 1 } + 'insert' { native_insert, 3 } + 'remove' { native_remove, 2 } + 'sort' { native_sort, 1 } + 'clone' { native_clone, 1 } + 'reverse' { native_reverse, 1 } + 'index_of' { native_index_of, 2 } + 'args' { native_args, 0 } + 'getenv' { native_getenv, 1 } + 'setenv' { native_setenv, 2 } + 'exit' { native_exit, 1 } + 'time' { native_time, 0 } + 'sleep' { native_sleep, 1 } + 'read_file' { native_read_file, 1 } + 'write_file' { native_write_file, 2 } + 'eprint' { native_eprint, 1 } + else { -1, 0 } + } +} + +// fold_binary constant-folds binary expressions whose operands are both +// literals, emitting the precomputed constant. Returns false when the +// expression cannot be folded (leaving it to the runtime). Division/modulo by +// zero and out-of-range shifts are deliberately not folded so the runtime +// still reports them. +fn (mut g Gen) fold_binary(e Expr) bool { + // integer folding + if e.left.kind == .int_lit && e.right.kind == .int_lit { + l := e.left.int_v + r := e.right.int_v + mut res := i64(0) + match e.op { + .plus { res = l + r } + .minus { res = l - r } + .star { res = l * r } + .slash { + if r == 0 { + return false + } + res = l / r + } + .percent { + if r == 0 { + return false + } + res = l % r + } + .amp { res = l & r } + .pipe { res = l | r } + .caret { res = l ^ r } + .lt_lt { + if r < 0 || r > 63 { + return false + } + res = l << u32(r) + } + .gt_gt { + if r < 0 || r > 63 { + return false + } + res = l >> u32(r) + } + .eq_eq { res = if l == r { 1 } else { 0 } } + .not_eq { res = if l != r { 1 } else { 0 } } + .lt { res = if l < r { 1 } else { 0 } } + .le { res = if l <= r { 1 } else { 0 } } + .gt { res = if l > r { 1 } else { 0 } } + .ge { res = if l >= r { 1 } else { 0 } } + else { return false } + } + g.code << op_push_i + g.code << obj.encode_i64(res) + return true + } + // float folding + if e.left.kind == .float_lit && e.right.kind == .float_lit { + l := e.left.float_v + r := e.right.float_v + mut res := 0.0 + mut is_bool := false + mut bres := false + match e.op { + .plus { res = l + r } + .minus { res = l - r } + .star { res = l * r } + .slash { + if r == 0.0 { + return false + } + res = l / r + } + .eq_eq { is_bool = true; bres = l == r } + .not_eq { is_bool = true; bres = l != r } + .lt { is_bool = true; bres = l < r } + .le { is_bool = true; bres = l <= r } + .gt { is_bool = true; bres = l > r } + .ge { is_bool = true; bres = l >= r } + else { return false } + } + if is_bool { + g.code << op_push_i + g.code << obj.encode_i64(if bres { 1 } else { 0 }) + } else { + g.code << op_push_f + g.code << obj.encode_f64(res) + } + return true + } + // string concatenation folding: "a" + "b" → one interned constant. + // The string is emitted as a relocation so the linker interns it in the + // final table, exactly like a plain string literal. + if e.left.kind == .str_lit && e.right.kind == .str_lit && e.op == .plus { + g.code << op_push_s + g.code << obj.encode_i64(0) // placeholder — rebased by the linker + g.relocs << obj.Reloc{ offset: u32(g.code.len) - 8, name: e.left.str_v + e.right.str_v, kind: 1 } + return true + } + // boolean short-circuit folding: only when both sides are bool literals + if e.left.kind == .bool_lit && e.right.kind == .bool_lit { + if e.op == .kw_and { + g.code << op_push_i + g.code << obj.encode_i64(if e.left.int_v != 0 && e.right.int_v != 0 { 1 } else { 0 }) + return true + } + if e.op == .kw_or { + g.code << op_push_i + g.code << obj.encode_i64(if e.left.int_v != 0 || e.right.int_v != 0 { 1 } else { 0 }) + return true + } + } + return false +} + fn (mut g Gen) gen_binary(e Expr) ! { + if g.fold_binary(e) { + return + } match e.op { .kw_and { // a and b → short-circuit: if !a or !b then 0 else 1 @@ -664,6 +1084,11 @@ fn (mut g Gen) gen_binary(e Expr) ! { .le { op_le } .gt { op_gt } .ge { op_ge } + .amp { op_and_b } + .pipe { op_or_b } + .caret { op_xor } + .lt_lt { op_shl } + .gt_gt { op_shr } else { return error('unsupported binary operator at line ${e.line}') } @@ -678,6 +1103,9 @@ fn (mut g Gen) gen_binary(e Expr) ! { // variable, or an enum variant `Enum.variant`. Everything else has no // known type (''). fn (mut g Gen) expr_type(e Expr) string { + if e.kind == .str_lit { + return 'string' + } if e.kind == .struct_lit { return e.name } @@ -691,14 +1119,28 @@ fn (mut g Gen) expr_type(e Expr) string { return e.left.name } } + // slicing or indexing a known string yields a string + if (e.kind == .slice || e.kind == .index) && g.expr_type(*e.left) == 'string' { + return 'string' + } + // string concatenation: "a" + "b" (or anything + a string literal) + if e.kind == .binary && e.op == .plus && (e.left.kind == .str_lit || e.right.kind == .str_lit) { + return 'string' + } + // string-producing builtins typed as strings so method chains keep working + if e.kind == .call { + return match e.name { + 'upper', 'lower', 'trim', 'str', 'getenv', 'read_file', 'join' { 'string' } + else { '' } + } + } return '' } // method_receiver_type resolves the struct type a method call is made on. -// The receiver must be a plain variable whose type the compiler knows -// (from a typed literal, an assignment, or a method receiver binding) -// or an enum variant expression (e.g. Color.red). -fn (mut g Gen) method_receiver_type(e Expr) !string { +// Returns '' when the type is statically unknown (at which point the +// call becomes a dynamic closure invocation via field access). +fn (mut g Gen) method_receiver_type(e Expr) string { recv := e.left if recv.kind == .ident { t := g.types[recv.name] or { '' } @@ -706,14 +1148,14 @@ fn (mut g Gen) method_receiver_type(e Expr) !string { return t } } - // enum variant: Color.red → type is "Color" + // enum variant: Color.red -> type is "Color" if recv.kind == .field && recv.left.kind == .ident { key := '${recv.left.name}.${recv.name}' if key in g.enum_vals { return recv.left.name } } - return error('cannot resolve method "${e.name}": receiver type unknown (line ${e.line})') + return '' } // gen_enum_to_string generates bytecode for `e.to_string()` on an enum value. diff --git a/compiler/compiler.v b/compiler/compiler.v index 429cd70..e1db6f0 100644 --- a/compiler/compiler.v +++ b/compiler/compiler.v @@ -8,6 +8,8 @@ import obj pub fn compile(src string) !obj.Obj { toks := tokenize(src)! prog := parse(toks)! + // conservative compile-time type check; catches provable errors early + check(prog)! return gen(prog) } @@ -15,3 +17,18 @@ pub fn compile_file(path string) !obj.Obj { src := os.read_file(path)! return compile(src)! } + +// resolve_import turns an import path into a readable source file. It tries +// the path as given first, then falls back to the package-manager layout so +// `import "pkg/file.vr"` finds vendor/pkg/file.vr. +pub fn resolve_import(path string) !string { + if os.exists(path) { + return path + } + for cand in ['vendor/${path}', 'vendor/${path}.vr', 'vendor/${path}/main.vr', 'vendor/${path}/src/main.vr'] { + if os.exists(cand) { + return cand + } + } + return error('cannot resolve import "${path}" (tried vendor/)"') +} diff --git a/compiler/lexer.v b/compiler/lexer.v index 9b9820f..330356b 100644 --- a/compiler/lexer.v +++ b/compiler/lexer.v @@ -2,7 +2,7 @@ module compiler pub fn tokenize(src string) ![]Tok { - mut l := Lexer{ src: src } + mut l := Lexer{ src: src, line: 1 } mut toks := []Tok{} for { t := l.next()! @@ -16,9 +16,10 @@ pub fn tokenize(src string) ![]Tok { struct Lexer { mut: - src string - pos int - line int + src string + pos int + line int + line_start int // byte offset where the current line begins } fn (mut l Lexer) peek() u8 { @@ -40,6 +41,7 @@ fn (mut l Lexer) advance() u8 { l.pos++ if c == `\n` { l.line++ + l.line_start = l.pos } return c } @@ -61,34 +63,35 @@ fn (mut l Lexer) next() !Tok { break } line := l.line + col := l.pos - l.line_start + 1 // 1-based column if l.pos >= l.src.len { - return Tok{ kind: .eof, lit: '', line: line } + return Tok{ kind: .eof, lit: '', line: line, col: col } } c := l.peek() match c { `(` { l.advance() - return Tok{ kind: .lparen, lit: '(', line: line } + return Tok{ kind: .lparen, lit: '(', line: line, col: col } } `)` { l.advance() - return Tok{ kind: .rparen, lit: ')', line: line } + return Tok{ kind: .rparen, lit: ')', line: line, col: col } } `{` { l.advance() - return Tok{ kind: .lbrace, lit: '{', line: line } + return Tok{ kind: .lbrace, lit: '{', line: line, col: col } } `}` { l.advance() - return Tok{ kind: .rbrace, lit: '}', line: line } + return Tok{ kind: .rbrace, lit: '}', line: line, col: col } } `[` { l.advance() - return Tok{ kind: .lbracket, lit: '[', line: line } + return Tok{ kind: .lbracket, lit: '[', line: line, col: col } } `]` { l.advance() - return Tok{ kind: .rbracket, lit: ']', line: line } + return Tok{ kind: .rbracket, lit: ']', line: line, col: col } } `.` { l.advance() @@ -96,112 +99,165 @@ fn (mut l Lexer) next() !Tok { l.advance() if l.peek() == `.` { l.advance() - return Tok{ kind: .dotdotdot, lit: '...', line: line } + return Tok{ kind: .dotdotdot, lit: '...', line: line, col: col } } - return Tok{ kind: .dotdot, lit: '..', line: line } + return Tok{ kind: .dotdot, lit: '..', line: line, col: col } } - return Tok{ kind: .dot, lit: '.', line: line } + return Tok{ kind: .dot, lit: '.', line: line, col: col } } `,` { l.advance() - return Tok{ kind: .comma, lit: ',', line: line } + return Tok{ kind: .comma, lit: ',', line: line, col: col } } `:` { l.advance() - return Tok{ kind: .colon, lit: ':', line: line } + return Tok{ kind: .colon, lit: ':', line: line, col: col } } `+` { l.advance() if l.peek() == `=` { l.advance() - return Tok{ kind: .plus_eq, lit: '+=', line: line } + return Tok{ kind: .plus_eq, lit: '+=', line: line, col: col } } - return Tok{ kind: .plus, lit: '+', line: line } + return Tok{ kind: .plus, lit: '+', line: line, col: col } } `-` { l.advance() if l.peek() == `=` { l.advance() - return Tok{ kind: .minus_eq, lit: '-=', line: line } + return Tok{ kind: .minus_eq, lit: '-=', line: line, col: col } } - return Tok{ kind: .minus, lit: '-', line: line } + return Tok{ kind: .minus, lit: '-', line: line, col: col } } `*` { l.advance() if l.peek() == `=` { l.advance() - return Tok{ kind: .star_eq, lit: '*=', line: line } + return Tok{ kind: .star_eq, lit: '*=', line: line, col: col } } - return Tok{ kind: .star, lit: '*', line: line } + return Tok{ kind: .star, lit: '*', line: line, col: col } } `/` { l.advance() if l.peek() == `=` { l.advance() - return Tok{ kind: .slash_eq, lit: '/=', line: line } + return Tok{ kind: .slash_eq, lit: '/=', line: line, col: col } } - return Tok{ kind: .slash, lit: '/', line: line } + return Tok{ kind: .slash, lit: '/', line: line, col: col } } `%` { l.advance() - return Tok{ kind: .percent, lit: '%', line: line } + return Tok{ kind: .percent, lit: '%', line: line, col: col } + } + `&` { + l.advance() + return Tok{ kind: .amp, lit: '&', line: line, col: col } + } + `|` { + l.advance() + return Tok{ kind: .pipe, lit: '|', line: line, col: col } + } + `^` { + l.advance() + return Tok{ kind: .caret, lit: '^', line: line, col: col } + } + `~` { + l.advance() + return Tok{ kind: .tilde, lit: '~', line: line, col: col } } `=` { l.advance() if l.peek() == `=` { l.advance() - return Tok{ kind: .eq_eq, lit: '==', line: line } + return Tok{ kind: .eq_eq, lit: '==', line: line, col: col } } - return Tok{ kind: .assign, lit: '=', line: line } + return Tok{ kind: .assign, lit: '=', line: line, col: col } } `!` { l.advance() if l.peek() == `=` { l.advance() - return Tok{ kind: .not_eq, lit: '!=', line: line } + return Tok{ kind: .not_eq, lit: '!=', line: line, col: col } } - return error('unexpected character "!" at line ${line} (did you mean "not"?)') + return error('unexpected character "!" at line ${line}, col ${col} (did you mean "not"?)') } `<` { l.advance() if l.peek() == `=` { l.advance() - return Tok{ kind: .le, lit: '<=', line: line } + return Tok{ kind: .le, lit: '<=', line: line, col: col } } - return Tok{ kind: .lt, lit: '<', line: line } + if l.peek() == `<` { + l.advance() + return Tok{ kind: .lt_lt, lit: '<<', line: line, col: col } + } + return Tok{ kind: .lt, lit: '<', line: line, col: col } } `>` { l.advance() if l.peek() == `=` { l.advance() - return Tok{ kind: .ge, lit: '>=', line: line } + return Tok{ kind: .ge, lit: '>=', line: line, col: col } } - return Tok{ kind: .gt, lit: '>', line: line } + if l.peek() == `>` { + l.advance() + return Tok{ kind: .gt_gt, lit: '>>', line: line, col: col } + } + return Tok{ kind: .gt, lit: '>', line: line, col: col } } `\"` { - return l.lex_string(line)! + return l.lex_string(line, col)! } `0`...`9` { - return l.lex_number(line) + return l.lex_number(line, col) } else { if (c >= `a` && c <= `z`) || (c >= `A` && c <= `Z`) || c == `_` { - return l.lex_ident(line) + return l.lex_ident(line, col) } - return error('unexpected character "${c.ascii_str()}" at line ${line}') + return error('unexpected character "${c.ascii_str()}" at line ${line}, col ${col}') } } } -fn (mut l Lexer) lex_number(line int) Tok { +fn (mut l Lexer) lex_number(line int, col int) Tok { start := l.pos for l.pos < l.src.len && l.peek() >= `0` && l.peek() <= `9` { l.advance() } - return Tok{ kind: .int_lit, lit: l.src[start..l.pos], line: line } + mut is_float := false + // fractional part: `.` followed by a digit (so `1..3` and `1...3` stay ints) + if l.peek() == `.` && l.pos + 1 < l.src.len && l.peek2() >= `0` && l.peek2() <= `9` { + is_float = true + l.advance() // consume `.` + for l.pos < l.src.len && l.peek() >= `0` && l.peek() <= `9` { + l.advance() + } + } + // exponent part: e / E followed by optional sign and digits + if l.peek() == `e` || l.peek() == `E` { + save := l.pos + l.advance() + if l.peek() == `+` || l.peek() == `-` { + l.advance() + } + if l.peek() >= `0` && l.peek() <= `9` { + is_float = true + for l.pos < l.src.len && l.peek() >= `0` && l.peek() <= `9` { + l.advance() + } + } else { + l.pos = save // not an exponent after all + } + } + lit := l.src[start..l.pos] + if is_float { + return Tok{ kind: .float_lit, lit: lit, line: line, col: col } + } + return Tok{ kind: .int_lit, lit: lit, line: line, col: col } } -fn (mut l Lexer) lex_ident(line int) Tok { +fn (mut l Lexer) lex_ident(line int, col int) Tok { start := l.pos for l.pos < l.src.len { c := l.peek() @@ -236,18 +292,26 @@ fn (mut l Lexer) lex_ident(line int) Tok { 'import' { TokKind.kw_import } 'enum' { TokKind.kw_enum } 'const' { TokKind.kw_const } + 'interface' { TokKind.kw_interface } + 'try' { TokKind.kw_try } + 'catch' { TokKind.kw_catch } + 'throw' { TokKind.kw_throw } else { TokKind.ident } } - return Tok{ kind: kind, lit: lit, line: line } + return Tok{ kind: kind, lit: lit, line: line, col: col } } -fn (mut l Lexer) lex_string(line int) !Tok { +fn (mut l Lexer) lex_string(line int, col int) !Tok { l.advance() // opening quote mut s := '' + mut has_interp := false for l.pos < l.src.len { c := l.advance() if c == `\"` { - return Tok{ kind: .str_lit, lit: s, line: line } + if has_interp { + return Tok{ kind: .str_interp, lit: s, line: line, col: col } + } + return Tok{ kind: .str_lit, lit: s, line: line, col: col } } if c == `\\` { if l.pos >= l.src.len { @@ -268,12 +332,18 @@ fn (mut l Lexer) lex_string(line int) !Tok { s += '\\' } else { - return error('invalid escape \\${e.ascii_str()} at line ${line}') + return error('invalid escape \\${e.ascii_str()} at line ${line}, col ${col}') } } continue } + if c == `$` && l.peek() == `{` { + has_interp = true + s += '\${' + l.advance() // skip the '{' + continue + } s += c.ascii_str() } - return error('unterminated string at line ${line}') + return error('unterminated string at line ${line}, col ${col}') } diff --git a/compiler/opcodes.v b/compiler/opcodes.v index 73ac572..c30d491 100644 --- a/compiler/opcodes.v +++ b/compiler/opcodes.v @@ -47,3 +47,61 @@ const op_shas = u8(40) const op_sdel = u8(41) const op_slen = u8(42) const op_skeys = u8(43) +const op_slice = u8(44) +const op_push_f = u8(45) +const op_native = u8(46) +const op_and_b = u8(47) +const op_or_b = u8(48) +const op_xor = u8(49) +const op_shl = u8(50) +const op_shr = u8(51) +const op_not_b = u8(52) +const op_try = u8(53) +const op_throw = u8(54) +const op_catch_done = u8(55) +const op_closure = u8(56) +const op_call_closure = u8(57) +const op_argc = u8(58) // push the current frame's arg count +const op_load_dyn = u8(59) // pop idx, push stack[bp + idx] +const op_varargs = u8(60) // — collect args[named..argc-1] into an array at local dst +const op_str_method = u8(61) // — call a string method (s.len(), s.contains(x), ...) + +// native builtin ids (keep in sync with vm/opcodes.v) +const native_abs = 100 +const native_min = 101 +const native_max = 102 +const native_pow = 103 +const native_sqrt = 104 +const native_floor = 105 +const native_ceil = 106 +const native_round = 107 +const native_rand = 108 +const native_rand_int = 109 +const native_int = 110 +const native_str = 111 +const native_float = 112 +const native_type = 113 +const native_split = 114 +const native_join = 115 +const native_contains = 116 +const native_starts_with = 117 +const native_ends_with = 118 +const native_trim = 119 +const native_lower = 120 +const native_upper = 121 +const native_pop = 122 +const native_insert = 123 +const native_remove = 124 +const native_sort = 125 +const native_clone = 126 +const native_reverse = 127 +const native_index_of = 128 +const native_args = 129 +const native_getenv = 130 +const native_setenv = 131 +const native_exit = 132 +const native_time = 133 +const native_sleep = 134 +const native_read_file = 135 +const native_write_file = 136 +const native_eprint = 137 diff --git a/compiler/parser.v b/compiler/parser.v index 29ec861..f99984e 100644 --- a/compiler/parser.v +++ b/compiler/parser.v @@ -64,7 +64,7 @@ fn (mut p Parser) advance() Tok { fn (mut p Parser) expect(k TokKind, what string) !Tok { t := p.cur() if t.kind != k { - return error('expected ${what}, got "${t.lit}" at line ${t.line}') + return error('expected ${what}, got "${t.lit}" at line ${t.line}, col ${t.col}') } return p.advance() } @@ -92,8 +92,9 @@ fn (mut p Parser) parse_program() !Program { .kw_struct { prog.structs << p.parse_struct_decl()! } .kw_enum { prog.enums << p.parse_enum_decl()! } .kw_const { prog.consts << p.parse_const_decl()! } + .kw_interface { prog.interfaces << p.parse_interface_decl()! } .kw_fn { prog.fns << p.parse_fn()! } - else { return error('unexpected token "${p.cur().lit}" at line ${p.cur().line}') } + else { return error('unexpected token "${p.cur().lit}" at line ${p.cur().line}, col ${p.cur().col}') } } } if prog.fns.len == 0 { @@ -163,6 +164,42 @@ fn (mut p Parser) parse_const_decl() !ConstDecl { return ConstDecl{ name: name.lit, value: value, line: t.line } } +// parse_interface_decl parses `interface Name { method1() method2() ... }`. +// Methods may optionally be followed by `()` (any parameter list is ignored) +// and are separated by commas or newlines. +fn (mut p Parser) parse_interface_decl() !InterfaceDecl { + t := p.expect(.kw_interface, "'interface'")! + name := p.expect(.ident, 'interface name')! + p.expect(.lbrace, "'{'")! + mut methods := []InterfaceMethod{} + if p.cur().kind != .rbrace { + for { + mname := p.expect(.ident, 'method name')! + if p.cur().kind == .lparen { + p.advance() + for p.cur().kind != .rparen { + if p.cur().kind == .eof { + return error('unexpected end of file in interface method (line ${mname.line})') + } + p.advance() + } + p.expect(.rparen, "')'")! + } + methods << InterfaceMethod{ name: mname.lit, line: mname.line } + if p.cur().kind == .comma { + p.advance() + continue + } + if p.cur().kind != .rbrace { + continue + } + break + } + } + p.expect(.rbrace, "'}'")! + return InterfaceDecl{ name: name.lit, methods: methods, line: t.line } +} + // parse_fn parses `fn name(params) { }` or a method `fn (p Type) name(params) { }`. fn (mut p Parser) parse_fn() !FnDecl { fn_tok := p.expect(.kw_fn, "'fn'")! @@ -176,18 +213,76 @@ fn (mut p Parser) parse_fn() !FnDecl { p.expect(.rparen, "')'")! } name := p.expect(.ident, 'function name')! - params := p.parse_params()! + // generic type parameters: fn first[T, U](arr) { ... } — captured so the + // type checker can validate call sites; the VM is dynamically typed, so + // they erase to a single function at runtime + mut type_params := []string{} + if p.cur().kind == .lbracket { + p.advance() + for p.cur().kind != .rbracket { + tp := p.expect(.ident, 'type parameter')!.lit + if tp in type_params { + return error('duplicate type parameter "${tp}" in function ${name.lit} (line ${name.line}, col ${name.col})') + } + type_params << tp + if p.cur().kind == .comma { + p.advance() + } + } + p.expect(.rbracket, "']'")! + } + params, defaults, has_defs, variadic := p.parse_params()! body := p.parse_block()! - return FnDecl{ name: name.lit, recv_name: recv_name, recv_type: recv_type, params: params, body: body, line: fn_tok.line } + return FnDecl{ + name: name.lit + type_params: type_params + recv_name: recv_name + recv_type: recv_type + params: params + defaults: defaults + has_defs: has_defs + variadic: variadic + body: body + line: fn_tok.line + } } -// parse_params parses `(a, b, c)` — the parameter list of a function. -fn (mut p Parser) parse_params() ![]string { +// parse_params parses `(a, b, c = expr, rest...)` — the parameter list of a +// function. Returns the names, the default-value expressions (parallel array, +// empty Expr{} when no default), whether each has a default, and whether the +// last parameter is variadic. +fn (mut p Parser) parse_params() !([]string, []Expr, []bool, bool) { p.expect(.lparen, "'('")! mut params := []string{} + mut defaults := []Expr{} + mut has_defs := []bool{} + mut variadic := false if p.cur().kind != .rparen { for { - params << p.expect(.ident, 'parameter name')!.lit + name_tok := p.expect(.ident, 'parameter name')! + name := name_tok.lit + if p.cur().kind == .dotdotdot { + // variadic parameter: `rest...` (must be last) + p.advance() + params << name + defaults << Expr{} + has_defs << false + variadic = true + if p.cur().kind == .comma { + return error('a variadic parameter must be last (line ${name_tok.line})') + } + break + } + mut def := Expr{} + mut has_def := false + if p.cur().kind == .assign { + p.advance() + def = p.parse_expr()! + has_def = true + } + params << name + defaults << def + has_defs << has_def if p.cur().kind == .comma { p.advance() continue @@ -196,7 +291,7 @@ fn (mut p Parser) parse_params() ![]string { } } p.expect(.rparen, "')'")! - return params + return params, defaults, has_defs, variadic } fn (mut p Parser) parse_block() ![]Stmt { @@ -217,6 +312,30 @@ fn (mut p Parser) parse_stmt() !Stmt { match t.kind { .kw_let { p.advance() + // destructuring let: `let { a, b } = expr` or `let [a, b] = expr` + if p.cur().kind == .lbrace || p.cur().kind == .lbracket { + is_field := p.cur().kind == .lbrace + p.advance() + mut names := []string{} + mut closing := TokKind.rbracket + if is_field { + closing = .rbrace + } + if p.cur().kind != closing { + for { + names << p.expect(.ident, 'binding name')!.lit + if p.cur().kind == .comma { + p.advance() + continue + } + break + } + } + p.expect(closing, "']' or '}'")! + p.expect(.assign, "'='")! + e := p.parse_expr()! + return Stmt{ kind: .destruct_stmt, expr: e, destruct_targets: names, destruct_field: is_field, line: t.line } + } name := p.expect(.ident, 'variable name')! p.expect(.assign, "'='")! e := p.parse_expr()! @@ -257,6 +376,14 @@ fn (mut p Parser) parse_stmt() !Stmt { .kw_for { p.advance() name := p.expect(.ident, 'loop variable')! + mut idx_name := '' + mut val_name := name.lit + if p.cur().kind == .comma { + // for i, v in arr { ... } + p.advance() + idx_name = name.lit + val_name = p.expect(.ident, 'loop value variable')!.lit + } p.expect(.kw_in, "'in'")! first := p.parse_expr()! if p.cur().kind == .dotdot || p.cur().kind == .dotdotdot { @@ -264,10 +391,10 @@ fn (mut p Parser) parse_stmt() !Stmt { p.advance() end := p.parse_expr()! body := p.parse_block()! - return Stmt{ kind: .for_range_stmt, target: name.lit, expr: first, cond: end, inclusive: inclusive, body: body, line: t.line } + return Stmt{ kind: .for_range_stmt, target: val_name, expr: first, cond: end, inclusive: inclusive, body: body, line: t.line } } body := p.parse_block()! - return Stmt{ kind: .for_in_stmt, target: name.lit, expr: first, body: body, line: t.line } + return Stmt{ kind: .for_in_stmt, target: val_name, idx_target: idx_name, expr: first, body: body, line: t.line } } .kw_return { p.advance() @@ -297,13 +424,29 @@ fn (mut p Parser) parse_stmt() !Stmt { } return Stmt{ kind: .assert_stmt, expr: e, line: t.line } } + .kw_try { + p.advance() + return p.parse_try_stmt(t)! + } + .kw_throw { + p.advance() + e := p.parse_expr()! + return Stmt{ kind: .throw_stmt, expr: e, line: t.line } + } .ident { p.advance() + // generic type args in statement position: first[int](...) + mut type_args := []string{} + if p.cur().kind == .lbracket && p.looks_like_generic_args() { + type_args = p.parse_type_args()! + } mut e := Expr{} if p.cur().kind == .lparen { // a call statement: foo(args), optionally chained foo().x - e = p.parse_call(t)! + e = p.parse_call(t, type_args)! e = p.parse_postfix_tail(e)! + } else if type_args.len > 0 { + return error('generic type arguments on a non-call "${t.lit}" (line ${t.line}, col ${t.col})') } else { e = p.parse_postfix_tail(Expr{ kind: .ident, name: t.lit, line: t.line })! } @@ -360,11 +503,11 @@ fn (mut p Parser) parse_stmt() !Stmt { } .kw_print, .kw_println { p.advance() - e := p.parse_call(t)! + e := p.parse_call(t, [])! return Stmt{ kind: .expr_stmt, expr: e, line: t.line } } else { - return error('unexpected token "${t.lit}" at line ${t.line}') + return error('unexpected token "${t.lit}" at line ${t.line}, col ${t.col}') } } } @@ -388,6 +531,21 @@ fn (mut p Parser) parse_if(t Tok) !Stmt { return Stmt{ kind: .if_stmt, cond: cond, body: body, els: els, line: t.line } } +// parse_try_stmt parses `try { body } catch ident { body }`. +fn (mut p Parser) parse_try_stmt(t Tok) !Stmt { + body := p.parse_block()! + p.expect(.kw_catch, "'catch'")! + catch_var := p.expect(.ident, 'error variable')!.lit + catch_body := p.parse_block()! + return Stmt{ + kind: .try_stmt + body: body + target: catch_var + els: catch_body + line: t.line + } +} + // bin_node allocates a binary-operator node. It takes copies of the operands // so that `&l`/`&r` target fresh heap objects (taking the address of a local // that is later reassigned would create a self-referential node). @@ -409,6 +567,15 @@ fn index_node(base Expr, idx Expr, line int) Expr { return Expr{ kind: .index, left: &b, right: &i, line: line } } +// slice_node builds `base[start..end]`. +// end uses -1 as sentinel for "open-ended" (slice to end). +fn slice_node(base Expr, start Expr, end Expr, _inclusive bool, line int) Expr { + mut b := base + mut s := start + mut e := end + return Expr{ kind: .slice, left: &b, right: &s, extra: &e, line: line } +} + // field_node builds `base.name`. fn field_node(base Expr, name string, line int) Expr { mut b := base @@ -436,8 +603,41 @@ fn (mut p Parser) parse_or() !Expr { } fn (mut p Parser) parse_and() !Expr { - mut e := p.parse_eq()! + mut e := p.parse_bitor()! for p.cur().kind == .kw_and { + op := p.advance() + rhs := p.parse_bitor()! + e = bin_node(op.kind, e, rhs, op.line) + } + return e +} + +// parse_bitor handles `|` (bitwise OR). +fn (mut p Parser) parse_bitor() !Expr { + mut e := p.parse_bitxor()! + for p.cur().kind == .pipe { + op := p.advance() + rhs := p.parse_bitxor()! + e = bin_node(op.kind, e, rhs, op.line) + } + return e +} + +// parse_bitxor handles `^` (bitwise XOR). +fn (mut p Parser) parse_bitxor() !Expr { + mut e := p.parse_bitand()! + for p.cur().kind == .caret { + op := p.advance() + rhs := p.parse_bitand()! + e = bin_node(op.kind, e, rhs, op.line) + } + return e +} + +// parse_bitand handles `&` (bitwise AND). +fn (mut p Parser) parse_bitand() !Expr { + mut e := p.parse_eq()! + for p.cur().kind == .amp { op := p.advance() rhs := p.parse_eq()! e = bin_node(op.kind, e, rhs, op.line) @@ -456,8 +656,19 @@ fn (mut p Parser) parse_eq() !Expr { } fn (mut p Parser) parse_rel() !Expr { - mut e := p.parse_add()! + mut e := p.parse_shift()! for p.cur().kind == .lt || p.cur().kind == .le || p.cur().kind == .gt || p.cur().kind == .ge { + op := p.advance() + rhs := p.parse_shift()! + e = bin_node(op.kind, e, rhs, op.line) + } + return e +} + +// parse_shift handles `<<` and `>>` (bitwise shift). +fn (mut p Parser) parse_shift() !Expr { + mut e := p.parse_add()! + for p.cur().kind == .lt_lt || p.cur().kind == .gt_gt { op := p.advance() rhs := p.parse_add()! e = bin_node(op.kind, e, rhs, op.line) @@ -487,7 +698,7 @@ fn (mut p Parser) parse_mul() !Expr { fn (mut p Parser) parse_unary() !Expr { t := p.cur() - if t.kind == .kw_not || t.kind == .minus { + if t.kind == .kw_not || t.kind == .minus || t.kind == .tilde { p.advance() e := p.parse_unary()! return unary_node(t.kind, e, t.line) @@ -514,8 +725,22 @@ fn (mut p Parser) parse_postfix_tail(e Expr) !Expr { if p.cur().kind == .lbracket { p.advance() idx := p.parse_expr()! - p.expect(.rbracket, "']'")! - cur = index_node(cur, idx, cur.line) + if p.cur().kind == .dotdot || p.cur().kind == .dotdotdot { + // arr[start..end] or arr[start..] slicing + p.advance() + if p.cur().kind == .rbracket { + // arr[start..] — slice to end + p.advance() + cur = slice_node(cur, idx, Expr{ kind: .int_lit, int_v: -1, line: cur.line }, false, cur.line) + } else { + end := p.parse_expr()! + p.expect(.rbracket, "']'")! + cur = slice_node(cur, idx, end, false, cur.line) + } + } else { + p.expect(.rbracket, "']'")! + cur = index_node(cur, idx, cur.line) + } continue } if p.cur().kind == .dot { @@ -542,10 +767,18 @@ fn (mut p Parser) parse_primary() !Expr { p.advance() return Expr{ kind: .int_lit, int_v: t.lit.i64(), line: t.line } } + .float_lit { + p.advance() + return Expr{ kind: .float_lit, float_v: t.lit.f64(), line: t.line } + } .str_lit { p.advance() return Expr{ kind: .str_lit, str_v: t.lit, line: t.line } } + .str_interp { + p.advance() + return p.parse_str_interp(t)! + } .kw_true { p.advance() return Expr{ kind: .bool_lit, int_v: 1, line: t.line } @@ -594,24 +827,52 @@ fn (mut p Parser) parse_primary() !Expr { } .kw_print, .kw_println { p.advance() - return p.parse_call(t)! + return p.parse_call(t, [])! + } + .kw_fn { + // anonymous function expression: fn(params) { body } + p.advance() + return p.parse_anon_fn(t)! } else { - return error('unexpected token "${t.lit}" at line ${t.line}') + return error('unexpected token "${t.lit}" at line ${t.line}, col ${t.col}') } } } fn (mut p Parser) parse_call_or_ident(t Tok) !Expr { + // generic type args: first[int](...) — captured for the type checker; + // only treated as type args when [ is followed by idents then ] then ( + mut type_args := []string{} + if p.cur().kind == .lbracket && p.looks_like_generic_args() { + type_args = p.parse_type_args()! + } if p.cur().kind == .lparen { - return p.parse_call(t)! + return p.parse_call(t, type_args)! + } + if type_args.len > 0 { + return error('generic type arguments on a non-call "${t.lit}" (line ${t.line}, col ${t.col})') } return Expr{ kind: .ident, name: t.lit, line: t.line } } -fn (mut p Parser) parse_call(name Tok) !Expr { +// parse_type_args parses `[int, string]` into a list of type names. +fn (mut p Parser) parse_type_args() ![]string { + p.advance() // consume '[' + mut args := []string{} + for p.cur().kind != .rbracket { + args << p.expect(.ident, 'type argument')!.lit + if p.cur().kind == .comma { + p.advance() + } + } + p.expect(.rbracket, "']'")! + return args +} + +fn (mut p Parser) parse_call(name Tok, type_args []string) !Expr { args := p.parse_args()! - return Expr{ kind: .call, name: name.lit, args: args, line: name.line } + return Expr{ kind: .call, name: name.lit, type_args: type_args, args: args, line: name.line } } // parse_args parses `(e1, e2, ...)` and returns the argument expressions. @@ -668,3 +929,113 @@ fn (mut p Parser) looks_like_struct_lit() bool { // typed struct literal: `{ ident :` or map literal: `{ "key" :` return (p.toks[p.pos + 1].kind == .ident || p.toks[p.pos + 1].kind == .str_lit) && p.toks[p.pos + 2].kind == .colon } + +// looks_like_generic_args checks if `[` starts generic type args like `[T]` or `[T, U]` +// rather than array indexing. It peeks ahead to see `ident ... ] (`. +fn (mut p Parser) looks_like_generic_args() bool { + // current token must be lbracket (the caller already checked this) + if p.pos + 2 >= p.toks.len { + return false + } + // must start with an ident after the [ + if p.toks[p.pos + 1].kind != .ident { + return false + } + // scan forward: ident, comma, ident, ..., rbracket, then lparen + mut i := p.pos + 2 + for i < p.toks.len && p.toks[i].kind != .rbracket { + if p.toks[i].kind != .ident && p.toks[i].kind != .comma { + return false + } + i++ + } + if i >= p.toks.len { + return false + } + // p.toks[i] should be rbracket + if i + 1 >= p.toks.len { + return false + } + return p.toks[i + 1].kind == .lparen +} + +// parse_str_interp handles string interpolation: "hello ${name} ${age}". +// It splits the raw string into alternating text/expression parts and builds +// a chain of + concatenations so the compiler needs no special handling. +fn (mut p Parser) parse_str_interp(t Tok) !Expr { + parts := split_str_interp(t.lit) + // parts alternates: text, expr, text, expr, ..., text + if parts.len == 1 { + return Expr{ kind: .str_lit, str_v: parts[0], line: t.line } + } + // build the first string part + mut result := Expr{ kind: .str_lit, str_v: parts[0], line: t.line } + mut i := 1 + for i < parts.len { + // parts[i] is an expression — tokenise and parse it + expr_src := parts[i] + expr_toks := tokenize(expr_src)! + mut ep := Parser{ toks: expr_toks } + expr := ep.parse_expr()! + result = bin_node(.plus, result, expr, t.line) + i++ + // parts[i] is the next text fragment + if i < parts.len { + if parts[i].len > 0 { + str_e := Expr{ kind: .str_lit, str_v: parts[i], line: t.line } + result = bin_node(.plus, result, str_e, t.line) + } + i++ + } + } + return result +} + +// parse_anon_fn parses an anonymous function expression: `fn(params) { body }`. +fn (mut p Parser) parse_anon_fn(t Tok) !Expr { + params, defaults, has_defs, variadic := p.parse_params()! + body := p.parse_block()! + return Expr{ + kind: .anon_fn + fparams: params + fdefaults: defaults + fhas_defs: has_defs + fvariadic: variadic + fn_body: body + line: t.line + } +} + +// split_str_interp splits an interpolated string at ${...} boundaries. +// Returns alternating [text, expr, text, expr, ..., text] fragments. +fn split_str_interp(s string) []string { + mut parts := []string{} + mut i := 0 + mut current := '' + for i < s.len { + if s[i] == `$` && i + 1 < s.len && s[i + 1] == `{` { + parts << current + current = '' + i += 2 + mut depth := 1 + mut expr := '' + for i < s.len && depth > 0 { + if s[i] == `{` { + depth++ + } else if s[i] == `}` { + depth-- + } + if depth > 0 { + expr += s[i].ascii_str() + } + i++ + } + parts << expr + } else { + current += s[i].ascii_str() + i++ + } +} + parts << current + return parts +} diff --git a/compiler/tokens.v b/compiler/tokens.v index 8230085..3a1f201 100644 --- a/compiler/tokens.v +++ b/compiler/tokens.v @@ -5,7 +5,9 @@ pub enum TokKind { eof ident int_lit + float_lit str_lit + str_interp lparen rparen lbrace @@ -20,6 +22,12 @@ pub enum TokKind { star slash percent + amp + pipe + caret + tilde + lt_lt + gt_gt eq_eq not_eq lt @@ -56,6 +64,10 @@ pub enum TokKind { kw_import kw_enum kw_const + kw_interface + kw_try + kw_catch + kw_throw } pub struct Tok { @@ -63,4 +75,5 @@ pub: kind TokKind lit string line int + col int // 1-based column within the line } diff --git a/examples/for_index.vr b/examples/for_index.vr new file mode 100644 index 0000000..e034e4a --- /dev/null +++ b/examples/for_index.vr @@ -0,0 +1,35 @@ +// for i, v in arr — iterate with both index and value + +fn main() { + let fruits = ["apple", "banana", "cherry"] + + // basic indexed iteration + for i, v in fruits { + println("${i}: ${v}") + } + + // use the index for computation + let nums = [10, 20, 30, 40, 50] + let sum = 0 + for i, v in nums { + if i % 2 == 0 { + sum += v + } + } + println("sum of even-indexed elements: ${sum}") + + // build a new array using index + let letters = ["a", "b", "c", "d"] + let tagged = [] + for i, v in letters { + push(tagged, "${v}${i}") + } + println(tagged) + + // plain for-in still works (no index) + for v in fruits { + print(v) + print(" ") + } + println("") +} diff --git a/examples/generics.vr b/examples/generics.vr new file mode 100644 index 0000000..f3a2691 --- /dev/null +++ b/examples/generics.vr @@ -0,0 +1,26 @@ +// generic function syntax (V-style type parameters) + +// generic functions: type params are parsed but the VM handles types dynamically +fn first[T](arr) { + return arr[0] +} + +fn last[T](arr) { + let n = len(arr) + return arr[n - 1] +} + +fn main() { + // works with integers + let nums = [10, 20, 30] + println("first num: ${first[int](nums)}") + println("last num: ${last[int](nums)}") + + // works with strings + let words = ["hello", "world", "foo"] + println("first word: ${first[string](words)}") + println("last word: ${last[string](words)}") + + // type args are optional (VM is dynamically typed) + println("first (no type arg): ${first(nums)}") +} diff --git a/examples/interpolation.vr b/examples/interpolation.vr new file mode 100644 index 0000000..82d0b98 --- /dev/null +++ b/examples/interpolation.vr @@ -0,0 +1,33 @@ +// string interpolation: embed expressions inside "..." with ${} + +fn main() { + let name = "alice" + let age = 30 + + // simple variable interpolation + println("hello ${name}!") + + // multiple interpolations + println("${name} is ${age} years old") + + // expression inside interpolation + println("${name} will be ${age + 1} next year") + + // interpolation with function calls + let nums = [10, 20, 30] + println("first element: ${nums[0]}") + + // string concatenation with interpolation + let greeting = "hi" + println("${greeting}, ${name}! age=${age}") + + // no interpolation (regular string) + println("no interpolation here") + + // empty interpolation adjacent to text + let x = 42 + println("value is ${x}") + + // arithmetic in interpolation + println("double age: ${age * 2}") +} diff --git a/examples/slicing.vr b/examples/slicing.vr new file mode 100644 index 0000000..b6b80cc --- /dev/null +++ b/examples/slicing.vr @@ -0,0 +1,44 @@ +// array and string slicing: arr[1..3], arr[1:], str[0..5] + +fn main() { + // --- array slicing --- + let nums = [10, 20, 30, 40, 50] + + // basic slice: arr[start..end] + let mid = nums[1..3] + println(mid) // [20, 30] + + // slice from start + let first_two = nums[0..2] + println(first_two) // [10, 20] + + // slice to end (open-ended) + let tail = nums[2..] + println(tail) // [30, 40, 50] + + // single element slice + let single = nums[3..4] + println(single) // [40] + + // --- string slicing --- + let greeting = "hello, world!" + + // basic substring + let hello = greeting[0..5] + println(hello) // "hello" + + // slice from index + let world = greeting[7..] + println(world) // "world!" + + // slice to index + let hello_comma = greeting[0..6] + println(hello_comma) // "hello," + + // --- slicing in expressions --- + let data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + let first = data[0..3] + let last = data[7..] + println(first) + println(last) +} diff --git a/fmt.v b/fmt.v new file mode 100644 index 0000000..59df6ee --- /dev/null +++ b/fmt.v @@ -0,0 +1,104 @@ +// fmt.v — `vr fmt`: a source formatter for VuurRaaf. +// +// Works line-by-line: it fixes indentation from brace depth, trims stray +// whitespace, collapses space runs outside string literals, and preserves +// every comment (whole-line and inline). It never reorders or rewrites code. +// +// vr fmt file.vr print the formatted source to stdout +// vr fmt -w file.vr rewrite the file in place +module main + +import os + +fn toolchain_fmt(args []string) ! { + if args.len == 0 { + return error('usage: vr fmt [-w] ') + } + mut write := false + mut files := []string{} + for a in args { + if a == '-w' || a == '--write' { + write = true + } else { + files << a + } + } + if files.len == 0 { + return error('usage: vr fmt [-w] ') + } + for f in files { + src := os.read_file(f) or { return error('cannot read "${f}": ${err.msg()}') } + formatted := fmt_source(src) + if write { + os.write_file(f, formatted) or { return error('cannot write "${f}": ${err.msg()}') } + println('formatted ${f}') + } else { + print(formatted) + } + } +} + +// fmt_source normalizes a whole source file. Comment lines are indented with +// the surrounding depth; closing braces drop one level like the code does. +fn fmt_source(src string) string { + mut out := '' + mut depth := 0 + for raw in src.split_into_lines() { + line := raw.trim_space() + if line == '' { + out += '\n' + continue + } + d := brace_delta(line) + mut indent := depth + if line.starts_with('}') && depth > 0 { + indent = depth - 1 + } + out += '\t'.repeat(indent) + collapse_spaces(line) + '\n' + depth += d + if depth < 0 { + depth = 0 + } + } + return out +} + +// collapse_spaces replaces runs of spaces outside of string literals with a +// single space, so indentation and padding become canonical without touching +// string contents. +fn collapse_spaces(line string) string { + mut out := '' + mut in_str := false + mut i := 0 + for i < line.len { + c := line[i] + if c == `"` { + in_str = !in_str + out += c.ascii_str() + i++ + continue + } + if in_str && c == `\\` { + out += c.ascii_str() + i++ + if i < line.len { + out += line[i].ascii_str() + i++ + } + continue + } + if !in_str && c == ` ` { + for i < line.len && line[i] == ` ` { + i++ + } + out += ' ' + continue + } + out += c.ascii_str() + i++ + } + return out +} + +// brace_delta is shared with the REPL (repl.v): it tokenizes the line so +// braces inside string literals do not affect depth. diff --git a/linker/linker.v b/linker/linker.v index d543add..07eccf9 100644 --- a/linker/linker.v +++ b/linker/linker.v @@ -15,6 +15,7 @@ pub fn link(paths []string, out string) ! { mut strings := []string{} mut symbols := map[string]int{} mut relocs := []obj.Reloc{} + mut lines := []obj.LineInfo{} for p in paths { o := obj.read(p)! base := code.len @@ -28,6 +29,9 @@ pub fn link(paths []string, out string) ! { for r in o.relocs { relocs << obj.Reloc{ offset: r.offset + u32(base), name: r.name, kind: r.kind } } + for l in o.lines { + lines << obj.LineInfo{ off: l.off + u32(base), line: l.line } + } } // resolve relocations for r in relocs { @@ -47,7 +51,7 @@ pub fn link(paths []string, out string) ! { for name, entry in symbols { fns << obj.BinFn{ name: name, entry: entry } } - obj.write_bin(out, obj.Bin{ fns: fns, strings: strings, code: code })! + obj.write_bin(out, obj.Bin{ fns: fns, strings: strings, code: code, lines: lines })! } fn intern_str(mut table []string, s string) int { diff --git a/main.v b/main.v index 61b209d..78fe250 100644 --- a/main.v +++ b/main.v @@ -70,6 +70,24 @@ fn main() { 'bench', 'b' { toolchain_bench(rest) or { die('bench', err) } } + 'repl', 'i' { + toolchain_repl() or { die('repl', err) } + } + 'fmt' { + toolchain_fmt(rest) or { die('fmt', err) } + } + 'init' { + toolchain_init(rest) or { die('init', err) } + } + 'get' { + toolchain_get(rest) or { die('get', err) } + } + 'install' { + toolchain_install() or { die('install', err) } + } + 'list' { + toolchain_list() or { die('list', err) } + } 'clean' { toolchain_clean() } @@ -153,6 +171,12 @@ fn toolchain_help() { println(' debug run with an instruction trace') println(' test run every test_* function') println(' bench [iterations] benchmark main()') + println(' repl interactive session') + println(' fmt [-w] format source') + println(' init [name] scaffold a project') + println(' get fetch a package into vendor/') + println(' install install deps from vr.mod') + println(' list show the project manifest') println(' clean remove build artifacts') println(' up rebuild the vr binary into bin/') println(' symlink link bin/vr into your PATH') @@ -311,16 +335,17 @@ fn toolchain_link(args []string) ! { fn toolchain_run(args []string) ! { if args.len == 0 { - return error('usage: vr run ') + return error('usage: vr run [program-args...]') } f := args[0] + prog_args := args[1..] if f.ends_with('.vbin') { bin := obj.read_bin(f)! - vm.run(bin, 'main', false)! + vm.run_with_args(bin, 'main', false, prog_args)! return } if f.ends_with('.vr') { - run_src(f, 'main', false)! + run_src_with_args(f, 'main', false, prog_args)! return } return error('unsupported file type: ${f} (expected .vr or .vbin)') @@ -423,6 +448,10 @@ fn toolchain_bench(args []string) ! { } fn run_src(src string, entry string, trace bool) ! { + run_src_with_args(src, entry, trace, []string{})! +} + +fn run_src_with_args(src string, entry string, trace bool, 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 { @@ -433,7 +462,7 @@ fn run_src(src string, entry string, trace bool) ! { obj.write(tmp_obj, o)! linker.link([tmp_obj], tmp_bin)! bin := obj.read_bin(tmp_bin)! - vm.run(bin, entry, trace)! + vm.run_with_args(bin, entry, trace, args)! } // --------------------------------------------------------------------------- diff --git a/obj/obj.v b/obj/obj.v index f8a4e1c..0fa6a53 100644 --- a/obj/obj.v +++ b/obj/obj.v @@ -13,6 +13,7 @@ module obj import os +import math pub const magic = 'VROBJ' pub const bin_magic = 'VRBIN' @@ -36,6 +37,7 @@ pub mut: strings []string code []u8 relocs []Reloc + lines []LineInfo } pub struct BinFn { @@ -44,11 +46,20 @@ pub mut: entry int } +// LineInfo maps a code offset to the source line it was generated from, +// enabling source-level locations in runtime errors. +pub struct LineInfo { +pub mut: + off u32 + line int +} + pub struct Bin { pub mut: fns []BinFn strings []string code []u8 + lines []LineInfo } // --------------------------------------------------------------------------- @@ -66,6 +77,16 @@ pub fn encode_i64(v i64) []u8 { return b } +// encode_f64 writes a little-endian f64 (its IEEE-754 bit pattern). +pub fn encode_f64(v f64) []u8 { + bits := math.f64_bits(v) + mut b := []u8{} + for i in 0..8 { + b << u8((bits >> (8 * i)) & 0xff) + } + return b +} + // patch_i64 writes a little-endian i64 over `code[off..off+8]`. pub fn patch_i64(mut code []u8, off u32, v i64) { for i in 0..8 { @@ -146,6 +167,11 @@ pub fn write(path string, o Obj) ! { b << r.name.bytes() b << r.kind } + b << encode_u32(u32(o.lines.len)) + for l in o.lines { + b << encode_u32(l.off) + b << encode_i64(i64(l.line)) + } os.write_bytes(path, b)! } @@ -180,6 +206,12 @@ pub fn read(path string) !Obj { kind := r.u8_()! o.relocs << Reloc{ offset: off, name: name, kind: kind } } + nlines := int(r.u32_()!) + for _ in 0..nlines { + off := r.u32_()! + line := int(r.i64_()!) + o.lines << LineInfo{ off: off, line: line } + } return o } @@ -203,6 +235,11 @@ pub fn write_bin(path string, bin Bin) ! { } b << encode_u32(u32(bin.code.len)) b << bin.code + b << encode_u32(u32(bin.lines.len)) + for l in bin.lines { + b << encode_u32(l.off) + b << encode_i64(i64(l.line)) + } os.write_bytes(path, b)! } @@ -229,5 +266,12 @@ pub fn read_bin(path string) !Bin { return error('executable file truncated') } bin.code = b[r.pos..r.pos + ncode] + r.pos += ncode + nlines := int(r.u32_()!) + for _ in 0..nlines { + off := r.u32_()! + line := int(r.i64_()!) + bin.lines << LineInfo{ off: off, line: line } + } return bin } diff --git a/pkg.v b/pkg.v new file mode 100644 index 0000000..6e29901 --- /dev/null +++ b/pkg.v @@ -0,0 +1,151 @@ +// pkg.v — `vr init` / `vr get` / `vr install`: a minimal package manager. +// +// Packages are fetched into ./vendor// and recorded in vr.mod as +// `dep ""`. The compiler resolves `import "pkg/file"` against vendor/ +// automatically (see compiler/compiler.v). Supported package specs: +// +// vr get owner/repo clone from GitHub (https://github.com/owner/repo) +// vr get https://host/repo clone any git repository +// vr get ./local/path copy a local directory +module main + +import os + +const manifest_file = 'vr.mod' +const vendor_dir = 'vendor' + +fn toolchain_init(args []string) ! { + pkg := if args.len > 0 { args[0] } else { os.base(os.getwd()) } + if os.exists(manifest_file) { + return error('${manifest_file} already exists here') + } + os.write_file(manifest_file, 'module ${pkg}\nversion 0.1.0\n\n# deps:\n# dep "owner/repo"\n')! + os.mkdir('vendor') or {} + if !os.exists('main.vr') { + os.write_file('main.vr', 'fn main() {\n\tprintln("hello from ${pkg}")\n}\n')! + } + println('initialized project "${pkg}" (${manifest_file}, main.vr)') +} + +fn toolchain_get(args []string) ! { + if args.len == 0 { + return error('usage: vr get ') + } + spec := args[0] + // determine the package name and how to fetch it + mut pkg := '' + if spec.starts_with('./') || spec.starts_with('/') { + pkg = os.base(spec) + } else if spec.contains('://') { + pkg = os.base(spec) + } else if spec.contains('/') { + pkg = spec.all_after('/') + } else { + pkg = spec + } + if pkg.ends_with('.git') { + pkg = pkg[..pkg.len - 4] + } + if pkg == '' || pkg == '.' { + return error('cannot derive a package name from "${spec}"') + } + os.mkdir(vendor_dir) or {} + dst := os.join_path(vendor_dir, pkg) + if os.exists(dst) { + return error('package "${pkg}" already exists at ${dst}') + } + if spec.starts_with('./') || spec.starts_with('/') { + copy_dir(spec, dst) or { return error('cannot copy ${spec}: ${err.msg()}') } + } else { + url := if spec.contains('://') { spec } else { 'https://github.com/${spec}.git' } + println('cloning ${url} -> ${dst} ...') + r := os.exec(['git', 'clone', '--depth', '1', url, dst]) + if r.exit_code != 0 { + return error('git clone failed: ${r.output}') + } + } + // record the dependency in the manifest (idempotent) + if os.exists(manifest_file) { + manifest := os.read_file(manifest_file) or { '' } + if !manifest.contains('dep "${spec}"') { + os.write_file(manifest_file, manifest.trim_space() + '\ndep "${spec}"\n') or { + return error('cannot update ${manifest_file}: ${err.msg()}') + } + } + } + println('added package "${pkg}" (${spec})') +} + +fn toolchain_install() ! { + if !os.exists(manifest_file) { + return error('no ${manifest_file} found — run `vr init` first') + } + manifest := os.read_file(manifest_file)! + mut n := 0 + for line in manifest.split_into_lines() { + t := line.trim_space() + if !t.starts_with('dep "') { + continue + } + mut spec := t[5..] + if spec.ends_with('"') { + spec = spec[..spec.len - 1] + } + if spec == '' { + continue + } + n++ + toolchain_get([spec]) or { eprintln(' install: ${err.msg()}') } + } + if n == 0 { + println('no dependencies in ${manifest_file}') + return + } + println('installed ${n} package(s) into ${vendor_dir}/') +} + +// copy_dir recursively copies a local directory tree. +fn copy_dir(src string, dst string) ! { + if !os.is_dir(src) { + return error('${src} is not a directory') + } + os.mkdir_all(dst) or { return error('cannot create ${dst}: ${err.msg()}') } + for entry in os.ls(src) or { return error('cannot read ${src}: ${err.msg()}') } { + sp := os.join_path(src, entry) + dp := os.join_path(dst, entry) + if os.is_dir(sp) { + copy_dir(sp, dp)! + } else if os.is_file(sp) { + os.cp(sp, dp, os.CopyParams{}) or { return error('cannot copy ${sp}: ${err.msg()}') } + } + } +} + +fn toolchain_list() ! { + if !os.exists(manifest_file) { + return error('no ${manifest_file} found here') + } + manifest := os.read_file(manifest_file)! + mod_name := manifest.all_before('\n').replace('module ', '').trim_space() + println('project: ${mod_name}') + mut found := false + for line in manifest.split_into_lines() { + t := line.trim_space() + if t.starts_with('dep "') { + found = true + mut spec := t[5..] + if spec.ends_with('"') { + spec = spec[..spec.len - 1] + } + mut pkg := os.base(spec) + if pkg.ends_with('.git') { + pkg = pkg[..pkg.len - 4] + } + status := if os.exists(os.join_path(vendor_dir, pkg)) { 'installed' } else { 'missing' } + println(' ${pkg} (${status}) <- ${spec}') + } + } + if !found { + println(' (no dependencies)') + } +} diff --git a/repl.v b/repl.v new file mode 100644 index 0000000..a58bcbd --- /dev/null +++ b/repl.v @@ -0,0 +1,203 @@ +// repl.v — an interactive read-eval-print loop for VuurRaaf. +// +// The REPL keeps two growing buffers: top-level declarations (fn / struct / +// enum / const / import / interface) and persistent statements (let, assert, +// assignment). Every other line — expressions, println, if/while/for blocks, +// match, try — is transient: it is evaluated once against the accumulated +// state and its output is not repeated on later lines. Expressions are +// echoed like Python's REPL (1 + 1 prints 2); statement keywords run for +// their side effects. +// +// > let x = 5 +// > x * 2 +// 10 +// > fn square(n) { return n * n } +// > square(9) +// 81 +// +// Multi-line input is gathered while braces are unbalanced: +// +// > fn fib(n) { +// ... if n < 2 { return n } +// ... return fib(n - 1) + fib(n - 2) +// ... } +module main + +import os +import obj +import compiler +import linker +import vm + +fn toolchain_repl() ! { + println('VuurRaaf ${version} — type :help for commands, :quit to exit') + mut top := '' // fn / struct / enum / const / import declarations + mut body := '' // persistent statements (let / assert / assignment) + mut pending := '' // accumulated multi-line input + mut depth := 0 // brace balance of pending input + for { + prompt := if depth > 0 { '... ' } else { '> ' } + line := os.input(prompt) + if line == '' { + continue + } + trimmed := line.trim_space() + if trimmed.starts_with(':') && depth == 0 { + act := repl_meta(trimmed) + if act == 1 { + break + } + if act == 2 { + top = '' + body = '' + println('session reset') + } + continue + } + // gather multi-line input until braces balance + pending += line + '\n' + depth += brace_delta(line) + if depth > 0 { + continue + } + block := pending + pending = '' + depth = 0 + match repl_classify(block) { + 'top' { + // fn / struct / enum / const / import — compile-time only + candidate := top + '\n' + block + if repl_compile(candidate, body, '') { + top = candidate + } + } + 'body' { + candidate := body + '\n' + block + if repl_compile(top, candidate, '') { + body = candidate + } + } + else { + // transient: run once against the accumulated state + tail := if repl_is_expr(block) { 'println(${block.trim_space()})' } else { block } + repl_compile(top, body, tail) + } + } + } +} + +// repl_meta handles :quit / :reset / :help. Returns 1 to quit, 2 to reset, +// and 0 otherwise. +fn repl_meta(cmd string) int { + match cmd { + ':q', ':quit', ':exit' { + println('bye') + return 1 + } + ':reset' { + return 2 + } + ':help', ':h' { + println(' :help this help') + println(' :quit :exit leave the REPL') + println(' :reset clear all definitions') + println(' expressions are echoed; let/assign/fn persist') + } + else { + eprintln('repl: unknown command "${cmd}" (:help for a list)') + } + } + return 0 +} + +// repl_compile compiles top declarations plus a main body (with an optional +// trailing tail statement), links, runs, and reports errors without exiting. +// Returns true when the program compiled and ran cleanly. +fn repl_compile(top string, body string, tail string) bool { + src := top + '\nfn main() {\n' + body + '\n' + tail + '\n}' + o := compiler.compile(src) or { + eprintln('repl: ${err.msg()}') + return false + } + tmp_obj := os.join_path(os.temp_dir(), 'vr_repl_${os.getpid()}.vobj') + tmp_bin := os.join_path(os.temp_dir(), 'vr_repl_${os.getpid()}.vbin') + defer { + os.rm(tmp_obj) or {} + os.rm(tmp_bin) or {} + } + obj.write(tmp_obj, o) or { + eprintln('repl: ${err.msg()}') + return false + } + linker.link([tmp_obj], tmp_bin) or { + eprintln('repl: ${err.msg()}') + return false + } + bin := obj.read_bin(tmp_bin) or { + eprintln('repl: ${err.msg()}') + return false + } + vm.run(bin, 'main', false) or { + eprintln('repl: ${err.msg()}') + return false + } + return true +} + +// repl_classify buckets a (possibly multi-line) input block. +fn repl_classify(block string) string { + toks := compiler.tokenize(block) or { return 'transient' } + if toks.len == 0 { + return 'transient' + } + match toks[0].kind { + .kw_fn, .kw_struct, .kw_enum, .kw_const, .kw_import, .kw_interface { + return 'top' + } + .kw_let, .kw_assert { + return 'body' + } + .ident { + // assignment persists; anything else is an expression + if toks.len >= 2 && toks[1].kind in [.assign, .plus_eq, .minus_eq, .star_eq, .slash_eq] { + return 'body' + } + return 'transient' + } + else { + return 'transient' + } + } +} + +// repl_is_expr reports whether a transient block is a bare expression that +// should be echoed (as opposed to a statement keyword like println or if). +fn repl_is_expr(block string) bool { + toks := compiler.tokenize(block) or { return false } + if toks.len == 0 { + return false + } + match toks[0].kind { + .kw_print, .kw_println, .kw_if, .kw_while, .kw_for, .kw_match, .kw_break, .kw_continue, .kw_return, .kw_try, .kw_throw { + return false + } + else { + return true + } + } +} + +// brace_delta counts the brace balance of a line using the tokenizer, so +// braces inside string literals do not confuse continuation detection. +fn brace_delta(line string) int { + toks := compiler.tokenize(line) or { return 0 } + mut d := 0 + for t in toks { + if t.kind == .lbrace { + d++ + } else if t.kind == .rbrace { + d-- + } + } + return d +} diff --git a/vm/gc.v b/vm/gc.v new file mode 100644 index 0000000..95c61a2 --- /dev/null +++ b/vm/gc.v @@ -0,0 +1,211 @@ +// gc.v — mark-and-sweep garbage collector for the VuurRaaf VM. +// +// The collector runs between opcodes (never mid-instruction, so no live value +// is ever hidden in a temporary). Roots are the value stack, which also holds +// every frame's locals (they live at bp+idx). Arrays and structs are traced +// transitively. The sweep compacts each pool and remaps surviving handles. +// +// String constants baked into the bytecode (op_push_s operands) live in +// strings[0..const_strs] and are never collected; only runtime-allocated +// strings participate in the cycle. +module vm + +const gc_alloc_trigger = 4096 + +// collect marks all heap values reachable from the stack, then sweeps and +// compacts the pools, remapping handles on the stack and inside live +// containers. +fn (mut v Vm) collect() { + // ---- mark ---- + mut str_mark := []bool{len: v.strings.len} + mut arr_mark := []bool{len: v.arrays.len} + mut struct_mark := []bool{len: v.structs.len} + mut float_mark := []bool{len: v.floats.len} + mut closure_mark := []bool{len: v.closures.len} + for i in 0..v.sp { + v.mark_value(v.stack[i], mut str_mark, mut arr_mark, mut struct_mark, mut float_mark, mut closure_mark) + } + // ---- remap tables: old index -> new index (-1 = collected) ---- + mut str_new := []int{len: v.strings.len, init: -1} + mut arr_new := []int{len: v.arrays.len, init: -1} + mut struct_new := []int{len: v.structs.len, init: -1} + mut float_new := []int{len: v.floats.len, init: -1} + mut closure_new := []int{len: v.closures.len, init: -1} + mut nstr := v.const_strs + for i in v.const_strs..v.strings.len { + if str_mark[i] { + str_new[i] = nstr + nstr++ + } + } + mut narr := 0 + for i in 0..v.arrays.len { + if arr_mark[i] { + arr_new[i] = narr + narr++ + } + } + mut nstruct := 0 + for i in 0..v.structs.len { + if struct_mark[i] { + struct_new[i] = nstruct + nstruct++ + } + } + mut nfloat := 0 + for i in 0..v.floats.len { + if float_mark[i] { + float_new[i] = nfloat + nfloat++ + } + } + mut nclosure := 0 + for i in 0..v.closures.len { + if closure_mark[i] { + closure_new[i] = nclosure + nclosure++ + } + } + // ---- rewrite live references ---- + for i in 0..v.sp { + v.stack[i] = v.remap(v.stack[i], str_new, arr_new, struct_new, float_new, closure_new) + } + for h in 0..v.arrays.len { + if arr_mark[h] { + for j in 0..v.arrays[h].len { + v.arrays[h][j] = v.remap(v.arrays[h][j], str_new, arr_new, struct_new, float_new, closure_new) + } + } + } + for h in 0..v.structs.len { + if struct_mark[h] { + for j in 0..v.structs[h].fields.len { + v.structs[h].fields[j].val = v.remap(v.structs[h].fields[j].val, str_new, arr_new, struct_new, float_new, closure_new) + } + } + } + // ---- compact pools ---- + mut strings := v.strings[..v.const_strs] + for i in v.const_strs..v.strings.len { + if str_mark[i] { + strings << v.strings[i] + } + } + v.strings = strings + mut arrays := [][]i64{} + for i in 0..v.arrays.len { + if arr_mark[i] { + arrays << v.arrays[i] + } + } + v.arrays = arrays + mut structs := []StructVal{} + for i in 0..v.structs.len { + if struct_mark[i] { + structs << v.structs[i] + } + } + v.structs = structs + mut floats := []f64{} + for i in 0..v.floats.len { + if float_mark[i] { + floats << v.floats[i] + } + } + v.floats = floats + mut closures := []Closure{} + for i in 0..v.closures.len { + if closure_mark[i] { + closures << v.closures[i] + } + } + v.closures = closures +} + +// mark_value traces a value and everything it references using an explicit +// worklist (arrays of arrays can nest deeply; recursion could overflow). +fn (mut v Vm) mark_value(x i64, mut str_mark []bool, mut arr_mark []bool, mut struct_mark []bool, mut float_mark []bool, mut closure_mark []bool) { + mut work := []i64{} + work << x + for work.len > 0 { + val := work.pop() + match v.tag(val) { + tag_str { + h := v.hand(val) + if h >= v.const_strs && h < str_mark.len && !str_mark[h] { + str_mark[h] = true + } + } + tag_arr { + h := v.hand(val) + if h >= 0 && h < arr_mark.len && !arr_mark[h] { + arr_mark[h] = true + for el in v.arrays[h] { + work << el + } + } + } + tag_struct { + h := v.hand(val) + if h >= 0 && h < struct_mark.len && !struct_mark[h] { + struct_mark[h] = true + for f in v.structs[h].fields { + work << f.val + } + } + } + tag_float { + h := v.hand(val) + if h >= 0 && h < float_mark.len && !float_mark[h] { + float_mark[h] = true + } + } + tag_closure { + h := v.hand(val) + if h >= 0 && h < closure_mark.len && !closure_mark[h] { + closure_mark[h] = true + } + } + else {} + } + } +} + +// remap translates a handle to its post-compaction index, leaving integers +// and uncollected values untouched. +fn (mut v Vm) remap(x i64, str_new []int, arr_new []int, struct_new []int, float_new []int, closure_new []int) i64 { + match v.tag(x) { + tag_str { + h := v.hand(x) + if h >= v.const_strs && h < str_new.len && str_new[h] >= 0 { + return v.mkstr(str_new[h]) + } + } + tag_arr { + h := v.hand(x) + if h >= 0 && h < arr_new.len && arr_new[h] >= 0 { + return v.mkarr(arr_new[h]) + } + } + tag_struct { + h := v.hand(x) + if h >= 0 && h < struct_new.len && struct_new[h] >= 0 { + return v.mkstruct_handle(struct_new[h]) + } + } + tag_float { + h := v.hand(x) + if h >= 0 && h < float_new.len && float_new[h] >= 0 { + return v.mkfloat(float_new[h]) + } + } + tag_closure { + h := v.hand(x) + if h >= 0 && h < closure_new.len && closure_new[h] >= 0 { + return v.mkclosure(closure_new[h]) + } + } + else {} + } + return x +} diff --git a/vm/native.v b/vm/native.v new file mode 100644 index 0000000..ed09809 --- /dev/null +++ b/vm/native.v @@ -0,0 +1,420 @@ +// native.v — host builtins for the VuurRaaf VM. +// +// The op_native instruction carries a builtin id and an argument count. Each +// builtin pops its arguments off the stack (left-to-right push order means the +// last argument is on top) and pushes a single result (except exit(), which +// halts the machine). This is where the language touches the host: file I/O, +// environment, time, randomness, and the math/collection helpers. +module vm + +import os +import math +import rand +import time + +fn (mut v Vm) native(id int, _argc int) ! { + match id { + native_abs { + x := v.pop()! + if v.is_float(x) { + v.push(v.push_float(math.abs(v.fval(x))))! + } else { + val := v.dec_int(x) + v.push(v.enc_int(if val < 0 { -val } else { val }))! + } + } + native_min { + b := v.pop()! + a := v.pop()! + if v.is_float(a) || v.is_float(b) { + v.push(v.push_float(math.min(v.to_f64(a), v.to_f64(b))))! + } else { + x := v.dec_int(a) + y := v.dec_int(b) + v.push(v.enc_int(if x < y { x } else { y }))! + } + } + native_max { + b := v.pop()! + a := v.pop()! + if v.is_float(a) || v.is_float(b) { + v.push(v.push_float(math.max(v.to_f64(a), v.to_f64(b))))! + } else { + x := v.dec_int(a) + y := v.dec_int(b) + v.push(v.enc_int(if x > y { x } else { y }))! + } + } + native_pow { + b := v.pop()! + a := v.pop()! + v.push(v.push_float(math.pow(v.to_f64(a), v.to_f64(b))))! + } + native_sqrt { + x := v.pop()! + v.push(v.push_float(math.sqrt(v.to_f64(x))))! + } + native_floor { + x := v.pop()! + v.push(v.enc_int(i64(math.floor(v.to_f64(x)))))! + } + native_ceil { + x := v.pop()! + v.push(v.enc_int(i64(math.ceil(v.to_f64(x)))))! + } + native_round { + x := v.pop()! + v.push(v.enc_int(i64(math.round(v.to_f64(x)))))! + } + native_rand { + v.push(v.push_float(rand.f64()))! + } + native_rand_int { + n := int(v.dec_int(v.pop()!)) + if n <= 0 { + return error('rand_int() expects a positive bound') + } + v.push(v.enc_int(i64(rand.intn(n) or { return error('rand_int() failed') })))! + } + native_int { + x := v.pop()! + if v.is_str(x) && v.valid_handle(x) { + v.push(v.enc_int(i64(v.strings[v.hand(x)].i64())))! + } else if v.is_float(x) { + v.push(v.enc_int(i64(v.fval(x))))! + } else if v.is_arr(x) || v.is_struct(x) { + return error('cannot convert a ${v.type_name(x)} to int') + } else { + v.push(x)! + } + } + native_str { + x := v.pop()! + v.push(v.alloc_str(v.val_str(x, 0)))! + } + native_float { + x := v.pop()! + if v.is_str(x) && v.valid_handle(x) { + v.push(v.push_float(v.strings[v.hand(x)].f64()))! + } else if v.is_float(x) { + v.push(x)! + } else if v.is_arr(x) || v.is_struct(x) { + return error('cannot convert a ${v.type_name(x)} to float') + } else { + v.push(v.push_float(f64(v.dec_int(x))))! + } + } + native_type { + x := v.pop()! + v.push(v.alloc_str(v.type_name(x)))! + } + native_split { + delim := v.pop_str()! + s := v.pop_str()! + parts := s.split(delim) + mut arr := []i64{} + for p in parts { + v.strings << p + arr << v.mkstr(v.strings.len - 1) + } + v.arrays << arr + v.push(v.mkarr(v.arrays.len - 1))! + } + native_join { + delim := v.pop_str()! + h := v.pop()! + if !v.is_arr(h) || !v.valid_arr_handle(h) { + return error('join() expects an array as its first argument') + } + a := v.arrays[v.hand(h)] + mut parts := []string{} + for x in a { + if v.is_str(x) && v.valid_handle(x) { + parts << v.strings[v.hand(x)] + } else { + parts << v.val_str(x, 0) + } + } + v.push(v.alloc_str(parts.join(delim)))! + } + native_contains { + sub := v.pop_str()! + s := v.pop_str()! + v.push(v.enc_int(bool_i64(s.contains(sub))))! + } + native_starts_with { + sub := v.pop_str()! + s := v.pop_str()! + v.push(v.enc_int(bool_i64(s.starts_with(sub))))! + } + native_ends_with { + sub := v.pop_str()! + s := v.pop_str()! + v.push(v.enc_int(bool_i64(s.ends_with(sub))))! + } + native_trim { + s := v.pop_str()! + v.push(v.alloc_str(s.trim_space()))! + } + native_lower { + s := v.pop_str()! + v.push(v.alloc_str(s.to_lower()))! + } + native_upper { + s := v.pop_str()! + v.push(v.alloc_str(s.to_upper()))! + } + native_pop { + h := v.pop()! + if !v.is_arr(h) || !v.valid_arr_handle(h) { + return error('pop() expects an array') + } + mut a := v.arrays[v.hand(h)] + if a.len == 0 { + return error('pop() on an empty array') + } + val := a[a.len - 1] + v.arrays[v.hand(h)] = a[..a.len - 1] + v.push(val)! + } + native_insert { + val := v.pop()! + idx := int(v.dec_int(v.pop()!)) + h := v.pop()! + if !v.is_arr(h) || !v.valid_arr_handle(h) { + return error('insert() expects an array as its first argument') + } + a := v.arrays[v.hand(h)] + if idx < 0 || idx > a.len { + return error('insert index ${idx} out of bounds (len ${a.len})') + } + mut na := []i64{} + for i, x in a { + if i == idx { + na << val + } + na << x + } + if idx == a.len { + na << val + } + v.arrays[v.hand(h)] = na + v.push(h)! + } + native_remove { + idx := int(v.dec_int(v.pop()!)) + h := v.pop()! + if !v.is_arr(h) || !v.valid_arr_handle(h) { + return error('remove() expects an array as its first argument') + } + a := v.arrays[v.hand(h)] + if idx < 0 || idx >= a.len { + return error('remove index ${idx} out of bounds (len ${a.len})') + } + mut na := []i64{} + for i, x in a { + if i != idx { + na << x + } + } + v.arrays[v.hand(h)] = na + v.push(h)! + } + native_sort { + h := v.pop()! + if !v.is_arr(h) || !v.valid_arr_handle(h) { + return error('sort() expects an array') + } + mut a := v.arrays[v.hand(h)] + // insertion sort by numeric value + for i in 1..a.len { + key := a[i] + mut j := i - 1 + for j >= 0 && v.num_gt(a[j], key) { + a[j + 1] = a[j] + j-- + } + a[j + 1] = key + } + v.push(h)! + } + native_clone { + x := v.pop()! + if v.is_arr(x) && v.valid_arr_handle(x) { + v.arrays << v.arrays[v.hand(x)].clone() + v.push(v.mkarr(v.arrays.len - 1))! + } else if v.is_struct(x) && v.valid_struct_handle(x) { + s := v.structs[v.hand(x)] + v.structs << StructVal{ fields: s.fields.clone() } + v.push(v.mkstruct_handle(v.structs.len - 1))! + } else if v.is_str(x) && v.valid_handle(x) { + v.push(v.alloc_str(v.strings[v.hand(x)]))! + } else if v.is_float(x) { + v.push(v.push_float(v.fval(x)))! + } else { + v.push(x)! + } + } + native_reverse { + h := v.pop()! + if !v.is_arr(h) || !v.valid_arr_handle(h) { + return error('reverse() expects an array') + } + mut a := v.arrays[v.hand(h)] + for i in 0..a.len / 2 { + a[i], a[a.len - 1 - i] = a[a.len - 1 - i], a[i] + } + v.push(h)! + } + native_index_of { + val := v.pop()! + h := v.pop()! + if v.is_arr(h) && v.valid_arr_handle(h) { + a := v.arrays[v.hand(h)] + for i, x in a { + if v.cmp(x, val, '==')! == 1 { + v.push(v.enc_int(i64(i)))! + return + } + } + v.push(v.enc_int(-1))! + return + } + if v.is_str(h) && v.valid_handle(h) { + if !v.is_str(val) || !v.valid_handle(val) { + return error('index_of() on a string expects a string needle') + } + s := v.strings[v.hand(h)] + needle := v.strings[v.hand(val)] + byte_idx := s.index(needle) or { -1 } + if byte_idx < 0 { + v.push(v.enc_int(-1))! + return + } + // convert byte offset to a rune index so UTF-8 strings count characters + rune_idx := s[..byte_idx].runes().len + v.push(v.enc_int(i64(rune_idx)))! + return + } + return error('index_of() expects an array or string as its first argument') + } + native_args { + mut arr := []i64{} + for s in v.prog_args { + v.strings << s + arr << v.mkstr(v.strings.len - 1) + } + v.arrays << arr + v.push(v.mkarr(v.arrays.len - 1))! + } + native_getenv { + name := v.pop_str()! + v.push(v.alloc_str(os.getenv(name)))! + } + native_setenv { + val := v.pop_str()! + name := v.pop_str()! + os.setenv(name, val, true) + v.push(v.enc_int(0))! + } + native_exit { + code := int(v.dec_int(v.pop()!)) + v.exit_code = i64(code) + v.did_exit = true + v.halted = true + } + native_time { + v.push(v.push_float(f64(time.now().unix_milli()) / 1000.0))! + } + native_sleep { + ms := int(v.dec_int(v.pop()!)) + time.sleep(time.Duration(ms) * time.millisecond) + v.push(v.enc_int(0))! + } + native_read_file { + path := v.pop_str()! + content := os.read_file(path) or { return error('cannot read file "${path}": ${err}') } + v.push(v.alloc_str(content))! + } + native_write_file { + content := v.pop_str()! + path := v.pop_str()! + os.write_file(path, content) or { return error('cannot write file "${path}": ${err}') } + v.push(v.enc_int(0))! + } + native_eprint { + x := v.pop()! + eprintln(v.val_str(x, 0)) + } + else { + return error('unknown native builtin ${id}') + } + } +} + +// pop_str pops the top value and requires it to be a string. +fn (mut v Vm) pop_str() !string { + x := v.pop()! + if !v.is_str(x) || !v.valid_handle(x) { + return error('expected a string argument') + } + return v.strings[v.hand(x)] +} + +// type_name returns the type label of a tagged value. +fn (mut v Vm) type_name(x i64) string { + if v.is_str(x) { + return 'string' + } + if v.is_float(x) { + return 'float' + } + if v.is_arr(x) { + return 'array' + } + if v.is_struct(x) { + return 'struct' + } + return 'int' +} + +// str_method dispatches a string method call. The receiver was pushed before +// the arguments, so it is the first argument from the native builtin's point +// of view; delegating keeps the behavior identical to the free-function forms. +fn (mut v Vm) str_method(name string, argc int) ! { + bid := match name { + 'to_upper' { native_upper } + 'to_lower' { native_lower } + 'trim' { native_trim } + 'contains' { native_contains } + 'starts_with' { native_starts_with } + 'ends_with' { native_ends_with } + 'split' { native_split } + 'index_of' { native_index_of } + 'to_int' { native_int } + 'to_float' { native_float } + else { -1 } + } + if bid >= 0 { + // native builtins pop the first argument (the receiver) last + v.native(bid, argc + 1)! + return + } + if name == 'len' { + h := v.pop()! + if !v.is_str(h) || !v.valid_handle(h) { + return error('len() on a non-string value') + } + v.push(v.enc_int(i64(v.strings[v.hand(h)].runes().len)))! + return + } + return error('unknown string method "${name}"') +} + +// num_gt compares two values by their numeric value (int or float). +fn (mut v Vm) num_gt(x i64, y i64) bool { + if v.is_float(x) || v.is_float(y) { + return v.to_f64(x) > v.to_f64(y) + } + return v.dec_int(x) > v.dec_int(y) +} diff --git a/vm/opcodes.v b/vm/opcodes.v index ecbeccd..1836d39 100644 --- a/vm/opcodes.v +++ b/vm/opcodes.v @@ -47,3 +47,61 @@ const op_shas = u8(40) // has(map, "key") -> 1 if key exists, 0 otherwise const op_sdel = u8(41) // delete(map, "key") -> removes the key const op_slen = u8(42) // slen(struct) -> number of fields const op_skeys = u8(43) // skeys(struct) -> array of field name strings +const op_slice = u8(44) // slice(arr/str, start, end) -> sliced arr/str +const op_push_f = u8(45) // push a float literal (8-byte little-endian f64) +const op_native = u8(46) // call a host builtin: +const op_and_b = u8(47) // bitwise AND +const op_or_b = u8(48) // bitwise OR +const op_xor = u8(49) // bitwise XOR +const op_shl = u8(50) // shift left +const op_shr = u8(51) // shift right +const op_not_b = u8(52) // bitwise NOT +const op_try = u8(53) // push handler: +const op_throw = u8(54) // throw: pop error, unwind to nearest handler, push error +const op_catch_done = u8(55) // pop handler (normal completion) +const op_closure = u8(56) // push closure: +const op_call_closure = u8(57) // call closure: +const op_argc = u8(58) // push the current frame's arg count +const op_load_dyn = u8(59) // pop idx, push stack[bp + idx] +const op_varargs = u8(60) // — collect args[named..argc-1] into array at local dst +const op_str_method = u8(61) // — call a string method (s.len(), s.contains(x), ...) + +// native builtin ids (the operand to op_native) +const native_abs = 100 +const native_min = 101 +const native_max = 102 +const native_pow = 103 +const native_sqrt = 104 +const native_floor = 105 +const native_ceil = 106 +const native_round = 107 +const native_rand = 108 +const native_rand_int = 109 +const native_int = 110 +const native_str = 111 +const native_float = 112 +const native_type = 113 +const native_split = 114 +const native_join = 115 +const native_contains = 116 +const native_starts_with = 117 +const native_ends_with = 118 +const native_trim = 119 +const native_lower = 120 +const native_upper = 121 +const native_pop = 122 +const native_insert = 123 +const native_remove = 124 +const native_sort = 125 +const native_clone = 126 +const native_reverse = 127 +const native_index_of = 128 +const native_args = 129 +const native_getenv = 130 +const native_setenv = 131 +const native_exit = 132 +const native_time = 133 +const native_sleep = 134 +const native_read_file = 135 +const native_write_file = 136 +const native_eprint = 137 diff --git a/vm/ops.v b/vm/ops.v index e3e3b61..1206f4d 100644 --- a/vm/ops.v +++ b/vm/ops.v @@ -1,6 +1,8 @@ // ops.v — arithmetic, comparison, and string operations for the VuurRaaf VM. module vm +import math + fn (mut v Vm) add(a i64, b i64) !i64 { if v.is_arr(a) || v.is_arr(b) { return error('cannot add arrays with +') @@ -17,18 +19,61 @@ fn (mut v Vm) add(a i64, b i64) !i64 { if v.is_str(b) { return v.alloc_str(v.num_str(a) + v.strings[v.hand(b)]) } + if v.is_float(a) || v.is_float(b) { + return v.push_float(v.to_f64(a) + v.to_f64(b)) + } return v.enc_int(v.dec_int(a) + v.dec_int(b)) } +// to_f64 promotes an integer or float tagged value to f64. +fn (mut v Vm) to_f64(x i64) f64 { + if v.is_float(x) { + return v.fval(x) + } + return f64(v.dec_int(x)) +} + fn (mut v Vm) alloc_str(s string) i64 { v.strings << s return v.mkstr(v.strings.len - 1) } fn (mut v Vm) num_str(x i64) string { + if v.is_float(x) { + return fmt_float(v.fval(x)) + } return v.dec_int(x).str() } +// fmt_float renders an f64 nicely: integral values lose the trailing ".0", +// -0 collapses to 0, and most floats avoid V's default scientific notation. +fn fmt_float(f f64) string { + if f == 0.0 { + return '0' + } + if math.is_nan(f) { + return 'NaN' + } + if math.is_inf(f, 1) { + return 'Inf' + } + if math.is_inf(f, -1) { + return '-Inf' + } + if f == math.floor(f) && math.abs(f) < 1e18 { + return i64(f).str() + } + if math.abs(f) < 1e18 { + mut s := '${f:.14f}' + s = s.trim_right('0').trim_right('.') + if s == '' { + return '0' + } + return s + } + return f.str() +} + fn (mut v Vm) arith(a i64, b i64, op string) !i64 { if v.is_str(a) || v.is_str(b) { return error('cannot use strings with "${op}"') @@ -39,6 +84,33 @@ fn (mut v Vm) arith(a i64, b i64, op string) !i64 { if v.is_struct(a) || v.is_struct(b) { return error('cannot use structs with "${op}"') } + if v.is_float(a) || v.is_float(b) { + x := v.to_f64(a) + y := v.to_f64(b) + match op { + '-' { + return v.push_float(x - y) + } + '*' { + return v.push_float(x * y) + } + '/' { + if y == 0.0 { + return error('division by zero') + } + return v.push_float(x / y) + } + '%' { + if y == 0.0 { + return error('division by zero') + } + return v.push_float(math.fmod(x, y)) + } + else { + return error('internal: bad arith op "${op}"') + } + } + } x := v.dec_int(a) y := v.dec_int(b) match op { @@ -97,6 +169,19 @@ fn (mut v Vm) cmp(a i64, b i64, op string) !i64 { if v.is_str(a) || v.is_str(b) { return error('cannot compare a string and a number') } + if v.is_float(a) || v.is_float(b) { + x := v.to_f64(a) + y := v.to_f64(b) + return bool_i64(match op { + '==' { x == y } + '!=' { x != y } + '<' { x < y } + '<=' { x <= y } + '>' { x > y } + '>=' { x >= y } + else { return error('internal: bad cmp op "${op}"') } + }) + } x := v.dec_int(a) y := v.dec_int(b) return bool_i64(match op { diff --git a/vm/print.v b/vm/print.v index 1f92f03..aa322c5 100644 --- a/vm/print.v +++ b/vm/print.v @@ -44,6 +44,12 @@ fn (mut v Vm) val_str(x i64, depth int) string { } return out + '}' } + if v.is_float(x) && v.valid_float_handle(x) { + return fmt_float(v.fval(x)) + } + if v.is_closure(x) && v.valid_closure_handle(x) { + return '' + } return v.dec_int(x).str() } @@ -52,6 +58,7 @@ fn (mut v Vm) trace_op(op u8) { op_halt { 'halt' } op_push_i { 'push_int' } op_push_s { 'push_str' } + op_push_f { 'push_float' } op_load { 'load' } op_store { 'store' } op_pop { 'pop' } @@ -93,6 +100,23 @@ fn (mut v Vm) trace_op(op u8) { op_sdel { 'sdel' } op_slen { 'slen' } op_skeys { 'skeys' } + op_slice { 'slice' } + op_native { 'native' } + op_and_b { 'and_b' } + op_or_b { 'or_b' } + op_xor { 'xor' } + op_shl { 'shl' } + op_shr { 'shr' } + op_not_b { 'not_b' } + op_try { 'try' } + op_throw { 'throw' } + op_catch_done { 'catch_done' } + op_closure { 'closure' } + op_call_closure { 'call_closure' } + op_argc { 'argc' } + op_load_dyn { 'load_dyn' } + op_varargs { 'varargs' } + op_str_method { 'str_method' } else { '??' } } mut s := '' @@ -100,10 +124,13 @@ fn (mut v Vm) trace_op(op u8) { if i > 0 { s += ' ' } - if v.is_str(v.stack[i]) && v.valid_handle(v.stack[i]) { - s += '"${v.strings[v.hand(v.stack[i])]}"' + x := v.stack[i] + if v.is_str(x) && v.valid_handle(x) { + s += '"${v.strings[v.hand(x)]}"' + } else if v.is_closure(x) { + s += '' } else { - s += v.val_str(v.stack[i], 0) + s += v.val_str(x, 0) } } println(' [ip=${v.ip:4}] ${name:-9} stack: [${s}]') diff --git a/vm/types.v b/vm/types.v index 1741078..19d85de 100644 --- a/vm/types.v +++ b/vm/types.v @@ -1,6 +1,8 @@ // types.v — core types and constants for the VuurRaaf VM. module vm +import obj + const stack_cap = 65536 // Field is one `name: value` entry of a struct value. @@ -15,18 +17,38 @@ mut: fields []Field } +// Handler records a try/catch handler pushed at runtime. +struct Closure { + entry int // code IP of the function body +} + +struct Handler { + ip int // catch_ip + bp int // frame base at try point + sp int // stack pointer right after the handler record +} + struct Vm { mut: - code []u8 - strings []string - arrays [][]i64 - structs []StructVal - stack []i64 - sp int - bp int - ip int - trace bool - halted bool + code []u8 + strings []string + arrays [][]i64 + structs []StructVal + floats []f64 + closures []Closure + stack []i64 + sp int + bp int + ip int + trace bool + halted bool + prog_args []string + exit_code i64 + did_exit bool + handlers []Handler + lines []obj.LineInfo // debug info: code offset -> source line + const_strs int // strings[0..const_strs] are bytecode constants, never collected + last_heap int // heap size at the last GC check (allocation trigger) } fn bool_i64(b bool) i64 { diff --git a/vm/value.v b/vm/value.v index f09b8c0..428af6b 100644 --- a/vm/value.v +++ b/vm/value.v @@ -1,49 +1,101 @@ // value.v — tagged value encoding and helpers for the VuurRaaf VM. // -// Stack values are 64-bit tagged integers with two tag bits: -// low bits 00 -> encoded number (value = raw << 2) -// low bits 01 -> string handle (handle = value >> 2, into v.strings) -// low bits 10 -> struct handle (handle = value >> 2, into v.structs) -// low bits 11 -> array handle (handle = value >> 2, into v.arrays) +// Stack values are 64-bit tagged integers with three tag bits: +// low bits 000 -> encoded integer (value = raw << 3) +// low bits 001 -> string handle (handle = value >> 3, into v.strings) +// low bits 010 -> struct handle (handle = value >> 3, into v.structs) +// low bits 011 -> array handle (handle = value >> 3, into v.arrays) +// low bits 100 -> float handle (handle = value >> 3, into v.floats) module vm -fn (mut v Vm) is_str(x i64) bool { - return x & 3 == 1 +const tag_mask = u64(7) +const tag_int = u64(0) +const tag_str = u64(1) +const tag_struct = u64(2) +const tag_arr = u64(3) +const tag_float = u64(4) +const tag_closure = u64(5) + +fn (mut v Vm) tag(x i64) u64 { + return u64(x) & tag_mask } -fn (mut v Vm) is_arr(x i64) bool { - return x & 3 == 3 +fn (mut v Vm) is_int(x i64) bool { + return u64(x) & tag_mask == tag_int +} + +fn (mut v Vm) is_str(x i64) bool { + return u64(x) & tag_mask == tag_str } fn (mut v Vm) is_struct(x i64) bool { - return x & 3 == 2 + return u64(x) & tag_mask == tag_struct +} + +fn (mut v Vm) is_arr(x i64) bool { + return u64(x) & tag_mask == tag_arr +} + +fn (mut v Vm) is_float(x i64) bool { + return u64(x) & tag_mask == tag_float +} + +fn (mut v Vm) is_closure(x i64) bool { + return u64(x) & tag_mask == tag_closure +} + +// is_num reports whether x is an integer (tag 0). Floats are a distinct type. +fn (mut v Vm) is_num(x i64) bool { + return u64(x) & tag_mask == tag_int } fn (mut v Vm) enc_int(x i64) i64 { - return u64(x) << 2 + return i64(u64(x) << 3) } fn (mut v Vm) dec_int(x i64) i64 { - return x >> 2 + return x >> 3 } fn (mut v Vm) hand(x i64) int { - return int(x >> 2) + return int(x >> 3) } fn (mut v Vm) mkstr(idx int) i64 { - return (u64(idx) << 2) | 1 + return i64((u64(idx) << 3) | tag_str) } fn (mut v Vm) mkarr(idx int) i64 { - return (u64(idx) << 2) | 3 + return i64((u64(idx) << 3) | tag_arr) } fn (mut v Vm) mkstruct_handle(idx int) i64 { - return (u64(idx) << 2) | 2 + return i64((u64(idx) << 3) | tag_struct) +} + +fn (mut v Vm) mkfloat(idx int) i64 { + return i64((u64(idx) << 3) | tag_float) +} + +fn (mut v Vm) mkclosure(idx int) i64 { + return i64((u64(idx) << 3) | tag_closure) +} + +// 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 + return v.mkfloat(v.floats.len - 1) +} + +// fval returns the f64 value of a float handle. +fn (mut v Vm) fval(x i64) f64 { + return v.floats[v.hand(x)] } fn (mut v Vm) truthy(x i64) bool { + if v.is_float(x) { + return v.fval(x) != 0.0 + } return x != 0 } @@ -61,3 +113,25 @@ fn (mut v Vm) valid_struct_handle(x i64) bool { h := v.hand(x) return h >= 0 && h < v.structs.len } + +fn (mut v Vm) valid_float_handle(x i64) bool { + h := v.hand(x) + return h >= 0 && h < v.floats.len +} + +fn (mut v Vm) valid_closure_handle(x i64) bool { + h := v.hand(x) + return h >= 0 && h < v.closures.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) { + tag_str { v.valid_handle(x) } + tag_struct { v.valid_struct_handle(x) } + tag_arr { v.valid_arr_handle(x) } + tag_float { v.valid_float_handle(x) } + tag_closure { v.valid_closure_handle(x) } + else { true } + } +} diff --git a/vm/vm.v b/vm/vm.v index ecfe963..837bfea 100644 --- a/vm/vm.v +++ b/vm/vm.v @@ -6,15 +6,25 @@ module vm import obj +import math // run executes the function named `entry` from the executable `bin` and // returns its return value (0 if it never returns one). pub fn run(bin obj.Bin, entry string, trace bool) !i64 { + return run_with_args(bin, entry, trace, []string{}) +} + +// run_with_args is run() with command-line arguments exposed to the program +// via the `args()` builtin. +pub fn run_with_args(bin obj.Bin, entry string, trace bool, args []string) !i64 { mut v := Vm{ - code: bin.code - strings: bin.strings.clone() - stack: []i64{len: stack_cap} - trace: trace + code: bin.code + strings: bin.strings.clone() + stack: []i64{len: stack_cap} + trace: trace + prog_args: args + lines: bin.lines + const_strs: bin.strings.len } mut entry_ip := -1 for f in bin.fns { @@ -38,15 +48,43 @@ pub fn run(bin obj.Bin, entry string, trace bool) !i64 { v.sp++ v.bp = v.sp v.ip = entry_ip - v.exec()! + v.exec() or { + return error('${err.msg()} at ${v.where()}') + } + if v.did_exit { + return v.exit_code + } if v.sp > 0 { return v.dec_int(v.stack[0]) } return 0 } +// where returns a source-level location for the current instruction pointer: +// `line 12 (ip 345)` when debug info is available, otherwise just `(ip 345)`. +fn (v Vm) where() string { + // line table entries are recorded in code order, so walk backwards from + // the most recent entry to find the last one at or before v.ip + for i := v.lines.len - 1; i >= 0; i-- { + if v.ip >= int(v.lines[i].off) { + return 'line ${v.lines[i].line} (ip ${v.ip})' + } + } + return '(ip ${v.ip})' +} + fn (mut v Vm) exec() ! { for !v.halted { + // garbage collection: runs between opcodes when the heap has grown by + // gc_alloc_trigger entries since the last collection, so no live value + // is ever mid-flight in an instruction handler + heap := v.strings.len + v.arrays.len + v.structs.len + v.floats.len + v.closures.len + if heap > v.last_heap + gc_alloc_trigger { + v.collect() + v.last_heap = v.strings.len + v.arrays.len + v.structs.len + v.floats.len + v.closures.len + } else { + v.last_heap = heap + } op := v.code[v.ip] if v.trace { v.trace_op(op) @@ -64,6 +102,11 @@ fn (mut v Vm) exec() ! { idx := int(v.read_i64()) v.push(v.mkstr(idx))! } + op_push_f { + v.ip++ + f := v.read_f64() + v.push(v.push_float(f))! + } op_load { v.ip++ idx := int(v.read_i64()) @@ -117,16 +160,17 @@ fn (mut v Vm) exec() ! { op_neg { v.ip++ a := v.pop()! - if v.is_str(a) { + if v.is_float(a) { + v.push(v.push_float(-v.fval(a)))! + } else if v.is_str(a) { return error('cannot negate a string') - } - if v.is_arr(a) { + } else if v.is_arr(a) { return error('cannot negate an array') - } - if v.is_struct(a) { + } else if v.is_struct(a) { return error('cannot negate a struct') + } else { + v.push(v.enc_int(-v.dec_int(a)))! } - v.push(v.enc_int(-v.dec_int(a)))! } op_eq { v.ip++ @@ -247,14 +291,22 @@ fn (mut v Vm) exec() ! { v.ip++ idx := int(v.dec_int(v.pop()!)) h := v.pop()! - if !v.is_arr(h) || !v.valid_arr_handle(h) { - return error('indexing a non-array value') + if v.is_arr(h) && v.valid_arr_handle(h) { + a := v.arrays[v.hand(h)] + if idx < 0 || idx >= a.len { + return error('array index ${idx} out of bounds (len ${a.len})') + } + v.push(a[idx])! + } else if v.is_str(h) && v.valid_handle(h) { + // rune-based string indexing: s[i] is the i-th character + runes := v.strings[v.hand(h)].runes() + if idx < 0 || idx >= runes.len { + return error('string index ${idx} out of bounds (len ${runes.len})') + } + v.push(v.alloc_str(runes[idx].str()))! + } else { + return error('indexing a non-array, non-string value') } - a := v.arrays[v.hand(h)] - if idx < 0 || idx >= a.len { - return error('array index ${idx} out of bounds (len ${a.len})') - } - v.push(a[idx])! } op_aset { v.ip++ @@ -276,8 +328,10 @@ fn (mut v Vm) exec() ! { v.push(v.enc_int(i64(v.arrays[v.hand(h)].len)))! } else if v.is_struct(h) && v.valid_struct_handle(h) { v.push(v.enc_int(i64(v.structs[v.hand(h)].fields.len)))! + } else if v.is_str(h) && v.valid_handle(h) { + v.push(v.enc_int(i64(v.strings[v.hand(h)].runes().len)))! } else { - return error('len() on a non-array, non-struct value') + return error('len() on a non-array, non-struct, non-string value') } } op_apush { @@ -370,6 +424,139 @@ fn (mut v Vm) exec() ! { op_skeys { v.op_skeys()! } + op_slice { + v.op_slice()! + } + op_native { + v.ip++ + id := int(v.read_i64()) + argc := int(v.read_i64()) + v.native(id, argc)! + } + op_and_b { + v.ip++ + b := v.pop()! + a := v.pop()! + v.push(v.enc_int(v.dec_int(a) & v.dec_int(b)))! + } + op_or_b { + v.ip++ + b := v.pop()! + a := v.pop()! + v.push(v.enc_int(v.dec_int(a) | v.dec_int(b)))! + } + op_xor { + v.ip++ + b := v.pop()! + a := v.pop()! + v.push(v.enc_int(v.dec_int(a) ^ v.dec_int(b)))! + } + op_shl { + v.ip++ + b := v.pop()! + a := v.pop()! + x := v.dec_int(a) + y := u32(v.dec_int(b)) + v.push(v.enc_int(x << y))! + } + op_shr { + v.ip++ + b := v.pop()! + a := v.pop()! + x := v.dec_int(a) + y := u32(v.dec_int(b)) + v.push(v.enc_int(x >> y))! + } + op_not_b { + v.ip++ + a := v.pop()! + v.push(v.enc_int(~v.dec_int(a)))! + } + op_try { + v.ip++ + catch_ip := int(v.read_i64()) + v.handlers << Handler{ ip: catch_ip, bp: v.bp, sp: v.sp } + } + op_throw { + v.ip++ + err_val := v.pop()! + if v.handlers.len == 0 { + return error('unhandled throw: ${v.val_str(err_val, 0)}') + } + h := v.handlers[v.handlers.len - 1] + v.handlers.delete_last() + v.bp = h.bp + v.sp = h.sp + v.push(err_val)! + v.ip = h.ip + } + op_catch_done { + v.ip++ + if v.handlers.len > 0 { + v.handlers.delete_last() + } + } + op_closure { + v.ip++ + entry := int(v.read_i64()) + v.closures << Closure{ entry: entry } + v.push(v.mkclosure(v.closures.len - 1))! + } + op_call_closure { + v.ip++ + argc := int(v.read_i64()) + // stack: [...closure, arg_0, ..., arg_{argc-1}] + h := v.stack[v.sp - argc - 1] + if !v.is_closure(h) || !v.valid_closure_handle(h) { + return error('cannot call a non-function value') + } + entry := v.closures[v.hand(h)].entry + // Shift args left to overwrite the closure slot, then + // insert a return-value placeholder so the callee's + // retv never overwrites the caller's local that held + // the closure handle. + for i := 0; i < argc; i++ { + v.stack[v.sp - argc - 1 + i] = v.stack[v.sp - argc + i] + } + v.stack[v.sp - 1] = v.enc_int(0) // return-value placeholder + v.sp-- // closure was removed; unwind one slot + v.call(entry, argc) + } + op_argc { + v.ip++ + argc := v.dec_int(v.stack[v.bp - 1]) + v.push(v.enc_int(argc))! + } + op_load_dyn { + v.ip++ + idx := int(v.dec_int(v.pop()!)) + if v.bp + idx < 0 || v.bp + idx >= v.sp { + return error('dynamic load index ${idx} out of range') + } + v.push(v.stack[v.bp + idx])! + } + op_varargs { + v.ip++ + named := int(v.read_i64()) + dst := int(v.read_i64()) + argc := int(v.dec_int(v.stack[v.bp - 1])) + mut n := argc - named + if n < 0 { + n = 0 + } + mut arr := []i64{len: n} + for i in 0..n { + arr[i] = v.stack[v.bp + named + i] + } + v.arrays << arr + v.stack[v.bp + dst] = v.mkarr(v.arrays.len - 1) + } + op_str_method { + v.ip++ + sidx := int(v.read_i64()) + argc := int(v.read_i64()) + v.str_method(v.strings[sidx], argc)! + } else { return error('unknown opcode ${op} at ip ${v.ip}') } @@ -453,6 +640,60 @@ fn (mut v Vm) op_skeys() ! { v.push(v.mkarr(v.arrays.len - 1))! } +// op_slice slices an array or string: stack = [value, start, end] → sliced value. +// end == -1 means "open-ended" (slice to the end). +fn (mut v Vm) op_slice() ! { + v.ip++ + end_val := v.dec_int(v.pop()!) + start_val := v.dec_int(v.pop()!) + h := v.pop()! + // --- array slicing --- + if v.is_arr(h) && v.valid_arr_handle(h) { + arr := v.arrays[v.hand(h)] + mut s := if start_val < 0 { 0 } else { int(start_val) } + mut e := if end_val < 0 { arr.len } else { int(end_val) } + if s > arr.len { + s = arr.len + } + if e > arr.len { + e = arr.len + } + if s > e { + e = s + } + mut sliced := []i64{} + for i in s..e { + sliced << arr[i] + } + v.arrays << sliced + v.push(v.mkarr(v.arrays.len - 1))! + return + } + // --- string slicing --- + if v.is_str(h) && v.valid_handle(h) { + src := v.strings[v.hand(h)] + runes := src.runes() + mut s := if start_val < 0 { 0 } else { int(start_val) } + mut e := if end_val < 0 { runes.len } else { int(end_val) } + if s > runes.len { + s = runes.len + } + if e > runes.len { + e = runes.len + } + if s > e { + e = s + } + mut sliced := '' + for i in s..e { + sliced += runes[i].str() + } + v.push(v.alloc_str(sliced))! + return + } + return error('slice() on a non-array, non-string value') +} + fn (mut v Vm) read_i64() i64 { mut val := u64(0) for i in 0..8 { @@ -462,6 +703,15 @@ fn (mut v Vm) read_i64() i64 { return i64(val) } +fn (mut v Vm) read_f64() f64 { + mut val := u64(0) + for i in 0..8 { + val |= u64(v.code[v.ip + i]) << u32(8 * i) + } + v.ip += 8 + return math.f64_from_bits(val) +} + fn (mut v Vm) push(x i64) ! { if v.sp >= v.stack.len { return error('stack overflow')