Let structs and lists nest to any depth #239

Closed
opened 2026-09-26 21:50:35 +02:00 by meleki · 2 comments
Owner

Structs and lists can only nest in two fixed shapes today, each two levels deep:

  • list -> struct -> flat values (a list of structs whose fields are flat),
  • struct -> list -> flat values (a struct field holding a list of flat items).

A struct in a struct, a list in a list, a list of structs with a list field, or a struct field holding a list of structs are not possible. The bound is a design choice from f60358c / 10d0303 (finite shapes, no path needed for an inspector row, no nested declaration), not a technical limit: the values are JSON, and withoutObjects in core/graph/liveObjects.ts already walks them recursively.

Decision: lift the bound completely, any depth. No third fixed level, because every fixed level adds exactly the special cases the code consists of today.

What changes

Kinds and values

  • ListItemKind and StructFieldKind (core/properties/kinds.ts:203-205) both become "any field kind plus list plus struct".
  • StructFieldDef becomes recursive: a field of kind struct declares its own fields, a field of kind list stays undeclared (items carry their own kind, as today).
  • The flat special cases go: FlatItem, flatItemOf, flatListOf, fieldKindOf, makeFlatItem, fieldLike (core/properties/listItems.ts). Items and fields are read, written and coerced by one recursive path. Nothing deeper is dropped silently any more.
  • structOf, coerceFieldValue, sameValue, text conversion and the save path become recursive.
  • Older projects stay valid unchanged: every shape stored so far is a special case of the new ones. A test loads the old shapes and checks they come back identical.

Editor

  • Inspector: + Field -> Struct and + Field -> List on every level of a struct declaration, nested struct fields shown inline, indented one step per level.
  • List tab: items of kind list and struct on every level. A nested list opens in the List tab with a path in its title (for example Enemies · [3] · drops).
  • ListSlot.field (one key) becomes a path (keys and indices), so every nested row has an address. Preview, commit and undo go through that path and rebuild the whole value from the owner, the same rule as today one level deep.
  • + Item offers list and struct everywhere (ListAccess.kinds is no longer cut to FIELD_KINDS inside a struct field).

Nodes

  • Pack, Expand and Item at stay level by level: nested data is taken apart by chaining them. No path syntax like a.b[2].c.
  • To check while building: what Pack writes and Expand / Item at hand out at depth 3+ must survive the coercion into a variable unchanged (today a nested value past the bound is dropped there).

Decisions (see comment)

  1. Nested struct fields are declared, like a top-level struct variable. Lists stay undeclared, a struct item in a list takes its shape from its value at any depth.
  2. Named struct types: not here, own issue (struct asset).
  3. Inspector: nested struct fields are indented further, no fold arrows for now. Lists keep opening in the List tab.
  4. No depth limit.

Not in scope

  • Named / reusable struct types (see question 2).
  • Path access in one node.
  • Any change to what an object at rest is ({"$object": ""}, #238).

Tests (plan)

  • Unit: round trip of every nesting at depth 3+, old shapes load unchanged, sameValue and coercion recursive, objects in deep structures emptied on every way out (#238 exits).
  • Unit: undo of an edit three levels deep in inspector and List tab.
  • e2e: + Field -> Struct inside a struct and + Item -> List inside a list with real clicks, and the path in the List tab title.
  • Counter-checks as usual (bring back the depth bound, the tests must fail).
Structs and lists can only nest in two fixed shapes today, each two levels deep: - list -> struct -> flat values (a list of structs whose fields are flat), - struct -> list -> flat values (a struct field holding a list of flat items). A struct in a struct, a list in a list, a list of structs with a list field, or a struct field holding a list of structs are not possible. The bound is a design choice from `f60358c` / `10d0303` (finite shapes, no path needed for an inspector row, no nested declaration), not a technical limit: the values are JSON, and `withoutObjects` in `core/graph/liveObjects.ts` already walks them recursively. Decision: lift the bound completely, any depth. No third fixed level, because every fixed level adds exactly the special cases the code consists of today. ## What changes ### Kinds and values - `ListItemKind` and `StructFieldKind` (`core/properties/kinds.ts:203-205`) both become "any field kind plus `list` plus `struct`". - `StructFieldDef` becomes recursive: a field of kind `struct` declares its own `fields`, a field of kind `list` stays undeclared (items carry their own kind, as today). - The flat special cases go: `FlatItem`, `flatItemOf`, `flatListOf`, `fieldKindOf`, `makeFlatItem`, `fieldLike` (`core/properties/listItems.ts`). Items and fields are read, written and coerced by one recursive path. Nothing deeper is dropped silently any more. - `structOf`, `coerceFieldValue`, `sameValue`, text conversion and the save path become recursive. - Older projects stay valid unchanged: every shape stored so far is a special case of the new ones. A test loads the old shapes and checks they come back identical. ### Editor - Inspector: `+ Field -> Struct` and `+ Field -> List` on every level of a struct declaration, nested struct fields shown inline, indented one step per level. - List tab: items of kind `list` and `struct` on every level. A nested list opens in the List tab with a path in its title (for example `Enemies · [3] · drops`). - `ListSlot.field` (one key) becomes a path (keys and indices), so every nested row has an address. Preview, commit and undo go through that path and rebuild the whole value from the owner, the same rule as today one level deep. - `+ Item` offers `list` and `struct` everywhere (`ListAccess.kinds` is no longer cut to `FIELD_KINDS` inside a struct field). ### Nodes - `Pack`, `Expand` and `Item at` stay level by level: nested data is taken apart by chaining them. No path syntax like `a.b[2].c`. - To check while building: what `Pack` writes and `Expand` / `Item at` hand out at depth 3+ must survive the coercion into a variable unchanged (today a nested value past the bound is dropped there). ## Decisions (see comment) 1. Nested struct fields are **declared**, like a top-level struct variable. Lists stay undeclared, a struct item in a list takes its shape from its value at any depth. 2. Named struct types: not here, own issue (struct asset). 3. Inspector: nested struct fields are indented further, no fold arrows for now. Lists keep opening in the List tab. 4. No depth limit. ## Not in scope - Named / reusable struct types (see question 2). - Path access in one node. - Any change to what an object at rest is (`{"$object": ""}`, #238). ## Tests (plan) - Unit: round trip of every nesting at depth 3+, old shapes load unchanged, `sameValue` and coercion recursive, objects in deep structures emptied on every way out (#238 exits). - Unit: undo of an edit three levels deep in inspector and List tab. - e2e: `+ Field -> Struct` inside a struct and `+ Item -> List` inside a list with real clicks, and the path in the List tab title. - Counter-checks as usual (bring back the depth bound, the tests must fail).
Author
Owner

Decisions (agreed 2026-09-26):

  1. Nested struct fields are declared. A struct field of kind struct carries its own fields, exactly like a top-level struct variable, and gets its own + Field in the inspector. A value coming in over an edge is brought into that shape. Lists stay undeclared: a struct item in a list keeps taking its fields from its value, at any depth. This changes only with the struct asset (see 2).
  2. Named struct types are not part of this issue. They come as their own asset kind in a separate issue; a declared nested shape is what such an asset would later replace.
  3. Inspector: nested struct fields are simply indented further. No fold arrows for now. Lists keep opening in the List tab.
  4. No depth limit.
Decisions (agreed 2026-09-26): 1. **Nested struct fields are declared.** A struct field of kind `struct` carries its own `fields`, exactly like a top-level struct variable, and gets its own `+ Field` in the inspector. A value coming in over an edge is brought into that shape. Lists stay undeclared: a struct item in a list keeps taking its fields from its value, at any depth. This changes only with the struct asset (see 2). 2. **Named struct types are not part of this issue.** They come as their own asset kind in a separate issue; a declared nested shape is what such an asset would later replace. 3. **Inspector:** nested struct fields are simply indented further. No fold arrows for now. Lists keep opening in the List tab. 4. **No depth limit.**
Author
Owner

Umgesetzt in 0c5ac32, Wiki in 19fa428 (Behaviors, Development).

Verhalten

  • Structs und Listen lassen sich beliebig tief verschachteln, ohne Limit.
  • Struct im Struct wird deklariert (Variante A): im Inspector bietet „+ Field“ auf jeder Ebene auch Struct und List an, die Felder stehen eingerückt darunter. Struct-Items in Listen nehmen ihre Felder weiter aus dem Wert.
  • Listen-Tab: Ein Listen-Item zeigt „n items“ und öffnet die Liste eine Ebene tiefer, der Titel zeigt den Pfad (Listy · nums · [3]).
  • Pack in Pack schreibt ein Struct ins Struct-Feld, zweimal Expand holt es wieder heraus. Objekte bleiben in jeder Tiefe erhalten und werden an jedem Ausgang geleert.
  • Nichts wird mehr stillschweigend verworfen (Laden, Node-Einstellungen, Schreiben in Variablen). Alte Projekte laden unverändert.

Aufbau

  • Eine Art-Liste NESTED_KINDS, die flachen Sonderfälle in listItems.ts sind weg.
  • Neu src/core/properties/valuePath.ts, ListSlot.field heißt jetzt ListSlot.path.
  • Feld-Befehle bekommen den Pfad zum Struct und ändern Deklaration, Wert und Overrides in einem Undo-Schritt.

Tests

  • ./check.sh: 172 Dateien, 2498 Tests, CHECK EXIT 0
  • e2e 21 + 22: ALL PASSED, 90 Checks (neu 21d-iii und 22f-ii, 21d und 22f nachgezogen)
  • 20 neue Unit-Tests (nested-values.test.ts, nested-list-slot.test.ts), drei Tests zur alten Grenze umgedreht
  • 6 Gegenproben, alle greifen (je 1 bis 8 Tests fallen)
  • Nicht abgedeckt: ein verschachteltes Struct innerhalb eines Struct-Items im Listen-Tab hat keinen e2e-Klick.

Benannte Struct-Typen (Struct-Asset) kommen wie besprochen in ein eigenes Issue.

Umgesetzt in 0c5ac32, Wiki in 19fa428 (*Behaviors*, *Development*). **Verhalten** - Structs und Listen lassen sich beliebig tief verschachteln, ohne Limit. - Struct im Struct wird deklariert (Variante A): im Inspector bietet „+ Field“ auf jeder Ebene auch Struct und List an, die Felder stehen eingerückt darunter. Struct-Items in Listen nehmen ihre Felder weiter aus dem Wert. - Listen-Tab: Ein Listen-Item zeigt „n items“ und öffnet die Liste eine Ebene tiefer, der Titel zeigt den Pfad (`Listy · nums · [3]`). - `Pack` in `Pack` schreibt ein Struct ins Struct-Feld, zweimal `Expand` holt es wieder heraus. Objekte bleiben in jeder Tiefe erhalten und werden an jedem Ausgang geleert. - Nichts wird mehr stillschweigend verworfen (Laden, Node-Einstellungen, Schreiben in Variablen). Alte Projekte laden unverändert. **Aufbau** - Eine Art-Liste `NESTED_KINDS`, die flachen Sonderfälle in `listItems.ts` sind weg. - Neu `src/core/properties/valuePath.ts`, `ListSlot.field` heißt jetzt `ListSlot.path`. - Feld-Befehle bekommen den Pfad zum Struct und ändern Deklaration, Wert und Overrides in einem Undo-Schritt. **Tests** - `./check.sh`: 172 Dateien, 2498 Tests, `CHECK EXIT 0` - e2e 21 + 22: ALL PASSED, 90 Checks (neu 21d-iii und 22f-ii, 21d und 22f nachgezogen) - 20 neue Unit-Tests (`nested-values.test.ts`, `nested-list-slot.test.ts`), drei Tests zur alten Grenze umgedreht - 6 Gegenproben, alle greifen (je 1 bis 8 Tests fallen) - Nicht abgedeckt: ein verschachteltes Struct innerhalb eines Struct-Items im Listen-Tab hat keinen e2e-Klick. Benannte Struct-Typen (Struct-Asset) kommen wie besprochen in ein eigenes Issue.
admin added this to the Development project 2026-09-27 11:28:31 +02:00
admin moved this to Done in Development on 2026-09-27 11:29:15 +02:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: meleki/feuerwerk#239