feat(reflect): generic value tree + Reflect.serialize/apply + JSON bridge (#44)
All checks were successful
bootstrap / cfree-fixpoint (push) Successful in 17s
ci / build-and-test (push) Successful in 1m15s
commit-lint / conventional-commits (push) Successful in 3s
docs / build-and-deploy (push) Successful in 19s

A self-describing Value node (null/int/fixed/bool/str/list/object) with
constructors, builders (Value.add/put) and accessors (get/at/count/kind/
as_int/as_str/…). Reflect.serialize(entity) walks an entity's whole component
set into a value tree — one member per component, each a sub-object of its
fields — and Reflect.apply(entity, value) writes one back; a fixed field
becomes a fixed node, everything else an int node, so the round-trip is
bit-exact, with the model id under "@kind". Json.encode/parse bridge the tree
to and from compact, stable, diffable text, with fixed written as an exact
terminating decimal that parses back bit-for-bit (verified across the raw
Q16.16 range). Together: a one-call, bit-exact save/load for entities.

Written in Ludic and spliced on demand (runtime/native/value.ludic +
reflect_io.ludic, like Query/Light), so a program that doesn't touch
Value.*/Json.*/Reflect.serialize compiles byte-identically and the C-free
bootstrap fixpoint holds (verified). The general tagged-union/any language type
stays tracked in #1; this ships the concrete value tree the serializer needs.

Adds 21 namespace-method docs pages + Value/Json sections,
examples/library/serialize.ludic, and a regression case. Whole CI set green:
x test 72/72, x test-tools 30/30, check-impl/vocabulary/docs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-08-31 15:19:21 +03:00
parent 0d1b09e4f0
commit 9452557f3c
31 changed files with 16753 additions and 14898 deletions

View file

@ -295,6 +295,36 @@ function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
if (meth == "has") { bare = "world_has"; push(labels, "entity"); push(labels, "prop") }
if (meth == "kind") { bare = "world_kind"; push(labels, "entity") }
if (meth == "model") { bare = "world_model_id"; push(labels, "name") }
# #44 — serialize an entity to a value tree and apply one back (reflect_io.ludic)
if (meth == "serialize") { bare = "reflect_serialize"; push(labels, "entity") }
if (meth == "apply") { bare = "reflect_apply"; push(labels, "entity"); push(labels, "value") }
}
# Value.* — the generic value tree (runtime/native/value.ludic, spliced on
# demand). Nodes are int/fixed/bool/str/list/object; the methods map straight
# to the spliced value_* functions, whose signatures carry the return types.
if (ns == "Value") {
if (meth == "null") { bare = "value_null" }
if (meth == "int") { bare = "value_int"; push(labels, "n") }
if (meth == "fixed") { bare = "value_fixed"; push(labels, "f") }
if (meth == "bool") { bare = "value_bool"; push(labels, "b") }
if (meth == "str") { bare = "value_str"; push(labels, "s") }
if (meth == "list") { bare = "value_list" }
if (meth == "object") { bare = "value_object" }
if (meth == "add") { bare = "value_add"; push(labels, "list"); push(labels, "item") }
if (meth == "put") { bare = "value_put"; push(labels, "obj"); push(labels, "key"); push(labels, "item") }
if (meth == "get") { bare = "value_get"; push(labels, "obj"); push(labels, "key") }
if (meth == "has") { bare = "value_has"; push(labels, "obj"); push(labels, "key") }
if (meth == "at") { bare = "value_at"; push(labels, "list"); push(labels, "index") }
if (meth == "key_at") { bare = "value_key_at"; push(labels, "obj"); push(labels, "index") }
if (meth == "count") { bare = "value_count"; push(labels, "value") }
if (meth == "kind") { bare = "value_kind"; push(labels, "value") }
if (meth == "as_int") { bare = "value_as_int"; push(labels, "value") }
if (meth == "as_str") { bare = "value_as_str"; push(labels, "value") }
}
# Json.* — the text bridge over the value tree (runtime/native/value.ludic).
if (ns == "Json") {
if (meth == "encode") { bare = "json_encode"; push(labels, "value") }
if (meth == "parse") { bare = "json_parse"; push(labels, "text") }
}
if (bare == null) { perr(`unknown builtin {ns}.{meth}`) }
reorder_named(e, labels)

View file

@ -178,6 +178,8 @@ function p_postfix() -> Node {
if e.a.kind == E_ID and e.a.s == "Query" { g_uses_query = true } # splice the ECS spatial-query runtime on demand
if e.a.kind == E_ID and e.a.s == "Reflect" { g_uses_reflect = true } # force-emit the reflection ABI (Reflect.* reads the world schema)
if e.a.kind == E_ID and e.a.s == "Light" { g_uses_light = true } # splice the 2D light-accumulation pass on demand
if e.a.kind == E_ID and (e.a.s == "Value" or e.a.s == "Json") { g_uses_value = true } # splice the value tree + JSON on demand (#44)
if e.a.kind == E_ID and e.a.s == "Reflect" and (e.s == "serialize" or e.s == "apply") { g_uses_value = true; g_uses_reflect_io = true } # Reflect.serialize/apply -> value tree + world table
}
else { if is_op("[") { pi = pi + 1; let lo = expr()
if is_op("..") { pi = pi + 1; let sl = node(E_SLICE); sl.a = e; sl.b = lo; sl.c = expr(); eat_op("]"); e = sl } # s[a..b] substring
@ -403,6 +405,8 @@ var g_uses_regex: bool = false # a program mentioned Regex.* -> splice the re
var g_uses_query: bool = false # a program mentioned Query.* -> splice the query runtime + reflection ABI
var g_uses_reflect: bool = false # a program mentioned Reflect.* -> force-emit the reflection ABI
var g_uses_light: bool = false # a program mentioned Light.* -> splice the 2D light pass
var g_uses_value: bool = false # Value.*/Json.*/Reflect.serialize -> splice the value tree + JSON (#44)
var g_uses_reflect_io: bool = false # Reflect.serialize/apply -> splice the reflection serializer
function already_loaded(full: pointer) -> bool {
var i = 0
@ -585,6 +589,20 @@ function maybe_splice_runtime() -> void {
do_import("runtime/native/light.ludic")
cur_dir = saved
}
# Value.*/Json.* (#44) get the generic value tree + JSON bridge spliced in; it
# is self-contained (only string/slice ops), so it works in a plain tool too.
if g_uses_value {
cur_dir = ""
do_import("runtime/native/value.ludic")
cur_dir = saved
}
# Reflect.serialize/apply add the reflection serializer, which stands on both
# the value tree (above) and the world table (force-emitted for Reflect.*).
if g_uses_reflect_io {
cur_dir = ""
do_import("runtime/native/reflect_io.ludic")
cur_dir = saved
}
}
function parse_program() -> void {
@ -606,6 +624,8 @@ function parse_program() -> void {
g_uses_query = false
g_uses_reflect = false
g_uses_light = false
g_uses_value = false
g_uses_reflect_io = false
g_tests = new []Node
loaded_paths = new []pointer
skipnl()

File diff suppressed because it is too large Load diff