# ============================================================================ # value.ludic — a generic value tree (`Value.*`) and its JSON bridge (`Json.*`), # in Ludic. The value tree is the tagged, self-describing node the reflection # serializer (runtime/native/reflect_io.ludic) walks an entity into, and the # shape a JSON save round-trips through. A node is one of: # # kind 0 null kind 1 int kind 2 fixed kind 3 bool # kind 4 str kind 5 list kind 6 object kind 7 float # # ludicc splices this file when it sees Value.* / Json.* / Reflect.serialize / # Reflect.apply (parse.ludic). Everything is plain Ludic over heap records — no # new runtime primitive — so the whole tree is deterministic and allocation is # the only cost. # ============================================================================ # one node of the tree. A list uses `kids`; an object uses `keys` + `kids` in # parallel (keys[i] labels kids[i]). Scalars use `num` (int/bool/fixed-raw) or # `txt` (str). property Val { tag: int = 0 num: int = 0 txt: pointer = null keys: []pointer kids: []Val } # a number, a string or a null is one record; only a list or an object carries lists (made when first # filled) - three allocations for every scalar was most of what a UI frame made and never gave back function value_new(tag: int) -> Val { let v = new Val v.tag = tag if tag == 5 or tag == 6 { value_lists(v) } return v } function value_lists(v: Val) -> void { if v.kids != null { return } v.keys = new []pointer v.kids = new []Val } function value_n(v: Val) -> int { if v.kids == null { return 0 } return len(v.kids) } # --- constructors ----------------------------------------------------------- function value_null() -> Val { return value_new(0) } function value_int(n: int) -> Val { let v = value_new(1); v.num = n; return v } function value_fixed(f: int) -> Val { let v = value_new(2); v.num = f; return v } # f = raw Q16.16 function value_bool(b: int) -> Val { let v = value_new(3); if b != 0 { v.num = 1 }; return v } function value_str(s: pointer) -> Val { let v = value_new(4); v.txt = s; return v } function value_list() -> Val { return value_new(5) } function value_float(x: float) -> Val { let v = value_new(7); v.num = float_bits(x); return v } # num = the IEEE bits function value_object() -> Val { return value_new(6) } # --- builders (return the container, so calls chain) ------------------------ function value_add(list: Val, item: Val) -> Val { value_lists(list) push(list.kids, item) return list } function value_put(obj: Val, key: pointer, item: Val) -> Val { value_lists(obj) var i = 0 while i < len(obj.keys) { if obj.keys[i] == key { obj.kids[i] = item; return obj }; i += 1 } push(obj.keys, key); push(obj.kids, item) return obj } # --- accessors -------------------------------------------------------------- function value_kind(v: Val) -> int { return v.tag } function value_count(v: Val) -> int { return value_n(v) } # a scalar field of an object set in place: the Value under `key` is changed when it is already that # kind, and made only the first time (a component's model, filled into the same object every frame) function value_slot(o: Val, key: pointer, tag: int) -> Val { value_lists(o) var i = 0 while i < len(o.keys) { if o.keys[i] == key { if o.kids[i].tag == tag { return o.kids[i] } let v = value_new(tag) o.kids[i] = v return v } i += 1 } let v = value_new(tag) push(o.keys, key) push(o.kids, v) return v } function value_set_int(o: Val, key: pointer, n: int) -> void { let v = value_slot(o, key, 1) v.num = n } function value_set_float(o: Val, key: pointer, x: float) -> void { let v = value_slot(o, key, 7) v.num = float_bits(x) } function value_set_str(o: Val, key: pointer, s: pointer) -> void { let v = value_slot(o, key, 4) v.txt = s } function value_set_bool(o: Val, key: pointer, b: bool) -> void { let v = value_slot(o, key, 3) v.num = 0 if b { v.num = 1 } } # a list field of an object set in place: the list under `key` is kept, and item i keeps its Value when it # is already that kind (a component's model, filled into the same object every frame) function value_list_fit(o: Val, key: pointer, n: int) -> Val { let l = value_slot(o, key, 5) value_lists(l) while len(l.kids) > n { List.pop(l.kids) } return l } function value_item(l: Val, i: int, tag: int) -> Val { if i < len(l.kids) { if l.kids[i].tag == tag { return l.kids[i] } let v = value_new(tag) l.kids[i] = v return v } let v = value_new(tag) push(l.kids, v) return v } function value_set_ints(o: Val, key: pointer, xs: []int) -> void { let l = value_list_fit(o, key, len(xs)) for i in 0 .. len(xs) { let v = value_item(l, i, 1) v.num = xs[i] } } function value_set_floats(o: Val, key: pointer, xs: []float) -> void { let l = value_list_fit(o, key, len(xs)) for i in 0 .. len(xs) { let v = value_item(l, i, 7) v.num = float_bits(xs[i]) } } function value_set_strs(o: Val, key: pointer, xs: []string) -> void { let l = value_list_fit(o, key, len(xs)) for i in 0 .. len(xs) { let v = value_item(l, i, 4) v.txt = xs[i] } } function value_set_bools(o: Val, key: pointer, xs: []bool) -> void { let l = value_list_fit(o, key, len(xs)) for i in 0 .. len(xs) { let v = value_item(l, i, 3) v.num = 0 if xs[i] { v.num = 1 } } } # a Value turned into a blank one of kind `tag`, its own lists (if it has them) kept and emptied - for # a pool that hands the same records out again (ludic.ui's, frame after frame) function value_become(v: Val, tag: int) -> Val { v.tag = tag v.num = 0 v.txt = null if tag == 5 or tag == 6 { value_lists(v) } if v.kids != null { value_clear(v) } return v } function value_be_int(v: Val, n: int) -> Val { value_become(v, 1) v.num = n return v } function value_be_float(v: Val, x: float) -> Val { value_become(v, 7) v.num = float_bits(x) return v } function value_be_str(v: Val, s: pointer) -> Val { value_become(v, 4) v.txt = s return v } # a result written into `into` when one is given (a record ludic.ui keeps and hands out again), made # as ever when it is null - what a component's call() answers a template with function value_into(into: Val, tag: int) -> Val { if into == null { return value_new(tag) } return value_become(into, tag) } function value_into_null(into: Val) -> Val { return value_into(into, 0) } function value_into_int(into: Val, n: int) -> Val { let v = value_into(into, 1) v.num = n return v } function value_into_float(into: Val, x: float) -> Val { let v = value_into(into, 7) v.num = float_bits(x) return v } function value_into_str(into: Val, s: pointer) -> Val { let v = value_into(into, 4) v.txt = s return v } function value_into_bool(into: Val, b: bool) -> Val { let v = value_into(into, 3) if b { v.num = 1 } return v } # a list result: `into`'s items are kept and changed in place where they are already that kind function value_into_list(into: Val, n: int) -> Val { if into == null { return value_list() } into.tag = 5 into.num = 0 into.txt = null value_lists(into) while len(into.kids) > n { List.pop(into.kids) } return into } function value_into_ints(into: Val, xs: []int) -> Val { let l = value_into_list(into, len(xs)) for i in 0 .. len(xs) { let v = value_item(l, i, 1) v.num = xs[i] } return l } function value_into_floats(into: Val, xs: []float) -> Val { let l = value_into_list(into, len(xs)) for i in 0 .. len(xs) { let v = value_item(l, i, 7) v.num = float_bits(xs[i]) } return l } function value_into_strs(into: Val, xs: []string) -> Val { let l = value_into_list(into, len(xs)) for i in 0 .. len(xs) { let v = value_item(l, i, 4) v.txt = xs[i] } return l } function value_into_bools(into: Val, xs: []bool) -> Val { let l = value_into_list(into, len(xs)) for i in 0 .. len(xs) { let v = value_item(l, i, 3) v.num = 0 if xs[i] { v.num = 1 } } return l } # a list or an object emptied in place, its room kept - for one filled again every frame function value_clear(v: Val) -> void { if v.kids == null { return } List.clear(v.kids) List.clear(v.keys) } function value_as_int(v: Val) -> int { if v.tag == 7 { return int(float_from_bits(v.num)) } return v.num } # a number as a float, whichever kind it was written as function value_as_float(v: Val) -> float { if v.tag == 7 { return float_from_bits(v.num) } if v.tag == 2 { return float(v.num) / 65536.0 } return float(v.num) } function value_as_str(v: Val) -> string { if v.txt == null { return "" }; return v.txt } function value_at(list: Val, i: int) -> Val { if i < 0 or i >= value_n(list) { return value_null() } return list.kids[i] } function value_key_at(obj: Val, i: int) -> string { if i < 0 or i >= value_n(obj) { return "" } return obj.keys[i] } function value_get(obj: Val, key: pointer) -> Val { var i = 0 while i < value_n(obj) { if obj.keys[i] == key { return obj.kids[i] }; i += 1 } return value_null() } function value_has(obj: Val, key: pointer) -> int { var i = 0 while i < value_n(obj) { if obj.keys[i] == key { return 1 }; i += 1 } return 0 } # --- JSON encode ------------------------------------------------------------ # escape a string's `"` `\` and newline, wrapping it in quotes. function json_quote(s: pointer) -> string { var out = "\"" var i = 0 let n = len(s) while i < n { let c = s[i] if c == '"' { out += "\\\"" } else { if c == '\\' { out += "\\\\" } else { if c == '\n' { out += "\\n" } else { out += s[i..i + 1] } } } i += 1 } return out + "\"" } # a raw Q16.16 fixed -> its EXACT decimal string. The denominator is 2^16, so the # decimal always terminates (in <=16 fractional digits): emit digits until the # remainder hits zero. This is the exact inverse of jp_number's fixed decode, so # a value_fixed round-trips through JSON bit-for-bit; short values (0.5, 2.0) stay # short. Each step keeps frac < 655360, well within i32. function json_fixed_str(raw: int) -> string { var a = raw var sign = "" if a < 0 { sign = "-"; a = -a } let ip = a / 65536 var frac = a % 65536 if frac == 0 { return sign + string(ip) + ".0" } var fs = "" while frac != 0 { frac *= 10 fs += string(frac / 65536) frac = frac % 65536 } return sign + string(ip) + "." + fs } function json_encode(v: Val) -> string { if v.tag == 0 { return "null" } if v.tag == 1 { return string(v.num) } if v.tag == 2 { return json_fixed_str(v.num) } if v.tag == 3 { if v.num != 0 { return "true" }; return "false" } if v.tag == 4 { return json_quote(v.txt) } if v.tag == 7 { return string(float_from_bits(v.num)) } if v.tag == 5 { var out = "[" var i = 0 while i < value_n(v) { if i > 0 { out += "," } out += json_encode(v.kids[i]) i += 1 } return out + "]" } var out = "{" var i = 0 while i < value_n(v) { if i > 0 { out += "," } out = out + json_quote(v.keys[i]) + ":" + json_encode(v.kids[i]) i += 1 } return out + "}" } # --- JSON parse ------------------------------------------------------------- # a tiny recursive-descent parser over a byte string. `JP` carries the cursor so # the recursion threads position without a global. Malformed input yields a null # node and leaves the cursor where it stopped (best-effort, not a validator). property JP { s: pointer = null, i: int = 0, n: int = 0 } function jp_skip_ws(p: JP) -> void { while p.i < p.n { let c = p.s[p.i] if c == ' ' or c == '\t' or c == '\n' or c == '\r' { p.i += 1 } else { return } } } function json_parse(s: pointer) -> Val { let p = new JP p.s = s; p.i = 0; p.n = len(s) let v = jp_value(p) free(p) return v } # Let a parsed tree go once what it describes is built: every node and its lists. Its strings stay - # a caller may keep a name it read out of the tree - and a tree put together by hand, which may share # a node or hold a literal, is not for this (Ludic has no collector; plan 23 of maroon-lake). function value_free(v: Val) -> void { if v == null { return } if v.kids != null { for i in 0 .. len(v.kids) { value_free(v.kids[i]) } free(v.kids) free(v.keys) } free(v) } function json_free(v: Val) -> void { value_free(v) } function jp_value(p: JP) -> Val { jp_skip_ws(p) if p.i >= p.n { return value_null() } let c = p.s[p.i] if c == '{' { return jp_object(p) } # '{' if c == '[' { return jp_list(p) } # '[' if c == '"' { return value_str(jp_string(p)) } # '"' if c == 't' { p.i += 4; return value_bool(1) } # true if c == 'f' { p.i += 5; return value_bool(0) } # false if c == 'n' { p.i += 4; return value_null() } # null return jp_number(p) } # read a quoted string (cursor on the opening quote) -> the unescaped contents. One allocation for a # string with no escape in it: built a character at a time, every step was a string never freed function jp_string(p: JP) -> string { p.i += 1 # skip opening quote let a = p.i while p.i < p.n { let c = p.s[p.i] if c == '"' { let out = p.s[a..p.i] p.i += 1 return out } if c == '\\' { p.i = a return jp_string_esc(p) } p.i += 1 } return p.s[a..p.n] } # the same with escapes in it, the rare case, a character at a time function jp_string_esc(p: JP) -> string { var out = "" while p.i < p.n { let c = p.s[p.i] if c == '"' { p.i += 1; return out } # closing quote if c == '\\' { # escape p.i += 1 if p.i < p.n { let e = p.s[p.i] if e == 'n' { out += "\n" } else { out += p.s[p.i..p.i + 1] } # \" \\ \/ -> the literal char p.i += 1 } } else { out += p.s[p.i..p.i + 1] p.i += 1 } } return out } # read a number; a '.' makes it a fixed node, otherwise an int node. function jp_number(p: JP) -> Val { var neg = 0 if p.i < p.n and p.s[p.i] == '-' { neg = 1; p.i += 1 } # '-' var ip = 0 while p.i < p.n and p.s[p.i] >= '0' and p.s[p.i] <= '9' { ip = ip * 10 + (p.s[p.i] - 48) p.i += 1 } if p.i < p.n and p.s[p.i] == '.' { # '.', a fixed p.i += 1 let digits = new []int while p.i < p.n and p.s[p.i] >= '0' and p.s[p.i] <= '9' { push(digits, p.s[p.i] - 48) p.i += 1 } # exact inverse of json_fixed_str's generation: fold the digits back from the # last, frac_{i-1} = (d_i*65536 + frac_i)/10. Every intermediate stays < 2^16*10, # so this is overflow-free and reproduces the raw Q16.16 for a generated decimal # (an arbitrary hand-written decimal lands within one raw unit). var frac = 0 var di = len(digits) - 1 while di >= 0 { frac = (digits[di] * 65536 + frac) / 10 di -= 1 } var raw = ip * 65536 + frac raw = jp_exponent(p, raw) if neg != 0 { raw = -raw } return value_fixed(raw) } if p.i < p.n and (p.s[p.i] == 'e' or p.s[p.i] == 'E') { # 1e-05: an exponent makes it a fixed var raw = jp_exponent(p, ip * 65536) if neg != 0 { raw = -raw } return value_fixed(raw) } if neg != 0 { ip = -ip } return value_int(ip) } # an optional exponent after a number's digits, applied to a raw Q16.16 value. Exporters # write noise like 7.49e-09 for a zero; a fixed rounds that to 0, which is what it was. function jp_exponent(p: JP, raw0: int) -> int { var raw = raw0 if p.i >= p.n or (p.s[p.i] != 'e' and p.s[p.i] != 'E') { return raw } p.i += 1 var eneg = 0 if p.i < p.n and p.s[p.i] == '-' { eneg = 1; p.i += 1 } else if p.i < p.n and p.s[p.i] == '+' { p.i += 1 } var e = 0 while p.i < p.n and p.s[p.i] >= '0' and p.s[p.i] <= '9' { e = e * 10 + (p.s[p.i] - 48) p.i += 1 } if e > 12 { e = 12 } var k = 0 while k < e { if eneg != 0 { raw = raw / 10 } else { raw = raw * 10 } k += 1 } return raw } function jp_list(p: JP) -> Val { let out = value_list() p.i += 1 # '[' jp_skip_ws(p) if p.i < p.n and p.s[p.i] == ']' { p.i += 1; return out } # empty ']' while p.i < p.n { push(out.kids, jp_value(p)) jp_skip_ws(p) if p.i < p.n and p.s[p.i] == ',' { p.i += 1; continue } # ',' break } jp_skip_ws(p) if p.i < p.n and p.s[p.i] == ']' { p.i += 1 } return out } function jp_object(p: JP) -> Val { let out = value_object() p.i += 1 # '{' jp_skip_ws(p) if p.i < p.n and p.s[p.i] == '}' { p.i += 1; return out } # empty '}' while p.i < p.n { jp_skip_ws(p) let key = jp_string(p) jp_skip_ws(p) if p.i < p.n and p.s[p.i] == ':' { p.i += 1 } # ':' push(out.keys, key) push(out.kids, jp_value(p)) jp_skip_ws(p) if p.i < p.n and p.s[p.i] == ',' { p.i += 1; continue } # ',' break } jp_skip_ws(p) if p.i < p.n and p.s[p.i] == '}' { p.i += 1 } return out }