Added default json, etc.

This commit is contained in:
allexanderbergmns
2026-08-25 15:11:40 +02:00
parent 012c8a0e16
commit 76e912c922
17 changed files with 878 additions and 10 deletions
+11 -1
View File
@@ -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)
```
+1
View File
@@ -6,6 +6,7 @@ pub enum ExprKind {
float_lit
str_lit
bool_lit
none_lit
ident
array_lit
struct_lit
+17 -8
View File
@@ -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})')
}
}
+13
View File
@@ -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 { '' }
}
}
+1
View File
@@ -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 }
+11
View File
@@ -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) // <named:i64> <dst:i64> — collect args[named..argc-1] into an array at local dst
const op_str_method = u8(61) // <name:str> <argc:i64> — 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
+4
View File
@@ -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()!
+1
View File
@@ -55,6 +55,7 @@ pub enum TokKind {
kw_return
kw_true
kw_false
kw_none
kw_and
kw_or
kw_not
+46
View File
@@ -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"))
}
+81
View File
@@ -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
+450
View File
@@ -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 + '"'
}
+180
View File
@@ -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 {
+11
View File
@@ -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) // <named:i64> <dst:i64> — collect args[named..argc-1] into array at local dst
const op_str_method = u8(61) // <name:str> <argc:i64> — 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
+16
View File
@@ -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 == '!=' {
+4
View File
@@ -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 := ''
+13
View File
@@ -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
}
+18 -1
View File
@@ -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}')
}