diff --git a/README.MD b/README.MD index 6146072..bc00c6a 100644 --- a/README.MD +++ b/README.MD @@ -260,6 +260,16 @@ fn main() { - bitwise operators: `& | ^ ~ << >>` - host builtins: `read_file` / `write_file`, `args()`, `getenv` / `setenv`, `exit`, `sleep`, `time()`, `type(x)`, `str(x)`, `int(x)`, `split` / `join` +- JSON: `json_encode(x)` / `json_decode(s)` — objects become structs, arrays + become arrays, integral numbers decode as ints, `null` decodes to `none` +- `none`: a literal for "no value" (JSON null); `x == none` compares, + and it renders as `none` / encodes as `null` +- string formatting: `format(x, "%.2f")` (printf-style: `%d %i %f %s %x %X`, + width, `-`/`0` flags, precision), `replace`, `split_lines`, `pad` / + `pad_left`, `repeat` — all also available as string methods (`s.replace()`, + `s.pad(4)`, ...) +- native builtin errors (failed `read_file`, `json_decode`, ...) are caught by + `try { } catch e { }` like explicit `throw`s - 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 @@ -351,5 +361,5 @@ vm/native.v host builtins incl. the build_* (.vrmm) builtins v.mod module definition compiler/ assembler/ linker/ vm/ obj/ the toolchain itself bin/ built binary + standalone tools -examples/ runnable examples (hello, lib, asm, tests, fib) +examples/ runnable examples (hello, lib, asm, tests, fib, json) ``` diff --git a/compiler/ast.v b/compiler/ast.v index e1bdd3b..4b42c70 100644 --- a/compiler/ast.v +++ b/compiler/ast.v @@ -6,6 +6,7 @@ pub enum ExprKind { float_lit str_lit bool_lit + none_lit ident array_lit struct_lit diff --git a/compiler/check.v b/compiler/check.v index b193ae6..8332167 100644 --- a/compiler/check.v +++ b/compiler/check.v @@ -18,6 +18,7 @@ enum CType { float_t string_t bool_t + none_t array_t struct_t enum_t @@ -221,7 +222,7 @@ fn (mut c Checker) check_stmt(st Stmt) ! { .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 { + if seq.kind == .int_t || seq.kind == .float_t || seq.kind == .bool_t || seq.kind == .none_t { return error('cannot iterate a ${type_name(seq.kind)} (line ${st.line})') } c.types[st.target] = TypeInfo{ kind: .unknown } @@ -271,6 +272,7 @@ fn (mut c Checker) check_expr(e Expr) !TypeInfo { .float_lit { TypeInfo{ kind: .float_t } } .str_lit { TypeInfo{ kind: .string_t } } .bool_lit { TypeInfo{ kind: .bool_t } } + .none_lit { TypeInfo{ kind: .none_t } } .ident { if e.name in c.types { c.types[e.name] @@ -324,7 +326,7 @@ fn (mut c Checker) check_expr(e Expr) !TypeInfo { } .method_call { recv := c.check_expr(*e.left)! - if recv.kind == .int_t || recv.kind == .float_t || recv.kind == .bool_t { + 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 { @@ -336,7 +338,7 @@ fn (mut c Checker) check_expr(e Expr) !TypeInfo { 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 { + if base.kind == .int_t || base.kind == .float_t || base.kind == .bool_t || base.kind == .none_t { return error('cannot slice a ${type_name(base.kind)} (line ${e.line})') } if base.kind == .string_t { @@ -424,6 +426,9 @@ fn (mut c Checker) check_binary(e Expr) !TypeInfo { if l.kind == .bool_t && r.kind == .bool_t { return error('cannot order booleans (line ${e.line})') } + if l.kind == .none_t || r.kind == .none_t { + return error('cannot order a none (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})') } @@ -450,7 +455,7 @@ fn (mut c Checker) check_call(e Expr) !TypeInfo { 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 { + if t.kind == .int_t || t.kind == .float_t || t.kind == .bool_t || t.kind == .none_t || t.kind == .closure_t { return error('len() on a ${type_name(t.kind)} (line ${e.line})') } return TypeInfo{ kind: .int_t } @@ -552,6 +557,9 @@ fn builtin_result_type(name string) TypeInfo { 'args', 'keys' { TypeInfo{ kind: .array_t } } 'len' { TypeInfo{ kind: .int_t } } 'write_file', 'setenv', 'exit', 'sleep', 'eprint' { TypeInfo{ kind: .unknown } } + // stdlib: JSON + string formatting + 'json_encode', 'format', 'replace', 'pad', 'pad_left', 'repeat' { TypeInfo{ kind: .string_t } } + 'json_decode', 'split_lines' { TypeInfo{ kind: .unknown } } // build-module builtins (.vrmm) 'build_compile', 'build_assemble', 'build_link', 'build_exec', 'build_base', 'build_dir', 'build_join', 'build_root' { TypeInfo{ kind: .string_t } } @@ -576,30 +584,31 @@ fn type_name(k CType) string { .struct_t { 'struct' } .enum_t { 'enum' } .closure_t { 'function' } + .none_t { 'none' } 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 { + if t.kind == .string_t || t.kind == .array_t || t.kind == .struct_t || t.kind == .bool_t || t.kind == .none_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 { + if t.kind == .string_t || t.kind == .array_t || t.kind == .struct_t || t.kind == .bool_t || t.kind == .none_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 { + if t.kind == .int_t || t.kind == .float_t || t.kind == .bool_t || t.kind == .none_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 { + if t.kind == .int_t || t.kind == .float_t || t.kind == .bool_t || t.kind == .string_t || t.kind == .none_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 b73d5d7..4d56de7 100644 --- a/compiler/codegen.v +++ b/compiler/codegen.v @@ -723,6 +723,9 @@ fn (mut g Gen) gen_expr(e Expr) ! { g.code << op_push_i g.code << obj.encode_i64(e.int_v) } + .none_lit { + g.code << op_push_none + } .ident { // check if it's a constant if e.name in g.consts { @@ -923,6 +926,15 @@ fn builtin_spec(name string) (int, int) { 'build_dir' { native_build_dir, 1 } 'build_join' { native_build_join, 2 } 'build_root' { native_build_root, 0 } + // stdlib: JSON + string formatting + 'json_encode' { native_json_encode, 1 } + 'json_decode' { native_json_decode, 1 } + 'format' { native_format, 2 } + 'replace' { native_replace, 3 } + 'split_lines' { native_split_lines, 1 } + 'pad' { native_pad, 2 } + 'pad_left' { native_pad_left, 2 } + 'repeat' { native_repeat, 2 } else { -1, 0 } } } @@ -1153,6 +1165,7 @@ fn (mut g Gen) expr_type(e Expr) string { 'upper', 'lower', 'trim', 'str', 'getenv', 'read_file', 'join' { 'string' } 'build_compile', 'build_assemble', 'build_link', 'build_exec', 'build_base', 'build_dir', 'build_join', 'build_root' { 'string' } + 'json_encode', 'format', 'replace', 'pad', 'pad_left', 'repeat' { 'string' } else { '' } } } diff --git a/compiler/lexer.v b/compiler/lexer.v index 1066164..5b6f6bc 100644 --- a/compiler/lexer.v +++ b/compiler/lexer.v @@ -291,6 +291,7 @@ fn (mut l Lexer) lex_ident(line int, col int) Tok { 'return' { TokKind.kw_return } 'true' { TokKind.kw_true } 'false' { TokKind.kw_false } + 'none' { TokKind.kw_none } 'and' { TokKind.kw_and } 'or' { TokKind.kw_or } 'not' { TokKind.kw_not } diff --git a/compiler/opcodes.v b/compiler/opcodes.v index 0632579..c80133a 100644 --- a/compiler/opcodes.v +++ b/compiler/opcodes.v @@ -65,6 +65,7 @@ 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), ...) +const op_push_none = u8(62) // push the `none` sentinel // native builtin ids (keep in sync with vm/opcodes.v) const native_abs = 100 @@ -127,3 +128,13 @@ const native_build_base = 153 const native_build_dir = 154 const native_build_join = 155 const native_build_root = 156 + +// stdlib builtins (JSON + string formatting) +const native_json_encode = 157 +const native_json_decode = 158 +const native_format = 159 +const native_replace = 160 +const native_split_lines = 161 +const native_pad = 162 +const native_pad_left = 163 +const native_repeat = 164 diff --git a/compiler/parser.v b/compiler/parser.v index f99984e..e01a076 100644 --- a/compiler/parser.v +++ b/compiler/parser.v @@ -787,6 +787,10 @@ fn (mut p Parser) parse_primary() !Expr { p.advance() return Expr{ kind: .bool_lit, int_v: 0, line: t.line } } + .kw_none { + p.advance() + return Expr{ kind: .none_lit, line: t.line } + } .lparen { p.advance() e := p.parse_expr()! diff --git a/compiler/tokens.v b/compiler/tokens.v index 3a1f201..f0ab8a5 100644 --- a/compiler/tokens.v +++ b/compiler/tokens.v @@ -55,6 +55,7 @@ pub enum TokKind { kw_return kw_true kw_false + kw_none kw_and kw_or kw_not diff --git a/examples/json.vr b/examples/json.vr new file mode 100644 index 0000000..fedde75 --- /dev/null +++ b/examples/json.vr @@ -0,0 +1,46 @@ +// json.vr — the stdlib JSON + formatting builtins. +// vr run examples/json.vr + +fn main() { + // --- encode: structs -> objects, arrays -> arrays, none -> null --- + let user = { + name: "amy", + age: 30, + active: 1, + score: 87.5, + tags: ["admin", "beta"], + meta: { last_seen: none } + } + println(json_encode(user)) + // {"name":"amy","age":30,"active":1,"score":87.5,"tags":["admin","beta"],"meta":{"last_seen":null}} + + // --- decode: objects become structs, numbers become ints when integral --- + let parsed = json_decode("{\"name\": \"bob\", \"age\": 42, \"scores\": [1, 2.5, 3]}") + println(parsed.name) + println(parsed.age) + println(parsed.scores[1]) + println(json_encode(parsed)) + + // null decodes to `none` — test with == + let maybe = json_decode("{\"value\": null}") + if maybe.value == none { + println("field is none") + } + + // --- format: printf-style --- + println(format(3.14159, "%.2f")) // 3.14 + println(format(42, "%05d")) // 00042 + println(format(255, "%X")) // FF + println(format("right", "%10s")) // right + println(format("left", "%-10s") + "|") // left | + + // --- string tools --- + println("hello world".replace("l", "L")) + println(len(split_lines("a\nb\nc"))) + println(pad_left("42", 5) + "|") + println("ab".repeat(3)) + + // --- json + formatting together --- + let total = 1234.5 + println("total: " + format(total, "%.2f")) +} diff --git a/examples/tests.vr b/examples/tests.vr index 1491693..12119da 100644 --- a/examples/tests.vr +++ b/examples/tests.vr @@ -226,6 +226,87 @@ fn test_structs() { assert a != c } +fn test_json() { + // encoding + assert json_encode(42) == "42" + assert json_encode(3.14) == "3.14" + assert json_encode(1.0) == "1" + assert json_encode("hi") == "\"hi\"" + assert json_encode([1, "two", 3.5]) == "[1,\"two\",3.5]" + let rec = { name: "amy", age: 30 } + assert json_encode(rec) == "{\"name\":\"amy\",\"age\":30}" + assert json_encode(none) == "null" + + // decoding + assert json_decode("42") == 42 + assert json_decode("3.14") == 3.14 + assert json_decode("\"hi\"") == "hi" + assert json_decode("true") == 1 + assert json_decode("null") == none + let obj = json_decode("{\"x\": 1, \"y\": [1, 2], \"z\": {\"deep\": true}}") + assert obj.x == 1 + assert obj.y[1] == 2 + assert obj.z.deep == 1 + + // round-trip + assert json_encode(json_decode("{\"a\": 1, \"b\": [2, 3]}")) == "{\"a\":1,\"b\":[2,3]}" + + // unicode escapes + assert json_decode("\"\\u0041\"") == "A" + assert json_decode("\"\\ud83d\\ude00\"") == "😀" +} + +fn test_format() { + assert format(42, "%d") == "42" + assert format(42, "%05d") == "00042" + assert format(-42, "%05d") == "-0042" + assert format(42, "%-5d") == "42 " + assert format(3.14159, "%.2f") == "3.14" + assert format(3.0, "%.2f") == "3.00" + assert format(255, "%X") == "FF" + assert format("hi", "%s") == "hi" + assert format(7, "%5d") == " 7" +} + +fn test_string_tools() { + assert replace("hello world", "l", "L") == "heLLo worLd" + assert "hello world".replace("o", "0") == "hell0 w0rld" + assert len(split_lines("a\nb\nc")) == 3 + assert "a\nb".split_lines()[1] == "b" + assert pad("x", 4) == "x " + assert "x".pad(4) == "x " + assert pad_left("x", 4) == " x" + assert "x".pad_left(4) == " x" + assert repeat("ab", 3) == "ababab" + assert "ab".repeat(2) == "abab" + assert pad("toolong", 2) == "toolong" +} + +fn test_none() { + assert none == none + assert none == 0 == false + assert none != 5 + if none { + assert false + } + assert not (none == 1) +} + +fn test_native_errors_are_catchable() { + try { + read_file("/no/such/file.vr") + assert false + } catch e { + assert len(e) > 0 + } + try { + json_decode("not json") + assert false + } catch e { + assert len(e) > 0 + } +} + fn test_failing() { // this one is meant to fail — shows up in `vr test` output assert 1 == 2 diff --git a/vm/json.v b/vm/json.v new file mode 100644 index 0000000..825c120 --- /dev/null +++ b/vm/json.v @@ -0,0 +1,450 @@ +// json.v — JSON encoding and decoding for the VuurRaaf VM. +// +// The decoder is self-contained (no json2) because json2.decode[Any] can loop +// forever on truncated input; this parser is strictly bounded — every loop +// consumes input, so malformed documents always fail with a position. +// +// Values map 1:1 to the language: objects -> structs (keys in input order, +// last duplicate wins), arrays -> arrays, strings -> strings, numbers -> int +// when integral (42, 42.0) else float, true/false -> 1/0, null -> `none`. +module vm + +import math + +// JsonParser walks a JSON document one byte at a time. +struct JsonParser { +mut: + src string + pos int +} + +// json_parse decodes a whole JSON document into a VM value. +fn (mut v Vm) json_parse(s string) !i64 { + mut p := JsonParser{ src: s } + p.skip_ws() + if p.pos >= p.src.len { + return p.error('empty input') + } + val := v.json_parse_value(mut p)! + p.skip_ws() + if p.pos < p.src.len { + return p.error('trailing data after the value') + } + return val +} + +fn (mut p JsonParser) peek() u8 { + if p.pos >= p.src.len { + return 0 + } + return p.src[p.pos] +} + +fn (mut p JsonParser) skip_ws() { + for p.pos < p.src.len { + c := p.src[p.pos] + if c == ` ` || c == `\t` || c == `\n` || c == `\r` { + p.pos++ + } else { + break + } + } +} + +// error builds a parse error with the current line and column. +fn (mut p JsonParser) error(msg string) IError { + mut line := 1 + mut col := 1 + for i in 0..p.pos { + if p.src[i] == `\n` { + line++ + col = 1 + } else { + col++ + } + } + return error('${msg} at line ${line}, col ${col}') +} + +fn (mut p JsonParser) expect_word(w string) ! { + if p.pos + w.len > p.src.len || p.src[p.pos..p.pos + w.len] != w { + return p.error('invalid token') + } + p.pos += w.len +} + +fn (mut v Vm) json_parse_value(mut p JsonParser) !i64 { + p.skip_ws() + match p.peek() { + `{` { + return v.json_parse_object(mut p)! + } + `[` { + return v.json_parse_array(mut p)! + } + `"` { + return v.alloc_str(p.parse_string()!) + } + `t` { + p.expect_word('true')! + return v.enc_int(1) + } + `f` { + p.expect_word('false')! + return v.enc_int(0) + } + `n` { + p.expect_word('null')! + return none_val + } + else { + c := p.peek() + if (c >= `0` && c <= `9`) || c == `-` { + return v.json_parse_number(mut p)! + } + return p.error('unexpected character "${c.ascii_str()}"') + } + } +} + +fn (mut v Vm) json_parse_object(mut p JsonParser) !i64 { + p.pos++ // consume '{' + mut fields := []Field{} + p.skip_ws() + if p.peek() == `}` { + p.pos++ + v.structs << StructVal{ fields: fields } + return v.mkstruct_handle(v.structs.len - 1) + } + for { + p.skip_ws() + if p.peek() != `"` { + return p.error('expected a string key in object') + } + key := p.parse_string()! + p.skip_ws() + if p.peek() != `:` { + return p.error('expected ":" after object key') + } + p.pos++ + val := v.json_parse_value(mut p)! + // last duplicate key wins (like most JSON parsers) + mut replaced := false + for i in 0..fields.len { + if fields[i].name == key { + fields[i].val = val + replaced = true + break + } + } + if !replaced { + fields << Field{ name: key, val: val } + } + p.skip_ws() + c := p.peek() + if c == `,` { + p.pos++ + continue + } + if c == `}` { + p.pos++ + break + } + return p.error('expected "," or "}" in object') + } + v.structs << StructVal{ fields: fields } + return v.mkstruct_handle(v.structs.len - 1) +} + +fn (mut v Vm) json_parse_array(mut p JsonParser) !i64 { + p.pos++ // consume '[' + mut arr := []i64{} + p.skip_ws() + if p.peek() == `]` { + p.pos++ + v.arrays << arr + return v.mkarr(v.arrays.len - 1) + } + for { + arr << v.json_parse_value(mut p)! + p.skip_ws() + c := p.peek() + if c == `,` { + p.pos++ + continue + } + if c == `]` { + p.pos++ + break + } + return p.error('expected "," or "]" in array') + } + v.arrays << arr + return v.mkarr(v.arrays.len - 1) +} + +fn (mut v Vm) json_parse_number(mut p JsonParser) !i64 { + start := p.pos + if p.peek() == `-` { + p.pos++ + } + mut digits := 0 + for p.pos < p.src.len && p.src[p.pos] >= `0` && p.src[p.pos] <= `9` { + p.pos++ + digits++ + } + if digits == 0 { + return p.error('invalid number') + } + mut is_float := false + if p.pos < p.src.len && p.src[p.pos] == `.` { + is_float = true + p.pos++ + mut fd := 0 + for p.pos < p.src.len && p.src[p.pos] >= `0` && p.src[p.pos] <= `9` { + p.pos++ + fd++ + } + if fd == 0 { + return p.error('invalid number (missing digits after ".")') + } + } + if p.pos < p.src.len && (p.src[p.pos] == `e` || p.src[p.pos] == `E`) { + is_float = true + p.pos++ + if p.pos < p.src.len && (p.src[p.pos] == `+` || p.src[p.pos] == `-`) { + p.pos++ + } + mut ed := 0 + for p.pos < p.src.len && p.src[p.pos] >= `0` && p.src[p.pos] <= `9` { + p.pos++ + ed++ + } + if ed == 0 { + return p.error('invalid number (missing exponent digits)') + } + } + raw := p.src[start..p.pos] + if is_float { + f := raw.f64() + // integral floats become ints so `42` and `42.0` round-trip cleanly + if f == math.floor(f) && math.abs(f) < 1e18 { + return v.enc_int(i64(f)) + } + return v.push_float(f) + } + return v.enc_int(raw.i64()) +} + +fn (mut p JsonParser) parse_string() !string { + p.pos++ // opening quote + mut out := '' + for { + if p.pos >= p.src.len { + return p.error('unterminated string') + } + c := p.src[p.pos] + if c == `"` { + p.pos++ + return out + } + if c == `\\` { + p.pos++ + if p.pos >= p.src.len { + return p.error('unterminated escape sequence') + } + e := p.src[p.pos] + p.pos++ + match e { + `"` { + out += '"' + } + `\\` { + out += '\\' + } + `/` { + out += '/' + } + `b` { + out += '\b' + } + `f` { + out += '\f' + } + `n` { + out += '\n' + } + `r` { + out += '\r' + } + `t` { + out += '\t' + } + `u` { + out += p.parse_unicode_escape()! + } + else { + return p.error('invalid escape "\\${e.ascii_str()}"') + } + } + continue + } + // pass a UTF-8 code point through as raw bytes + width := utf8_width(c) + if width == 0 { + return p.error('invalid UTF-8 byte') + } + out += p.src[p.pos..p.pos + width] + p.pos += width + } + return p.error('unterminated string') +} + +// parse_unicode_escape handles \uXXXX, combining surrogate pairs so emoji and +// astral characters decode correctly. +fn (mut p JsonParser) parse_unicode_escape() !string { + if p.pos + 4 > p.src.len { + return p.error('invalid \\u escape') + } + hi := hex4(p.src[p.pos..p.pos + 4]) or { return p.error('invalid \\u escape') } + p.pos += 4 + mut code := hi + if hi >= 0xD800 && hi <= 0xDBFF { + // high surrogate: combine with an immediately following low surrogate + if p.pos + 6 <= p.src.len && p.src[p.pos] == `\\` && p.src[p.pos + 1] == `u` { + lo := hex4(p.src[p.pos + 2..p.pos + 6]) or { return p.error('invalid \\u escape') } + if lo >= 0xDC00 && lo <= 0xDFFF { + code = 0x10000 + (hi - 0xD800) * 0x400 + (lo - 0xDC00) + p.pos += 6 + } + } + } else if hi >= 0xDC00 && hi <= 0xDFFF { + return p.error('unpaired low surrogate') + } + return utf8_encode(code) +} + +fn hex4(s string) !u32 { + mut n := u32(0) + for i in 0..4 { + c := s[i] + d := match c { + `0`...`9` { u32(c - `0`) } + `a`...`f` { u32(c - `a` + 10) } + `A`...`F` { u32(c - `A` + 10) } + else { return error('bad hex digit "${c.ascii_str()}"') } + } + n = n * 16 + d + } + return n +} + +fn utf8_width(b u8) int { + if b < 0x80 { + return 1 + } + if b >= 0xC0 && b <= 0xDF { + return 2 + } + if b >= 0xE0 && b <= 0xEF { + return 3 + } + if b >= 0xF0 && b <= 0xF7 { + return 4 + } + return 0 +} + +// utf8_encode renders a Unicode code point as UTF-8 bytes. +fn utf8_encode(code u32) string { + if code < 0x80 { + return u8(code).ascii_str() + } + mut bytes := []u8{} + if code < 0x800 { + bytes << u8(0xC0 | (code >> 6)) + bytes << u8(0x80 | (code & 0x3F)) + } else if code < 0x10000 { + bytes << u8(0xE0 | (code >> 12)) + bytes << u8(0x80 | ((code >> 6) & 0x3F)) + bytes << u8(0x80 | (code & 0x3F)) + } else { + bytes << u8(0xF0 | (code >> 18)) + bytes << u8(0x80 | ((code >> 12) & 0x3F)) + bytes << u8(0x80 | ((code >> 6) & 0x3F)) + bytes << u8(0x80 | (code & 0x3F)) + } + return bytes.bytestr() +} + +// --------------------------------------------------------------------------- +// encoding + +// json_encode_value renders a VuurRaaf value as a JSON string. Ints (and +// bools, which are 0/1) become numbers, floats use fmt_float so integral +// floats stay clean, structs become objects, arrays become arrays, and the +// `none` value becomes null. Functions cannot be encoded. +fn (mut v Vm) json_encode_value(x i64, depth int) !string { + if depth > 64 { + return error('value is nested too deeply (possible cycle)') + } + 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) { + mut parts := []string{} + for el in v.arrays[v.hand(x)] { + parts << v.json_encode_value(el, depth + 1)! + } + return '[' + parts.join(',') + ']' + } + if v.is_struct(x) && v.valid_struct_handle(x) { + mut parts := []string{} + for f in v.structs[v.hand(x)].fields { + parts << json_quote(f.name) + ':' + v.json_encode_value(f.val, depth + 1)! + } + return '{' + parts.join(',') + '}' + } + 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 { + hex := '0123456789ABCDEF' + mut out := '"' + for b in s.bytes() { + match b { + `"` { out += '\\"' } + `\\` { out += '\\\\' } + `\n` { out += '\\n' } + `\r` { out += '\\r' } + `\t` { out += '\\t' } + `\b` { out += '\\b' } + `\f` { out += '\\f' } + else { + if b < 0x20 { + out += '\\u00' + out += hex[int(b >> 4)].ascii_str() + out += hex[int(b & 0xF)].ascii_str() + } else { + out += b.ascii_str() + } + } + } + } + return out + '"' +} diff --git a/vm/native.v b/vm/native.v index fb63a0b..3f82b3e 100644 --- a/vm/native.v +++ b/vm/native.v @@ -581,12 +581,187 @@ fn (mut v Vm) native(id int, _argc int) ! { 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)))! + } else { return error('unknown native builtin ${id}') } } } +// --------------------------------------------------------------------------- +// 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) { @@ -645,6 +820,11 @@ fn (mut v Vm) str_method(name string, argc int) ! { '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 { diff --git a/vm/opcodes.v b/vm/opcodes.v index 27fbbf7..823f7d8 100644 --- a/vm/opcodes.v +++ b/vm/opcodes.v @@ -65,6 +65,7 @@ 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), ...) +const op_push_none = u8(62) // push the `none` sentinel // native builtin ids (the operand to op_native) const native_abs = 100 @@ -126,3 +127,13 @@ const native_build_base = 153 const native_build_dir = 154 const native_build_join = 155 const native_build_root = 156 + +// stdlib builtins (JSON + string formatting) +const native_json_encode = 157 +const native_json_decode = 158 +const native_format = 159 +const native_replace = 160 +const native_split_lines = 161 +const native_pad = 162 +const native_pad_left = 163 +const native_repeat = 164 diff --git a/vm/ops.v b/vm/ops.v index 1206f4d..082b1c2 100644 --- a/vm/ops.v +++ b/vm/ops.v @@ -4,6 +4,9 @@ module vm import math fn (mut v Vm) add(a i64, b i64) !i64 { + if v.is_none(a) || v.is_none(b) { + return error('cannot use none with +') + } if v.is_arr(a) || v.is_arr(b) { return error('cannot add arrays with +') } @@ -75,6 +78,9 @@ fn fmt_float(f f64) string { } fn (mut v Vm) arith(a i64, b i64, op string) !i64 { + if v.is_none(a) || v.is_none(b) { + return error('cannot use none with "${op}"') + } if v.is_str(a) || v.is_str(b) { return error('cannot use strings with "${op}"') } @@ -139,6 +145,16 @@ fn (mut v Vm) arith(a i64, b i64, op string) !i64 { } fn (mut v Vm) cmp(a i64, b i64, op string) !i64 { + if v.is_none(a) || v.is_none(b) { + // none equals only none; ordering a none is an error + if op == '==' { + return bool_i64(v.is_none(a) && v.is_none(b)) + } + if op == '!=' { + return bool_i64(!(v.is_none(a) && v.is_none(b))) + } + return error('cannot order none') + } if v.is_arr(a) || v.is_arr(b) { // arrays compare by identity (handle equality) with ==/!= if op == '==' || op == '!=' { diff --git a/vm/print.v b/vm/print.v index aa322c5..874728d 100644 --- a/vm/print.v +++ b/vm/print.v @@ -11,6 +11,9 @@ fn (mut v Vm) val_str(x i64, depth int) string { if depth > 16 { return '...' } + if v.is_none(x) { + return 'none' + } if v.is_str(x) && v.valid_handle(x) { return v.strings[v.hand(x)] } @@ -117,6 +120,7 @@ fn (mut v Vm) trace_op(op u8) { op_load_dyn { 'load_dyn' } op_varargs { 'varargs' } op_str_method { 'str_method' } + op_push_none { 'push_none' } else { '??' } } mut s := '' diff --git a/vm/value.v b/vm/value.v index 428af6b..660f02e 100644 --- a/vm/value.v +++ b/vm/value.v @@ -16,6 +16,12 @@ const tag_arr = u64(3) const tag_float = u64(4) const tag_closure = u64(5) +// none_val is the sentinel for the `none` literal (and JSON null). Tag 110 +// is not a valid encoded integer (those are multiples of 8) nor any handle, +// so it can never collide with a real value. The GC ignores it (its tag +// matches no heap pool). +const none_val = i64(6) + fn (mut v Vm) tag(x i64) u64 { return u64(x) & tag_mask } @@ -44,6 +50,10 @@ fn (mut v Vm) is_closure(x i64) bool { return u64(x) & tag_mask == tag_closure } +fn (mut v Vm) is_none(x i64) bool { + return x == none_val +} + // 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 @@ -93,6 +103,9 @@ fn (mut v Vm) fval(x i64) f64 { } fn (mut v Vm) truthy(x i64) bool { + if v.is_none(x) { + return false + } if v.is_float(x) { return v.fval(x) != 0.0 } diff --git a/vm/vm.v b/vm/vm.v index 7b95d34..c1526db 100644 --- a/vm/vm.v +++ b/vm/vm.v @@ -443,7 +443,20 @@ fn (mut v Vm) exec() ! { v.ip++ id := int(v.read_i64()) argc := int(v.read_i64()) - v.native(id, argc)! + v.native(id, argc) or { + // a failed builtin becomes a VM-level throw, so try/catch can + // intercept it exactly like an explicit `throw`; with no + // handler it keeps propagating to the caller + if v.handlers.len == 0 { + return err + } + h := v.handlers[v.handlers.len - 1] + v.handlers.delete_last() + v.bp = h.bp + v.sp = h.sp + v.push(v.alloc_str(err.msg()))! + v.ip = h.ip + } } op_and_b { v.ip++ @@ -569,6 +582,10 @@ fn (mut v Vm) exec() ! { argc := int(v.read_i64()) v.str_method(v.strings[sidx], argc)! } + op_push_none { + v.ip++ + v.push(none_val)! + } else { return error('unknown opcode ${op} at ip ${v.ip}') }