mirror of
https://github.com/bearlanguageorg/bear.git
synced 2026-08-26 15:17:19 +00:00
Added default json, etc.
This commit is contained in:
@@ -260,6 +260,16 @@ fn main() {
|
|||||||
- bitwise operators: `& | ^ ~ << >>`
|
- bitwise operators: `& | ^ ~ << >>`
|
||||||
- host builtins: `read_file` / `write_file`, `args()`, `getenv` / `setenv`,
|
- host builtins: `read_file` / `write_file`, `args()`, `getenv` / `setenv`,
|
||||||
`exit`, `sleep`, `time()`, `type(x)`, `str(x)`, `int(x)`, `split` / `join`
|
`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 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
|
`for i in 0...10 { }`; loop variables are scoped to the loop body
|
||||||
- `break` / `continue` inside `while` and `for` loops (in `for` loops
|
- `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
|
v.mod module definition
|
||||||
compiler/ assembler/ linker/ vm/ obj/ the toolchain itself
|
compiler/ assembler/ linker/ vm/ obj/ the toolchain itself
|
||||||
bin/ built binary + standalone tools
|
bin/ built binary + standalone tools
|
||||||
examples/ runnable examples (hello, lib, asm, tests, fib)
|
examples/ runnable examples (hello, lib, asm, tests, fib, json)
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ pub enum ExprKind {
|
|||||||
float_lit
|
float_lit
|
||||||
str_lit
|
str_lit
|
||||||
bool_lit
|
bool_lit
|
||||||
|
none_lit
|
||||||
ident
|
ident
|
||||||
array_lit
|
array_lit
|
||||||
struct_lit
|
struct_lit
|
||||||
|
|||||||
+17
-8
@@ -18,6 +18,7 @@ enum CType {
|
|||||||
float_t
|
float_t
|
||||||
string_t
|
string_t
|
||||||
bool_t
|
bool_t
|
||||||
|
none_t
|
||||||
array_t
|
array_t
|
||||||
struct_t
|
struct_t
|
||||||
enum_t
|
enum_t
|
||||||
@@ -221,7 +222,7 @@ fn (mut c Checker) check_stmt(st Stmt) ! {
|
|||||||
.for_in_stmt {
|
.for_in_stmt {
|
||||||
seq := c.check_expr(st.expr)!
|
seq := c.check_expr(st.expr)!
|
||||||
// iterate enums and arrays; unknown is allowed (dynamic)
|
// 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})')
|
return error('cannot iterate a ${type_name(seq.kind)} (line ${st.line})')
|
||||||
}
|
}
|
||||||
c.types[st.target] = TypeInfo{ kind: .unknown }
|
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 } }
|
.float_lit { TypeInfo{ kind: .float_t } }
|
||||||
.str_lit { TypeInfo{ kind: .string_t } }
|
.str_lit { TypeInfo{ kind: .string_t } }
|
||||||
.bool_lit { TypeInfo{ kind: .bool_t } }
|
.bool_lit { TypeInfo{ kind: .bool_t } }
|
||||||
|
.none_lit { TypeInfo{ kind: .none_t } }
|
||||||
.ident {
|
.ident {
|
||||||
if e.name in c.types {
|
if e.name in c.types {
|
||||||
c.types[e.name]
|
c.types[e.name]
|
||||||
@@ -324,7 +326,7 @@ fn (mut c Checker) check_expr(e Expr) !TypeInfo {
|
|||||||
}
|
}
|
||||||
.method_call {
|
.method_call {
|
||||||
recv := c.check_expr(*e.left)!
|
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})')
|
return error('cannot call a method on a ${type_name(recv.kind)} (line ${e.line})')
|
||||||
}
|
}
|
||||||
for a in e.args {
|
for a in e.args {
|
||||||
@@ -336,7 +338,7 @@ fn (mut c Checker) check_expr(e Expr) !TypeInfo {
|
|||||||
base := c.check_expr(*e.left)!
|
base := c.check_expr(*e.left)!
|
||||||
_ = c.check_expr(*e.right)!
|
_ = c.check_expr(*e.right)!
|
||||||
_ = c.check_expr(*e.extra)!
|
_ = 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})')
|
return error('cannot slice a ${type_name(base.kind)} (line ${e.line})')
|
||||||
}
|
}
|
||||||
if base.kind == .string_t {
|
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 {
|
if l.kind == .bool_t && r.kind == .bool_t {
|
||||||
return error('cannot order booleans (line ${e.line})')
|
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)) {
|
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})')
|
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})')
|
return error('len() takes exactly one argument (line ${e.line})')
|
||||||
}
|
}
|
||||||
t := c.check_expr(e.args[0])!
|
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 error('len() on a ${type_name(t.kind)} (line ${e.line})')
|
||||||
}
|
}
|
||||||
return TypeInfo{ kind: .int_t }
|
return TypeInfo{ kind: .int_t }
|
||||||
@@ -552,6 +557,9 @@ fn builtin_result_type(name string) TypeInfo {
|
|||||||
'args', 'keys' { TypeInfo{ kind: .array_t } }
|
'args', 'keys' { TypeInfo{ kind: .array_t } }
|
||||||
'len' { TypeInfo{ kind: .int_t } }
|
'len' { TypeInfo{ kind: .int_t } }
|
||||||
'write_file', 'setenv', 'exit', 'sleep', 'eprint' { TypeInfo{ kind: .unknown } }
|
'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-module builtins (.vrmm)
|
||||||
'build_compile', 'build_assemble', 'build_link', 'build_exec', 'build_base',
|
'build_compile', 'build_assemble', 'build_link', 'build_exec', 'build_base',
|
||||||
'build_dir', 'build_join', 'build_root' { TypeInfo{ kind: .string_t } }
|
'build_dir', 'build_join', 'build_root' { TypeInfo{ kind: .string_t } }
|
||||||
@@ -576,30 +584,31 @@ fn type_name(k CType) string {
|
|||||||
.struct_t { 'struct' }
|
.struct_t { 'struct' }
|
||||||
.enum_t { 'enum' }
|
.enum_t { 'enum' }
|
||||||
.closure_t { 'function' }
|
.closure_t { 'function' }
|
||||||
|
.none_t { 'none' }
|
||||||
else { 'value' }
|
else { 'value' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn (mut c Checker) expect_numeric(t TypeInfo, what string, line int) ! {
|
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})')
|
return error('${what} on a ${type_name(t.kind)} (line ${line})')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn (mut c Checker) expect_int(t TypeInfo, what string, line int) ! {
|
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})')
|
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) ! {
|
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})')
|
return error('${what} on a ${type_name(t.kind)} (line ${line})')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn (mut c Checker) expect_struct_like(t TypeInfo, what string, line int) ! {
|
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})')
|
return error('${what} on a ${type_name(t.kind)} (line ${line})')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -723,6 +723,9 @@ fn (mut g Gen) gen_expr(e Expr) ! {
|
|||||||
g.code << op_push_i
|
g.code << op_push_i
|
||||||
g.code << obj.encode_i64(e.int_v)
|
g.code << obj.encode_i64(e.int_v)
|
||||||
}
|
}
|
||||||
|
.none_lit {
|
||||||
|
g.code << op_push_none
|
||||||
|
}
|
||||||
.ident {
|
.ident {
|
||||||
// check if it's a constant
|
// check if it's a constant
|
||||||
if e.name in g.consts {
|
if e.name in g.consts {
|
||||||
@@ -923,6 +926,15 @@ fn builtin_spec(name string) (int, int) {
|
|||||||
'build_dir' { native_build_dir, 1 }
|
'build_dir' { native_build_dir, 1 }
|
||||||
'build_join' { native_build_join, 2 }
|
'build_join' { native_build_join, 2 }
|
||||||
'build_root' { native_build_root, 0 }
|
'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 }
|
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' }
|
'upper', 'lower', 'trim', 'str', 'getenv', 'read_file', 'join' { 'string' }
|
||||||
'build_compile', 'build_assemble', 'build_link', 'build_exec', 'build_base',
|
'build_compile', 'build_assemble', 'build_link', 'build_exec', 'build_base',
|
||||||
'build_dir', 'build_join', 'build_root' { 'string' }
|
'build_dir', 'build_join', 'build_root' { 'string' }
|
||||||
|
'json_encode', 'format', 'replace', 'pad', 'pad_left', 'repeat' { 'string' }
|
||||||
else { '' }
|
else { '' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -291,6 +291,7 @@ fn (mut l Lexer) lex_ident(line int, col int) Tok {
|
|||||||
'return' { TokKind.kw_return }
|
'return' { TokKind.kw_return }
|
||||||
'true' { TokKind.kw_true }
|
'true' { TokKind.kw_true }
|
||||||
'false' { TokKind.kw_false }
|
'false' { TokKind.kw_false }
|
||||||
|
'none' { TokKind.kw_none }
|
||||||
'and' { TokKind.kw_and }
|
'and' { TokKind.kw_and }
|
||||||
'or' { TokKind.kw_or }
|
'or' { TokKind.kw_or }
|
||||||
'not' { TokKind.kw_not }
|
'not' { TokKind.kw_not }
|
||||||
|
|||||||
@@ -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_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_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_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)
|
// native builtin ids (keep in sync with vm/opcodes.v)
|
||||||
const native_abs = 100
|
const native_abs = 100
|
||||||
@@ -127,3 +128,13 @@ const native_build_base = 153
|
|||||||
const native_build_dir = 154
|
const native_build_dir = 154
|
||||||
const native_build_join = 155
|
const native_build_join = 155
|
||||||
const native_build_root = 156
|
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
|
||||||
|
|||||||
@@ -787,6 +787,10 @@ fn (mut p Parser) parse_primary() !Expr {
|
|||||||
p.advance()
|
p.advance()
|
||||||
return Expr{ kind: .bool_lit, int_v: 0, line: t.line }
|
return Expr{ kind: .bool_lit, int_v: 0, line: t.line }
|
||||||
}
|
}
|
||||||
|
.kw_none {
|
||||||
|
p.advance()
|
||||||
|
return Expr{ kind: .none_lit, line: t.line }
|
||||||
|
}
|
||||||
.lparen {
|
.lparen {
|
||||||
p.advance()
|
p.advance()
|
||||||
e := p.parse_expr()!
|
e := p.parse_expr()!
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ pub enum TokKind {
|
|||||||
kw_return
|
kw_return
|
||||||
kw_true
|
kw_true
|
||||||
kw_false
|
kw_false
|
||||||
|
kw_none
|
||||||
kw_and
|
kw_and
|
||||||
kw_or
|
kw_or
|
||||||
kw_not
|
kw_not
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
}
|
||||||
@@ -226,6 +226,87 @@ fn test_structs() {
|
|||||||
assert a != c
|
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() {
|
fn test_failing() {
|
||||||
// this one is meant to fail — shows up in `vr test` output
|
// this one is meant to fail — shows up in `vr test` output
|
||||||
assert 1 == 2
|
assert 1 == 2
|
||||||
|
|||||||
@@ -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
@@ -581,12 +581,187 @@ fn (mut v Vm) native(id int, _argc int) ! {
|
|||||||
native_build_root {
|
native_build_root {
|
||||||
v.push(v.alloc_str(v.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 {
|
else {
|
||||||
return error('unknown native builtin ${id}')
|
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).
|
// copy_tree recursively copies a directory tree (used by build_copy).
|
||||||
fn (mut v Vm) copy_tree(src string, dst string) ! {
|
fn (mut v Vm) copy_tree(src string, dst string) ! {
|
||||||
if !os.is_dir(src) {
|
if !os.is_dir(src) {
|
||||||
@@ -645,6 +820,11 @@ fn (mut v Vm) str_method(name string, argc int) ! {
|
|||||||
'index_of' { native_index_of }
|
'index_of' { native_index_of }
|
||||||
'to_int' { native_int }
|
'to_int' { native_int }
|
||||||
'to_float' { native_float }
|
'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 }
|
else { -1 }
|
||||||
}
|
}
|
||||||
if bid >= 0 {
|
if bid >= 0 {
|
||||||
|
|||||||
@@ -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_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_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_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)
|
// native builtin ids (the operand to op_native)
|
||||||
const native_abs = 100
|
const native_abs = 100
|
||||||
@@ -126,3 +127,13 @@ const native_build_base = 153
|
|||||||
const native_build_dir = 154
|
const native_build_dir = 154
|
||||||
const native_build_join = 155
|
const native_build_join = 155
|
||||||
const native_build_root = 156
|
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,6 +4,9 @@ module vm
|
|||||||
import math
|
import math
|
||||||
|
|
||||||
fn (mut v Vm) add(a i64, b i64) !i64 {
|
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) {
|
if v.is_arr(a) || v.is_arr(b) {
|
||||||
return error('cannot add arrays with +')
|
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 {
|
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) {
|
if v.is_str(a) || v.is_str(b) {
|
||||||
return error('cannot use strings with "${op}"')
|
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 {
|
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) {
|
if v.is_arr(a) || v.is_arr(b) {
|
||||||
// arrays compare by identity (handle equality) with ==/!=
|
// arrays compare by identity (handle equality) with ==/!=
|
||||||
if op == '==' || op == '!=' {
|
if op == '==' || op == '!=' {
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ fn (mut v Vm) val_str(x i64, depth int) string {
|
|||||||
if depth > 16 {
|
if depth > 16 {
|
||||||
return '...'
|
return '...'
|
||||||
}
|
}
|
||||||
|
if v.is_none(x) {
|
||||||
|
return 'none'
|
||||||
|
}
|
||||||
if v.is_str(x) && v.valid_handle(x) {
|
if v.is_str(x) && v.valid_handle(x) {
|
||||||
return v.strings[v.hand(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_load_dyn { 'load_dyn' }
|
||||||
op_varargs { 'varargs' }
|
op_varargs { 'varargs' }
|
||||||
op_str_method { 'str_method' }
|
op_str_method { 'str_method' }
|
||||||
|
op_push_none { 'push_none' }
|
||||||
else { '??' }
|
else { '??' }
|
||||||
}
|
}
|
||||||
mut s := ''
|
mut s := ''
|
||||||
|
|||||||
+13
@@ -16,6 +16,12 @@ const tag_arr = u64(3)
|
|||||||
const tag_float = u64(4)
|
const tag_float = u64(4)
|
||||||
const tag_closure = u64(5)
|
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 {
|
fn (mut v Vm) tag(x i64) u64 {
|
||||||
return u64(x) & tag_mask
|
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
|
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.
|
// is_num reports whether x is an integer (tag 0). Floats are a distinct type.
|
||||||
fn (mut v Vm) is_num(x i64) bool {
|
fn (mut v Vm) is_num(x i64) bool {
|
||||||
return u64(x) & tag_mask == tag_int
|
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 {
|
fn (mut v Vm) truthy(x i64) bool {
|
||||||
|
if v.is_none(x) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if v.is_float(x) {
|
if v.is_float(x) {
|
||||||
return v.fval(x) != 0.0
|
return v.fval(x) != 0.0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -443,7 +443,20 @@ fn (mut v Vm) exec() ! {
|
|||||||
v.ip++
|
v.ip++
|
||||||
id := int(v.read_i64())
|
id := int(v.read_i64())
|
||||||
argc := 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 {
|
op_and_b {
|
||||||
v.ip++
|
v.ip++
|
||||||
@@ -569,6 +582,10 @@ fn (mut v Vm) exec() ! {
|
|||||||
argc := int(v.read_i64())
|
argc := int(v.read_i64())
|
||||||
v.str_method(v.strings[sidx], argc)!
|
v.str_method(v.strings[sidx], argc)!
|
||||||
}
|
}
|
||||||
|
op_push_none {
|
||||||
|
v.ip++
|
||||||
|
v.push(none_val)!
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
return error('unknown opcode ${op} at ip ${v.ip}')
|
return error('unknown opcode ${op} at ip ${v.ip}')
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user