// 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 import net.http import obj import compiler import assembler import linker import regex import encoding.base64 import crypto.sha256 import crypto.md5 import encoding.csv 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(), by_name: s.by_name.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)) } // ------------------------------------------------------------------- // build-module builtins (.vrmm) — these let a VuurRaaf program drive // the toolchain itself, the way V's .vsh scripts drive `v build`. native_build_compile { out := v.pop_str()! src := v.pop_str()! o := compiler.compile_file(src) or { return error('build_compile: ${err.msg()}') } o_path := if out.len == 0 { src.all_before_last('.') + '.vobj' } else { out } obj.write(o_path, o) or { return error('build_compile: cannot write ${o_path}: ${err.msg()}') } println('compiled ${src} -> ${o_path} (${o.code.len} bytes code, ${o.symbols.len} symbols)') v.push(v.alloc_str(o_path))! } native_build_assemble { out := v.pop_str()! src := v.pop_str()! o := assembler.assemble_file(src) or { return error('build_assemble: ${err.msg()}') } o_path := if out.len == 0 { src.all_before_last('.') + '.vobj' } else { out } obj.write(o_path, o) or { return error('build_assemble: cannot write ${o_path}: ${err.msg()}') } println('assembled ${src} -> ${o_path} (${o.code.len} bytes code)') v.push(v.alloc_str(o_path))! } native_build_link { out := v.pop_str()! h := v.pop()! if !v.is_arr(h) || !v.valid_arr_handle(h) { return error('build_link() expects an array of object files as its first argument') } mut objs := []string{} for x in v.arrays[v.hand(h)] { if !v.is_str(x) || !v.valid_handle(x) { return error('build_link() expects string paths inside the object array') } objs << v.strings[v.hand(x)] } if objs.len == 0 { return error('build_link() needs at least one object file') } o_path := if out.len == 0 { os.base(objs[0]).all_before_last('.') + '.vbin' } else { out } linker.link(objs, o_path) or { return error('build_link: ${err.msg()}') } println('linked ${objs.len} object file(s) -> ${o_path}') v.push(v.alloc_str(o_path))! } native_build_run { f := v.pop_str()! if f.ends_with('.vbin') { bin := obj.read_bin(f) or { return error('build_run: ${err.msg()}') } code := run_with_args(bin, 'main', false, []string{}) or { return error('build_run: ${err.msg()}') } v.push(v.enc_int(code))! return } if !f.ends_with('.vr') { return error('build_run() expects a .vr or .vbin file') } tmp_obj := os.join_path(os.temp_dir(), 'vr_build_${os.getpid()}.vobj') tmp_bin := os.join_path(os.temp_dir(), 'vr_build_${os.getpid()}.vbin') defer { os.rm(tmp_obj) or {} os.rm(tmp_bin) or {} } o := compiler.compile_file(f) or { return error('build_run: ${err.msg()}') } obj.write(tmp_obj, o) or { return error('build_run: ${err.msg()}') } linker.link([tmp_obj], tmp_bin) or { return error('build_run: ${err.msg()}') } bin := obj.read_bin(tmp_bin) or { return error('build_run: ${err.msg()}') } code := run_with_args(bin, 'main', false, []string{}) or { return error('build_run: ${err.msg()}') } v.push(v.enc_int(code))! } native_build_test { src := v.pop_str()! o := compiler.compile_file(src) or { return error('build_test: ${err.msg()}') } mut tests := []string{} for s in o.symbols { if s.name.starts_with('test_') { tests << s.name } } if tests.len == 0 { return error('build_test: no test_* functions found in ${src}') } tmp_obj := os.join_path(os.temp_dir(), 'vr_build_${os.getpid()}.vobj') tmp_bin := os.join_path(os.temp_dir(), 'vr_build_${os.getpid()}.vbin') defer { os.rm(tmp_obj) or {} os.rm(tmp_bin) or {} } obj.write(tmp_obj, o) or { return error('build_test: ${err.msg()}') } linker.link([tmp_obj], tmp_bin) or { return error('build_test: ${err.msg()}') } bin := obj.read_bin(tmp_bin) or { return error('build_test: ${err.msg()}') } mut passes := 0 mut fails := 0 for t in tests { run(bin, t, false) or { eprintln(' FAIL ${t} — ${err}') fails++ continue } println(' PASS ${t}') passes++ } if fails > 0 { return error('build_test: ${fails} of ${tests.len} test(s) failed in ${src}') } println('${passes} test(s) passed') v.push(v.enc_int(i64(passes)))! } native_build_bench { n := int(v.dec_int(v.pop()!)) src := v.pop_str()! if n < 1 { return error('build_bench() expects a positive iteration count') } tmp_obj := os.join_path(os.temp_dir(), 'vr_build_${os.getpid()}.vobj') tmp_bin := os.join_path(os.temp_dir(), 'vr_build_${os.getpid()}.vbin') defer { os.rm(tmp_obj) or {} os.rm(tmp_bin) or {} } o := compiler.compile_file(src) or { return error('build_bench: ${err.msg()}') } obj.write(tmp_obj, o) or { return error('build_bench: ${err.msg()}') } linker.link([tmp_obj], tmp_bin) or { return error('build_bench: ${err.msg()}') } bin := obj.read_bin(tmp_bin) or { return error('build_bench: ${err.msg()}') } start := time.now().unix_milli() for _ in 0..n { run(bin, 'main', false) or { return error('build_bench: ${err.msg()}') } } ms := time.now().unix_milli() - start rate := if ms > 0 { f64(n) / (f64(ms) / 1000.0) } else { f64(0) } println('bench: ${n} runs of main() in ${ms}ms (${rate:.0} runs/s)') v.push(v.enc_int(0))! } native_build_clean { mut n := 0 if files := os.ls('.') { for f in files { if f.ends_with('.vobj') || f.ends_with('.vbin') { os.rm(f) or {} n++ } } } println('cleaned ${n} artifact(s)') v.push(v.enc_int(i64(n)))! } native_build_exec { cmd := v.pop_str()! res := os.execute(cmd) if res.exit_code != 0 { return error('build_exec: "${cmd}" failed with exit code ${res.exit_code}: ${res.output}') } v.push(v.alloc_str(res.output))! } native_build_exec_status { cmd := v.pop_str()! res := os.execute(cmd) v.push(v.enc_int(i64(res.exit_code)))! } native_build_exists { 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()}') } v.push(v.enc_int(0))! } native_build_rm { p := v.pop_str()! if !os.exists(p) { v.push(v.enc_int(0))! return } if os.is_dir(p) { os.rmdir_all(p) or { return error('build_rm: cannot remove ${p}: ${err.msg()}') } } else { os.rm(p) or { return error('build_rm: cannot remove ${p}: ${err.msg()}') } } v.push(v.enc_int(1))! } native_build_copy { dst := v.pop_str()! src := v.pop_str()! if os.is_dir(src) { v.copy_tree(src, dst) or { return error('build_copy: ${err.msg()}') } } else { os.cp(src, dst, os.CopyParams{}) or { return error('build_copy: cannot copy ${src} -> ${dst}: ${err.msg()}') } } v.push(v.enc_int(0))! } native_build_glob { pat := v.pop_str()! files := os.glob(pat) or { return error('build_glob: ${err.msg()}') } mut arr := []i64{} for f in files { v.strings << f arr << v.mkstr(v.strings.len - 1) } v.arrays << arr v.push(v.mkarr(v.arrays.len - 1))! } native_build_ls { dir := v.pop_str()! entries := os.ls(dir) or { return error('build_ls: cannot read ${dir}: ${err.msg()}') } mut arr := []i64{} for f in entries { v.strings << f arr << v.mkstr(v.strings.len - 1) } v.arrays << arr v.push(v.mkarr(v.arrays.len - 1))! } native_build_base { p := v.pop_str()! v.push(v.alloc_str(os.base(p)))! } native_build_dir { p := v.pop_str()! v.push(v.alloc_str(os.dir(p)))! } native_build_join { b := v.pop_str()! a := v.pop_str()! v.push(v.alloc_str(os.join_path(a, b)))! } native_build_root { v.push(v.alloc_str(v.build_root))! } // ------------------------------------------------------------------- // stdlib: JSON + string formatting native_json_encode { x := v.pop()! s := v.json_encode_value(x, 0) or { return error('json_encode: ${err.msg()}') } v.push(v.alloc_str(s))! } native_json_decode { s := v.pop_str()! val := v.json_parse(s) or { return error('json_decode: ${err.msg()}') } v.push(val)! } native_format { spec := v.pop_str()! x := v.pop()! s := v.format_value(x, spec) or { return error('format: ${err.msg()}') } v.push(v.alloc_str(s))! } native_replace { to := v.pop_str()! from := v.pop_str()! s := v.pop_str()! v.push(v.alloc_str(s.replace(from, to)))! } native_split_lines { s := v.pop_str()! mut arr := []i64{} for ln in s.split_into_lines() { v.strings << ln arr << v.mkstr(v.strings.len - 1) } v.arrays << arr v.push(v.mkarr(v.arrays.len - 1))! } native_pad { width := int(v.dec_int(v.pop()!)) s := v.pop_str()! n := s.runes().len v.push(v.alloc_str(if n < width { s + ' '.repeat(width - n) } else { s }))! } native_pad_left { width := int(v.dec_int(v.pop()!)) s := v.pop_str()! n := s.runes().len v.push(v.alloc_str(if n < width { ' '.repeat(width - n) + s } else { s }))! } native_repeat { n := int(v.dec_int(v.pop()!)) s := v.pop_str()! if n < 0 { return error('repeat() expects a non-negative count') } 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))! } // ------------------------------------------------------------------- // HTTP client native_http_get { url := v.pop_str()! resp := http.get(url) or { return error('http_get: ${err.msg()}') } v.push_http_response(resp)! } native_http_post { data := v.pop_str()! url := v.pop_str()! resp := http.post(url, data) or { return error('http_post: ${err.msg()}') } v.push_http_response(resp)! } // ------------------------------------------------------------------- // date/time native_now { v.push(v.enc_int(time.now().unix()))! } native_time_ms { v.push(v.enc_int(time.now().unix_milli()))! } native_format_time { spec := v.pop_str()! t := v.dec_int(v.pop()!) v.push(v.alloc_str(time.unix(t).custom_format(spec)))! } native_parse_time { s := v.pop_str()! t := time.parse(s) or { return error('parse_time: ${err.msg()}') } v.push(v.enc_int(t.unix()))! } native_weekday { t := v.dec_int(v.pop()!) v.push(v.alloc_str(time.unix(t).weekday_str()))! } // ------------------------------------------------------------------- // regex — RE2-style patterns via V's regex module. Every call compiles // the pattern fresh; the VuurRaaf-level module can cache if it needs to. native_regex_match { s := v.pop_str()! pat := v.pop_str()! mut re := regex.regex_opt(pat) or { return error('regex: ${err.msg()}') } v.push(v.enc_int(bool_i64(re.matches_string(s))))! } native_regex_find_all { s := v.pop_str()! pat := v.pop_str()! mut re := regex.regex_opt(pat) or { return error('regex: ${err.msg()}') } matches := re.find_all_str(s) mut arr := []i64{} for m in matches { v.strings << m arr << v.mkstr(v.strings.len - 1) } v.arrays << arr v.push(v.mkarr(v.arrays.len - 1))! } native_regex_replace { repl := v.pop_str()! s := v.pop_str()! pat := v.pop_str()! mut re := regex.regex_opt(pat) or { return error('regex: ${err.msg()}') } v.push(v.alloc_str(re.replace(s, repl)))! } native_regex_split { s := v.pop_str()! pat := v.pop_str()! mut re := regex.regex_opt(pat) or { return error('regex: ${err.msg()}') } parts := re.split(s) 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))! } // ------------------------------------------------------------------- // base64 + hashes native_base64_encode { s := v.pop_str()! v.push(v.alloc_str(base64.encode_str(s)))! } native_base64_decode { s := v.pop_str()! v.push(v.alloc_str(base64.decode_str(s)))! } native_sha256 { s := v.pop_str()! v.push(v.alloc_str(sha256.hexhash(s)))! } native_md5 { s := v.pop_str()! v.push(v.alloc_str(md5.hexhash(s)))! } native_csv_parse { s := v.pop_str()! mut r := csv.new_reader(s) // read() yields one row ([]string) at a time until EOF mut rows := [][]string{} for { row := r.read() or { break } if row.len == 0 { break } rows << row } mut outer := []i64{} for row in rows { mut inner := []i64{} for cell in row { v.strings << cell inner << v.mkstr(v.strings.len - 1) } v.arrays << inner outer << v.mkarr(v.arrays.len - 1) } v.arrays << outer v.push(v.mkarr(v.arrays.len - 1))! } // ------------------------------------------------------------------- // extended HTTP: any method, custom headers (a {name: value} struct), // and a per-request timeout in milliseconds native_http_req { timeout_ms := int(v.dec_int(v.pop()!)) headers_h := v.pop()! data := v.pop_str()! url := v.pop_str()! method := v.pop_str()! mut h := http.new_header() if v.is_struct(headers_h) && v.valid_struct_handle(headers_h) { for f in v.structs[v.hand(headers_h)].fields { if v.is_str(f.val) && v.valid_handle(f.val) { h.add_custom(f.name, v.strings[v.hand(f.val)]) or {} } } } m := match method.to_upper() { 'GET' { http.Method.get } 'POST' { http.Method.post } 'PUT' { http.Method.put } 'DELETE' { http.Method.delete } 'PATCH' { http.Method.patch } 'HEAD' { http.Method.head } else { return error('http_req: unsupported method "${method}"') } } resp := http.fetch(method: m, url: url, data: data, header: h, read_timeout: i64(timeout_ms) * time.millisecond) or { return error('http_req: ${err.msg()}') } v.push_http_response(resp)! } // ------------------------------------------------------------------- // filesystem path helpers native_path_ext { p := v.pop_str()! v.push(v.alloc_str(os.file_ext(p)))! } native_path_abs { p := v.pop_str()! v.push(v.alloc_str(os.abs_path(p)))! } native_path_rel { base := v.pop_str()! p := v.pop_str()! v.push(v.alloc_str(v.rel_path(p, base)))! } // ------------------------------------------------------------------- // process spawn with separate stdout/stderr pipes native_exec_full { cmd := v.pop_str()! mut code := 0 mut out := '' mut errs := '' if os.user_os() == 'windows' { res := os.execute(cmd) code = res.exit_code out = res.output } else { mut p := os.new_process('sh') p.set_args(['-c', cmd]) p.use_stdio_ctl = true p.run() p.wait() if p.err.len > 0 { return error('exec_full: ${p.err}') } code = p.code out = p.stdout_slurp() errs = p.stderr_slurp() } out_h := v.alloc_str(out) err_h := v.alloc_str(errs) mut fields := []Field{len: 3} fields[0] = Field{ name: 'code', val: v.enc_int(i64(code)) } fields[1] = Field{ name: 'stdout', val: out_h } fields[2] = Field{ name: 'stderr', val: err_h } v.structs << StructVal{ fields: fields, by_name: v.index_fields(fields) } v.push(v.mkstruct_handle(v.structs.len - 1))! } else { return error('unknown native builtin ${id}') } } } // rel_path computes a relative path from `base` to `p` (both absolutized), // e.g. rel_path("/a/b/c.txt", "/a") == "b/c.txt". Used by os.rel(). fn (v Vm) rel_path(p string, base string) string { ap := os.abs_path(p).replace('\\', '/') ab := os.abs_path(base).replace('\\', '/') if ap == ab { return '.' } aparts := ap.split('/') bparts := ab.split('/') mut i := 0 for i < aparts.len && i < bparts.len && aparts[i] == bparts[i] { i++ } mut out := []string{} for _ in i..bparts.len { out << '..' } for j in i..aparts.len { out << aparts[j] } if out.len == 0 { return '.' } return out.join('/') } // push_http_response wraps an HTTP response as a {status, body} struct value // so scripts can read res.status and res.body. fn (mut v Vm) push_http_response(resp http.Response) ! { body_h := v.alloc_str(resp.body) mut fields := []Field{len: 2} fields[0] = Field{ name: 'status', val: v.enc_int(i64(resp.status_code)) } fields[1] = Field{ name: 'body', val: body_h } v.structs << StructVal{ fields: fields, by_name: v.index_fields(fields) } v.push(v.mkstruct_handle(v.structs.len - 1))! } // --------------------------------------------------------------------------- // format() — a small printf-style formatter: %d %i %f %s %x %X %% with // optional width, left-align (-), zero-padding (0), and precision (.N). fn (mut v Vm) format_value(x i64, spec string) !string { if spec.len < 2 || spec[0] != `%` { return error('expected a printf-style spec like "%d" or "%.2f", got "${spec}"') } mut i := 1 mut left := false mut zero := false if i < spec.len && spec[i] == `-` { left = true i++ } if i < spec.len && spec[i] == `0` { zero = true i++ } mut width := 0 for i < spec.len && spec[i] >= `0` && spec[i] <= `9` { width = width * 10 + int(spec[i] - `0`) i++ } mut prec := -1 if i < spec.len && spec[i] == `.` { i++ prec = 0 for i < spec.len && spec[i] >= `0` && spec[i] <= `9` { prec = prec * 10 + int(spec[i] - `0`) i++ } } if i >= spec.len { return error('incomplete format spec "${spec}"') } conv := spec[i] mut core := '' match conv { `d`, `i` { num := if v.is_float(x) { i64(v.fval(x)) } else { v.dec_int(x) } core = num.str() } `f` { f := if v.is_float(x) { v.fval(x) } else { f64(v.dec_int(x)) } core = v.format_fixed(f, if prec < 0 { 6 } else { prec }) } `s` { core = v.val_str(x, 0) if prec >= 0 && core.len > prec { core = core[..prec] } } `x` { core = u64(v.dec_int(x)).hex() } `X` { core = u64(v.dec_int(x)).hex().to_upper() } `%` { return '%' } else { return error('unsupported conversion "%${conv.ascii_str()}" (supported: %d %i %f %s %x %X %%)') } } if core.len < width { n := width - core.len if left { core += ' '.repeat(n) } else if zero && core.starts_with('-') { core = '-' + '0'.repeat(n) + core[1..] } else if zero { core = '0'.repeat(n) + core } else { core = ' '.repeat(n) + core } } return core } // format_fixed renders an f64 in fixed-point notation with `prec` decimals // (rounding), the way printf's %f does. fn (mut v Vm) format_fixed(f f64, prec int) string { mut p := prec if p < 0 { p = 0 } if p > 20 { p = 20 } if math.is_nan(f) { return 'NaN' } if math.is_inf(f, 1) { return 'Inf' } if math.is_inf(f, -1) { return '-Inf' } neg := f < 0.0 af := math.abs(f) // beyond ~15 digits a float no longer carries exact decimal places, so // fall back to the friendliest available rendering if af >= 1e15 { return fmt_float(f) } scale := math.pow(10.0, f64(p)) r := math.round(af * scale) i := i64(r / scale) core := i.str() if p == 0 { return if neg { '-' + core } else { core } } d := i64(math.round(math.fmod(r, scale))) mut ds := d.str() if ds.len < p { ds = '0'.repeat(p - ds.len) + ds } return (if neg { '-' } else { '' }) + core + '.' + ds } // copy_tree recursively copies a directory tree (used by build_copy). fn (mut v Vm) copy_tree(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) { v.copy_tree(sp, dp)! } else if os.is_file(sp) { os.cp(sp, dp, os.CopyParams{}) or { return error('cannot copy ${sp}: ${err.msg()}') } } } } // 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 } 'replace' { native_replace } 'split_lines' { native_split_lines } 'pad' { native_pad } 'pad_left' { native_pad_left } 'repeat' { native_repeat } 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) }