diff --git a/compiler/ast.v b/compiler/ast.v index 4b42c70..f04fc58 100644 --- a/compiler/ast.v +++ b/compiler/ast.v @@ -122,7 +122,8 @@ pub mut: pub struct ImportDecl { pub mut: - path string + path string // the file to load (module name for bare `import os`) + name string // module name ('' for quoted file imports: flat merge) line int } diff --git a/compiler/check.v b/compiler/check.v index 8332167..fd2a101 100644 --- a/compiler/check.v +++ b/compiler/check.v @@ -46,7 +46,8 @@ mut: enums map[string][]string consts map[string]TypeInfo loop_depth int - checked map[string]bool // imported files already checked + checked map[string]bool // imported files already checked + modules map[string]bool // imported module names (bare `import os`) } // check validates a parsed program and returns an error on the first problem. @@ -76,7 +77,10 @@ fn check(prog Program) ! { } // imported files are checked (and their symbols merged) recursively for imp in prog.imports { - c.check_import(imp.path)! + if imp.name.len > 0 { + c.modules[imp.name] = true + } + c.check_import(imp.path, imp.name)! } for fd in prog.fns { c.check_fn(fd)! @@ -93,7 +97,7 @@ fn def_count(fd FnDecl) int { return n } -fn (mut c Checker) check_import(path string) ! { +fn (mut c Checker) check_import(path string, mod_name string) ! { if path in c.checked { return } @@ -101,7 +105,9 @@ fn (mut c Checker) check_import(path string) ! { 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 + // merge declarations from the import (bare modules register their + // functions under both the bare name and the "mod.fn" name, so internal + // calls check against the former and program calls against the latter) for sd in prog.structs { if sd.name !in c.structs { c.structs[sd.name] = sd.fields @@ -121,9 +127,15 @@ fn (mut c Checker) check_import(path string) ! { 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 } } + if mod_name.len > 0 { + key := '${mod_name}.${fd.name}' + if key !in c.fns { + c.fns[key] = 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)! + c.check_import(imp.path, imp.name)! } for fd in prog.fns { c.check_fn(fd)! @@ -324,16 +336,35 @@ fn (mut c Checker) check_expr(e Expr) !TypeInfo { } TypeInfo{ kind: .unknown } } - .method_call { - recv := c.check_expr(*e.left)! - if recv.kind == .int_t || recv.kind == .float_t || recv.kind == .bool_t || recv.kind == .none_t { - return error('cannot call a method on a ${type_name(recv.kind)} (line ${e.line})') - } + .method_call { + // module call: os.exists(x) — the receiver is an imported module name + if e.left.kind == .ident && e.left.name in c.modules { for a in e.args { _ = c.check_expr(a)! } - TypeInfo{ kind: .unknown } + key := '${e.left.name}.${e.name}' + if key in c.fns { + sig := c.fns[key] + if !sig.variadic { + if e.args.len < sig.min_args { + return error('${key}() 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('${key}() expects at most ${sig.has_defs.len} argument(s), got ${e.args.len} (line ${e.line})') + } + } + } + return TypeInfo{ kind: .unknown } } + recv := c.check_expr(*e.left)! + if recv.kind == .int_t || recv.kind == .float_t || recv.kind == .bool_t || recv.kind == .none_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)! @@ -560,6 +591,8 @@ fn builtin_result_type(name string) TypeInfo { // stdlib: JSON + string formatting 'json_encode', 'format', 'replace', 'pad', 'pad_left', 'repeat' { TypeInfo{ kind: .string_t } } 'json_decode', 'split_lines' { TypeInfo{ kind: .unknown } } + 'cwd', 'json_pretty' { TypeInfo{ kind: .string_t } } + 'build_is_dir' { TypeInfo{ kind: .int_t } } // build-module builtins (.vrmm) 'build_compile', 'build_assemble', 'build_link', 'build_exec', 'build_base', 'build_dir', 'build_join', 'build_root' { TypeInfo{ kind: .string_t } } diff --git a/compiler/codegen.v b/compiler/codegen.v index 4d56de7..3d7d789 100644 --- a/compiler/codegen.v +++ b/compiler/codegen.v @@ -43,6 +43,7 @@ mut: loops []LoopCtx enter_off u32 next_lbl int + modules map[string]bool // imported module names (bare `import os`) } fn gen(prog Program) !obj.Obj { @@ -78,22 +79,41 @@ fn gen(prog Program) !obj.Obj { } g.structs[sd.name] = sd.fields } - // compile imported files and merge their objects + // compile imported files and merge their objects. Bare module imports + // (`import os`) prefix the module's function symbols and internal call + // relocations with "os.", so programs call os.exists(...) and modules can + // never collide with each other or with the program's own functions. for imp in prog.imports { + mod_name := imp.name + prefix := if mod_name.len > 0 { mod_name + '.' } else { '' } imported := compile_file(resolve_import(imp.path)!)! - // merge symbols from the imported object + if mod_name.len > 0 { + g.modules[mod_name] = true + } + // the imported object's own symbol names (for rewriting call sites) + mut own := map[string]bool{} for s in imported.symbols { - g.symbols << s + own[s.name] = true + } + // append imported bytecode first so symbol entries can be rebased + code_off := g.code.len + g.code << imported.code + // merge symbols from the imported object (prefixed and rebased: entries + // are relative to the imported code, which now sits at code_off) + for s in imported.symbols { + g.symbols << obj.Symbol{ name: prefix + s.name, entry: code_off + s.entry } } // merge strings for s in imported.strings { g.strings << s } - // append imported bytecode and adjust relocations - code_off := g.code.len - g.code << imported.code + // adjust relocations (and prefix module-internal call targets) for r in imported.relocs { - g.relocs << obj.Reloc{ offset: u32(code_off) + r.offset, name: r.name, kind: r.kind } + mut rname := r.name + if r.kind == 0 && prefix.len > 0 && r.name in own { + rname = prefix + r.name + } + g.relocs << obj.Reloc{ offset: u32(code_off) + r.offset, name: rname, kind: r.kind } } // merge debug info, rebasing offsets into this object's code space for l in imported.lines { @@ -190,12 +210,14 @@ fn (mut g Gen) gen_fn(fd FnDecl) ! { // 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 + // resolve intra-function jump targets. Targets are encoded PC-relative + // (delta from the end of the 8-byte operand), so bytecode stays + // position-independent when module objects are merged or linked. for f in g.fixups { target := g.labels[f.name] or { return error('internal error: unresolved label ${f.name} in fn ${fd.name}') } - obj.patch_i64(mut g.code, f.off, i64(target)) + obj.patch_i64(mut g.code, f.off, i64(target - (int(f.off) + 8))) } g.fixups.clear() g.labels.clear() @@ -603,13 +625,24 @@ fn (mut g Gen) gen_expr(e Expr) ! { g.emit_field_name(e.name) g.code << op_sget } - .method_call { - // p.dist(x) → call .dist p, x - 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 { + .method_call { + // module call: os.exists(x) — the receiver is an imported module name + if e.left.kind == .ident && e.left.name in g.modules { + for a in e.args { + g.gen_expr(a)! + } + g.code << op_call + g.code << obj.encode_i64(0) // placeholder — patched by the linker + g.relocs << obj.Reloc{ offset: u32(g.code.len) - 8, name: '${e.left.name}.${e.name}', kind: 0 } + g.code << obj.encode_i64(i64(e.args.len)) + return + } + // p.dist(x) → call .dist p, x + 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)! @@ -713,8 +746,9 @@ fn (mut g Gen) gen_expr(e Expr) ! { 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)) + // Patch the skip jump to land at the closure opcode we emit next + // (PC-relative, like all other jump targets). + obj.patch_i64(mut g.code, skip_fix_off, i64(g.code.len - (int(skip_fix_off) + 8))) g.code << op_closure g.code << obj.encode_i64(0) g.relocs << obj.Reloc{ offset: u32(g.code.len) - 8, name: name, kind: 0 } @@ -830,10 +864,12 @@ 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 + // closure call: ident(args) where ident is a local holding a closure. + // The local's value is pushed as the call sequence's first slot; + // op_call_closure consumes it along with the args, leaving only the + // result on the stack. 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)! } @@ -935,6 +971,9 @@ fn builtin_spec(name string) (int, int) { 'pad' { native_pad, 2 } 'pad_left' { native_pad_left, 2 } 'repeat' { native_repeat, 2 } + 'build_is_dir' { native_build_is_dir, 1 } + 'cwd' { native_cwd, 0 } + 'json_pretty' { native_json_pretty, 1 } else { -1, 0 } } } @@ -1166,6 +1205,7 @@ fn (mut g Gen) expr_type(e Expr) string { 'build_compile', 'build_assemble', 'build_link', 'build_exec', 'build_base', 'build_dir', 'build_join', 'build_root' { 'string' } 'json_encode', 'format', 'replace', 'pad', 'pad_left', 'repeat' { 'string' } + 'cwd', 'json_pretty' { 'string' } else { '' } } } diff --git a/compiler/compiler.v b/compiler/compiler.v index e1db6f0..da3f70d 100644 --- a/compiler/compiler.v +++ b/compiler/compiler.v @@ -4,6 +4,10 @@ module compiler import os import obj +// stdlib_dir is the toolchain's bundled library (lib/*.vr), resolved at build +// time so `import os` works from any working directory. +const stdlib_dir = @VMODROOT + '/lib' + // compile parses and compiles VuurRaaf source into an object file. pub fn compile(src string) !obj.Obj { toks := tokenize(src)! @@ -19,16 +23,19 @@ pub fn compile_file(path string) !obj.Obj { } // 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. +// the path as given first, then the package-manager layout (vendor/) and the +// toolchain's own standard library (lib/), so both `import "pkg/file.vr"` +// and the bare module form `import os` resolve from anywhere. 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'] { + for cand in ['vendor/${path}', 'vendor/${path}.vr', 'vendor/${path}/main.vr', + 'vendor/${path}/src/main.vr', os.join_path(stdlib_dir, path + '.vr'), + os.join_path(stdlib_dir, path, 'main.vr')] { if os.exists(cand) { return cand } } - return error('cannot resolve import "${path}" (tried vendor/)"') + return error('cannot resolve import "${path}" (tried vendor/ and lib/)') } diff --git a/compiler/opcodes.v b/compiler/opcodes.v index c80133a..7b2373f 100644 --- a/compiler/opcodes.v +++ b/compiler/opcodes.v @@ -138,3 +138,6 @@ const native_split_lines = 161 const native_pad = 162 const native_pad_left = 163 const native_repeat = 164 +const native_build_is_dir = 165 +const native_cwd = 166 +const native_json_pretty = 167 diff --git a/compiler/parser.v b/compiler/parser.v index e01a076..699dab5 100644 --- a/compiler/parser.v +++ b/compiler/parser.v @@ -2,7 +2,7 @@ // // Grammar (informal): // program := import* (struct | enum | const | fn)* -// import := 'import' STRING +// import := 'import' STRING | 'import' IDENT // struct := 'struct' IDENT '{' [IDENT (',' IDENT)*] '}' // enum := 'enum' IDENT '{' [IDENT (',' IDENT)*] '}' // const := 'const' IDENT '=' expr @@ -123,11 +123,16 @@ fn (mut p Parser) parse_struct_decl() !StructDecl { return StructDecl{ name: name.lit, fields: fields, line: t.line } } -// parse_import parses `import "path/to/file.vr"`. +// parse_import parses `import "path/to/file.vr"` (flat file merge) or a +// bare module name like `import os` (namespaced stdlib/vendor module). fn (mut p Parser) parse_import() !ImportDecl { t := p.expect(.kw_import, "'import'")! - path := p.expect(.str_lit, 'import path')! - return ImportDecl{ path: path.lit, line: t.line } + if p.cur().kind == .str_lit { + path := p.advance().lit + return ImportDecl{ path: path, line: t.line } + } + name := p.expect(.ident, 'module name')!.lit + return ImportDecl{ name: name, path: name, line: t.line } } // parse_enum_decl parses `enum Name { variant1 variant2 ... }`. diff --git a/examples/imports.vr b/examples/imports.vr new file mode 100644 index 0000000..69e2821 --- /dev/null +++ b/examples/imports.vr @@ -0,0 +1,44 @@ +// imports.vr — the bundled standard library, imported by name like in V. +// vr run examples/imports.vr +// +// The toolchain ships lib/*.vr; `import os` resolves against it (and vendor/) +// from any working directory. Module functions are namespaced: os.exists(), +// json.encode(), strings.join(), math.clamp(). + +import os +import json +import strings +import math + +fn main() { + // --- os: filesystem helpers --- + println("cwd: " + os.cwd()) + println("lib has json.vr: " + str(os.exists("lib/json.vr"))) + for f in os.list_dir("lib") { + println(" lib/" + f) + } + + // --- json: encode / decode / pretty --- + let user = { + name: "amy", + age: 30, + score: 87.5, + tags: ["admin", "beta"], + active: 1 + } + println(json.pretty(user)) + + let back = json.decode(json.encode(user)) + println("decoded back: " + back.name + ", " + str(back.age)) + + // --- strings: namespaced string API --- + println(strings.join(["one", "two", "three"], ", ")) + println(strings.capitalize("vuurraaf")) + println(strings.pad_left("42", 5) + "|") + println(strings.repeat("ab", 3)) + + // --- math --- + println(str(math.clamp(150, 0, 100))) + println(str(math.sign(-42))) + println(strings.format(math.pi(), "%.4f")) +} diff --git a/examples/tests.vr b/examples/tests.vr index 12119da..2c05d09 100644 --- a/examples/tests.vr +++ b/examples/tests.vr @@ -1,6 +1,11 @@ // tests.vr — functions named test_* are picked up by `vr test`. // vr test examples/tests.vr +import os +import json +import strings +import math + fn add(a, b) { return a + b } @@ -292,6 +297,43 @@ fn test_none() { assert not (none == 1) } +fn test_module_imports() { + // os module + if not os.exists("examples") { + assert false + } + if not os.is_dir("examples") { + assert false + } + if not os.is_file("main.v") { + assert false + } + assert os.base("a/b/c.vr") == "c.vr" + assert os.dir("a/b/c.vr") == "a/b" + assert len(os.glob("lib/*.vr")) == 4 + let tmp = os.join(os.cwd(), "tmp_test.txt") + os.write_lines(tmp, ["x", "y"]) + assert len(os.read_lines(tmp)) == 2 + os.remove(tmp) + + // json module + let data = json.decode("{\"a\": 1, \"b\": [2, 3]}") + assert json.encode(data) == "{\"a\":1,\"b\":[2,3]}" + assert strings.contains(json.pretty(data), "\n") + + // strings module + assert strings.join(["a", "b"], "-") == "a-b" + assert strings.capitalize("hello") == "Hello" + assert strings.replace("foo", "o", "0") == "f00" + assert strings.pad_left("7", 3) == " 7" + assert strings.format(3.14159, "%.2f") == "3.14" + + // math module + assert math.clamp(150, 0, 100) == 100 + assert math.sign(-3) == -1 + assert math.abs(-7) == 7 +} + fn test_native_errors_are_catchable() { try { read_file("/no/such/file.vr") @@ -307,6 +349,38 @@ fn test_native_errors_are_catchable() { } } +fn test_closures() { + // closures in expression contexts must not corrupt surrounding values + let pick = fn (x) { + if x > 10 { + return "big" + } + return "small" + } + assert "a: " + pick(50) == "a: big" + assert pick(3) + "!" == "small!" + assert pick(50) + " " + pick(3) == "big small" + let dbl = fn (x) { + return x * 2 + } + assert str(dbl(21)) == "42" + assert "v=" + str(dbl(4)) == "v=8" + // closure containing a loop + let sum = fn () { + let total = 0 + for i in 0..5 { + total = total + i + } + return total + } + assert sum() == 10 + // closure call as a statement argument + let tag = fn (t) { + return "[" + t + "]" + } + assert tag("warn") == "[warn]" +} + fn test_failing() { // this one is meant to fail — shows up in `vr test` output assert 1 == 2 diff --git a/lib/json.vr b/lib/json.vr new file mode 100644 index 0000000..fa17305 --- /dev/null +++ b/lib/json.vr @@ -0,0 +1,18 @@ +// json.vr — JSON helpers (part of the VuurRaaf stdlib). +// +// import json +// let data = json.decode("{\"a\": 1}") +// println(json.encode(data)) +// println(json.pretty(data)) + +fn encode(x) { + return json_encode(x) +} + +fn decode(s) { + return json_decode(s) +} + +fn pretty(x) { + return json_pretty(x) +} diff --git a/lib/math.vr b/lib/math.vr new file mode 100644 index 0000000..a51ddfa --- /dev/null +++ b/lib/math.vr @@ -0,0 +1,61 @@ +// math.vr — math helpers (part of the VuurRaaf stdlib). +// +// import math +// println(math.clamp(150, 0, 100)) +// println(math.sign(-3)) + +fn abs(x) { + return abs(x) +} + +fn min(a, b) { + return min(a, b) +} + +fn max(a, b) { + return max(a, b) +} + +fn floor(x) { + return floor(x) +} + +fn ceil(x) { + return ceil(x) +} + +fn round(x) { + return round(x) +} + +fn sqrt(x) { + return sqrt(x) +} + +fn pow(a, b) { + return pow(a, b) +} + +fn clamp(x, lo, hi) { + if x < lo { + return lo + } + if x > hi { + return hi + } + return x +} + +fn sign(x) { + if x < 0 { + return -1 + } + if x > 0 { + return 1 + } + return 0 +} + +fn pi() { + return 3.14159 +} diff --git a/lib/os.vr b/lib/os.vr new file mode 100644 index 0000000..5830bc2 --- /dev/null +++ b/lib/os.vr @@ -0,0 +1,75 @@ +// os.vr — operating system and filesystem helpers (part of the VuurRaaf stdlib). +// +// import os +// if os.exists("build.vrmm") { +// for f in os.list_dir(".") { +// println(f) +// } +// } + +fn exists(path) { + return build_exists(path) +} + +fn is_dir(path) { + return build_is_dir(path) +} + +fn is_file(path) { + return build_exists(path) and not build_is_dir(path) +} + +fn mkdir(path) { + build_mkdir(path) + return 0 +} + +fn remove(path) { + return build_rm(path) +} + +fn copy(src, dst) { + build_copy(src, dst) + return 0 +} + +fn list_dir(path) { + return build_ls(path) +} + +fn glob(pattern) { + return build_glob(pattern) +} + +fn join(a, b) { + return build_join(a, b) +} + +fn base(path) { + return build_base(path) +} + +fn dir(path) { + return build_dir(path) +} + +fn read_lines(path) { + return split_lines(read_file(path)) +} + +fn write_lines(path, lines) { + write_file(path, join(lines, "\n") + "\n") + return 0 +} + +fn cwd() { + return cwd() +} + +fn env(name) { + return getenv(name) +} + +fn exec(cmd) { + return build_exec(cmd) +} diff --git a/lib/strings.vr b/lib/strings.vr new file mode 100644 index 0000000..38e8d9e --- /dev/null +++ b/lib/strings.vr @@ -0,0 +1,72 @@ +// strings.vr — string helpers (part of the VuurRaaf stdlib). +// +// import strings +// println(strings.join(["a", "b"], ", ")) +// println(strings.capitalize("hello")) + +fn length(s) { + return len(s) +} + +fn lines(s) { + return split_lines(s) +} + +fn split(s, delim) { + return split(s, delim) +} + +fn join(parts, delim) { + return join(parts, delim) +} + +fn replace(s, from, to) { + return replace(s, from, to) +} + +fn contains(s, sub) { + return contains(s, sub) +} + +fn starts_with(s, prefix) { + return starts_with(s, prefix) +} + +fn ends_with(s, suffix) { + return ends_with(s, suffix) +} + +fn upper(s) { + return upper(s) +} + +fn lower(s) { + return lower(s) +} + +fn trim(s) { + return trim(s) +} + +fn pad(s, width) { + return pad(s, width) +} + +fn pad_left(s, width) { + return pad_left(s, width) +} + +fn repeat(s, n) { + return repeat(s, n) +} + +fn format(x, spec) { + return format(x, spec) +} + +fn capitalize(s) { + if len(s) == 0 { + return s + } + return upper(s[0]) + s[1..] +} diff --git a/vm/json.v b/vm/json.v index 825c120..b04dfe1 100644 --- a/vm/json.v +++ b/vm/json.v @@ -421,6 +421,52 @@ fn (mut v Vm) json_encode_value(x i64, depth int) !string { return v.dec_int(x).str() } +// json_pretty_value renders a value as indented, multi-line JSON (the +// `json_pretty` builtin used by the json stdlib module). +fn (mut v Vm) json_pretty_value(x i64, depth int) !string { + ind := ' '.repeat(depth) + ind1 := ' '.repeat(depth + 1) + if v.is_none(x) { + return 'null' + } + if v.is_str(x) && v.valid_handle(x) { + return json_quote(v.strings[v.hand(x)]) + } + if v.is_arr(x) && v.valid_arr_handle(x) { + a := v.arrays[v.hand(x)] + if a.len == 0 { + return '[]' + } + mut parts := []string{} + for el in a { + parts << ind1 + v.json_pretty_value(el, depth + 1)! + } + return '[\n' + parts.join(',\n') + '\n' + ind + ']' + } + if v.is_struct(x) && v.valid_struct_handle(x) { + s := v.structs[v.hand(x)] + if s.fields.len == 0 { + return '{}' + } + mut parts := []string{} + for f in s.fields { + parts << ind1 + json_quote(f.name) + ': ' + v.json_pretty_value(f.val, depth + 1)! + } + return '{\n' + parts.join(',\n') + '\n' + ind + '}' + } + if v.is_float(x) && v.valid_float_handle(x) { + f := v.fval(x) + if math.is_nan(f) || math.is_inf(f, 1) || math.is_inf(f, -1) { + return error('cannot encode NaN or Infinity as JSON') + } + return fmt_float(f) + } + if v.is_closure(x) && v.valid_closure_handle(x) { + return error('cannot encode a function value as JSON') + } + return v.dec_int(x).str() +} + // json_quote escapes a string into a JSON string literal. UTF-8 bytes pass // through untouched; control characters become \\u00XX escapes. fn json_quote(s string) string { diff --git a/vm/native.v b/vm/native.v index 3f82b3e..0d2e338 100644 --- a/vm/native.v +++ b/vm/native.v @@ -513,6 +513,10 @@ fn (mut v Vm) native(id int, _argc int) ! { p := v.pop_str()! v.push(v.enc_int(bool_i64(os.exists(p))))! } + native_build_is_dir { + p := v.pop_str()! + v.push(v.enc_int(bool_i64(os.is_dir(p))))! + } native_build_mkdir { p := v.pop_str()! os.mkdir_all(p) or { return error('build_mkdir: cannot create ${p}: ${err.msg()}') } @@ -635,6 +639,14 @@ fn (mut v Vm) native(id int, _argc int) ! { } v.push(v.alloc_str(s.repeat(n)))! } + native_cwd { + v.push(v.alloc_str(os.getwd()))! + } + native_json_pretty { + x := v.pop()! + s := v.json_pretty_value(x, 0) or { return error('json_pretty: ${err.msg()}') } + v.push(v.alloc_str(s))! + } else { return error('unknown native builtin ${id}') } diff --git a/vm/opcodes.v b/vm/opcodes.v index 823f7d8..d24a644 100644 --- a/vm/opcodes.v +++ b/vm/opcodes.v @@ -137,3 +137,6 @@ const native_split_lines = 161 const native_pad = 162 const native_pad_left = 163 const native_repeat = 164 +const native_build_is_dir = 165 +const native_cwd = 166 +const native_json_pretty = 167 diff --git a/vm/vm.v b/vm/vm.v index c1526db..56bec0b 100644 --- a/vm/vm.v +++ b/vm/vm.v @@ -239,20 +239,22 @@ fn (mut v Vm) exec() ! { } op_jmp { v.ip++ - v.ip = int(v.read_i64()) + // jump targets are PC-relative (delta from the end of the + // operand), so merged/linked bytecode stays position-independent + v.ip += int(v.read_i64()) } op_jz { v.ip++ target := int(v.read_i64()) if !v.truthy(v.pop()!) { - v.ip = target + v.ip += target } } op_jnz { v.ip++ target := int(v.read_i64()) if v.truthy(v.pop()!) { - v.ip = target + v.ip += target } } op_call { @@ -499,7 +501,7 @@ fn (mut v Vm) exec() ! { } op_try { v.ip++ - catch_ip := int(v.read_i64()) + catch_ip := int(v.read_i64()) + v.ip v.handlers << Handler{ ip: catch_ip, bp: v.bp, sp: v.sp } } op_throw { @@ -536,15 +538,15 @@ fn (mut v Vm) exec() ! { 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. + // Shift args left to overwrite the closure slot (and drop the + // duplicated tail), so the callee's retv lands exactly where the + // call sequence began and no stale value is left below the + // result. The caller's own local holding the closure sits below + // the pushed sequence and is never touched. 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.sp-- // drop the duplicated arg tail; closure slot was consumed v.call(entry, argc) } op_argc {