Follow-up to #20: generic value tree + Reflect.serialize/deserialize (needs #1) #44

Closed
opened 2026-08-31 12:29:10 +02:00 by orkun · 1 comment
Owner

Follow-up to #20, which shipped the reflection core in 12f2dbe: Reflect.* — enumerate properties/fields by index, resolve ids by name, read a field's declared type, and generic get/set/has/kind/model. That's enough to write an auto-serializer or debug inspector by hand (the shipped example does exactly that, summing every field of every component while naming none).

This tracks the remaining piece the proposal sketched: a generic value tree and a one-call Reflect.serialize(entity) / deserialize on top of it.

Why it's separate: a value tree needs an any/tagged-union value type (a node that is "int 7" or "fixed 0.5" or "list [...]"), which is exactly the tagged-union / container work in #1. Reflect.field_type already returns the type name per field, so once #1 lands a canonical value type, serialize is a thin walk:

# sketch
for p in Reflect.props_of(entity) {
  for f in Reflect.fields_of(p) {
    node.set(Reflect.field_name(p, f), value_of(Reflect.get(entity, p, f), Reflect.field_type(p, f)))
  }
}

Scope:

  • a canonical value-tree type (depends on #1 tagged unions / any)
  • Reflect.serialize(entity) -> value walking components + fields
  • Reflect.deserialize(value) -> entity (or apply-into-entity) as the inverse
  • JSON bridge (value tree <-> text) for saves and tooling

Sequence this after #1. Related: #20 (the shipped reflection core), #1 (type system — prerequisite).

Follow-up to #20, which shipped the reflection **core** in 12f2dbe: `Reflect.*` — enumerate properties/fields by index, resolve ids by name, read a field's declared type, and generic `get`/`set`/`has`/`kind`/`model`. That's enough to write an auto-serializer or debug inspector by hand (the shipped example does exactly that, summing every field of every component while naming none). This tracks the remaining piece the proposal sketched: a **generic value tree** and a one-call `Reflect.serialize(entity)` / `deserialize` on top of it. Why it's separate: a value tree needs an `any`/tagged-union value type (a node that is "int 7" or "fixed 0.5" or "list [...]"), which is exactly the tagged-union / container work in #1. `Reflect.field_type` already returns the type name per field, so once #1 lands a canonical value type, `serialize` is a thin walk: ``` # sketch for p in Reflect.props_of(entity) { for f in Reflect.fields_of(p) { node.set(Reflect.field_name(p, f), value_of(Reflect.get(entity, p, f), Reflect.field_type(p, f))) } } ``` Scope: - [ ] a canonical value-tree type (depends on #1 tagged unions / `any`) - [ ] `Reflect.serialize(entity) -> value` walking components + fields - [ ] `Reflect.deserialize(value) -> entity` (or apply-into-entity) as the inverse - [ ] JSON bridge (value tree <-> text) for saves and tooling Sequence this after #1. Related: #20 (the shipped reflection core), #1 (type system — prerequisite).
Author
Owner

Shipped in 9452557.

Canonical value tree — a self-describing Value node: null/int/fixed/bool/str/list/object (Value.kind returns the tag). Constructors (Value.int, Value.fixed, Value.str, Value.list, Value.object, …), builders (Value.add, Value.put), and accessors (Value.get/has/at/key_at/count/as_int/as_str). This is the concrete node the issue sketched ("int 7" / "fixed 0.5" / "list […]"); I built it directly rather than blocking on the general #1 any type — #1 stays the home for the language-level tagged union.

Reflect.serialize(entity) -> value — exactly the walk from the issue sketch: for each component the entity has, for each field, read Reflect.get + Reflect.field_type and emit a typed node. A fixed field → a fixed node, everything else → an int node, so the round-trip is bit-exact; the model id rides under a reserved "@kind" key.

Reflect.apply(entity, value) -> int — the inverse: writes a value tree's fields back into a live entity (skipping @-keys and unknown names, so partial/older saves load cleanly), returning the count applied.

JSON bridge — Json.encode / Json.parse turn the tree to and from compact, stable, diffable text. fixed is written as an exact terminating decimal (Q16.16 always terminates) and parsed back bit-for-bit — verified exact across a sweep of the raw Q16.16 range (the naive 5-digit approach failed ~66% of values; the terminating-decimal + backward-recurrence decode is exact). So serialize → encode → parse → apply reproduces every field bit-for-bit.

Additive — written in Ludic and spliced on demand (runtime/native/value.ludic + reflect_io.ludic, the Query/Light pattern). Programs that don't touch Value.*/Json.*/Reflect.serialize compile byte-identically (verified on snake/reflect/query) and x bootstrap-cfree reproduces the seed byte-for-byte.

Docs + example — examples/library/serialize.ludic (serialize → JSON → parse → apply round-trip, the issue's motivating flow), 21 namespace-method pages + Value/Json sections. Whole CI set green: x test 72/72, x test-tools 30/30, check-impl (376 ns-methods)/vocabulary/docs.

Shipped in 9452557. **Canonical value tree** — a self-describing `Value` node: `null`/`int`/`fixed`/`bool`/`str`/`list`/`object` (`Value.kind` returns the tag). Constructors (`Value.int`, `Value.fixed`, `Value.str`, `Value.list`, `Value.object`, …), builders (`Value.add`, `Value.put`), and accessors (`Value.get`/`has`/`at`/`key_at`/`count`/`as_int`/`as_str`). This is the concrete node the issue sketched ("int 7" / "fixed 0.5" / "list […]"); I built it directly rather than blocking on the general #1 `any` type — #1 stays the home for the language-level tagged union. **`Reflect.serialize(entity) -> value`** — exactly the walk from the issue sketch: for each component the entity has, for each field, read `Reflect.get` + `Reflect.field_type` and emit a typed node. A `fixed` field → a `fixed` node, everything else → an `int` node, so the round-trip is **bit-exact**; the model id rides under a reserved `"@kind"` key. **`Reflect.apply(entity, value) -> int`** — the inverse: writes a value tree's fields back into a live entity (skipping `@`-keys and unknown names, so partial/older saves load cleanly), returning the count applied. **JSON bridge** — `Json.encode` / `Json.parse` turn the tree to and from compact, stable, diffable text. `fixed` is written as an **exact terminating decimal** (Q16.16 always terminates) and parsed back bit-for-bit — verified exact across a sweep of the raw Q16.16 range (the naive 5-digit approach failed ~66% of values; the terminating-decimal + backward-recurrence decode is exact). So `serialize → encode → parse → apply` reproduces every field bit-for-bit. **Additive** — written in Ludic and spliced on demand (runtime/native/value.ludic + reflect_io.ludic, the Query/Light pattern). Programs that don't touch `Value.*`/`Json.*`/`Reflect.serialize` compile **byte-identically** (verified on snake/reflect/query) and `x bootstrap-cfree` reproduces the seed byte-for-byte. **Docs + example** — `examples/library/serialize.ludic` (serialize → JSON → parse → apply round-trip, the issue's motivating flow), 21 namespace-method pages + Value/Json sections. Whole CI set green: `x test` 72/72, `x test-tools` 30/30, check-impl (376 ns-methods)/vocabulary/docs.
orkun closed this issue 2026-08-31 14:19:42 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: workshopsoft/ludic#44
No description provided.