Skip to content

Lists as trees (listtree, P34 B) - #51

Merged
cedricvogel-leo merged 3 commits into
mainfrom
feature/list-trees
Nov 5, 2026
Merged

cedricvogel-leo merged 3 commits into
mainfrom
feature/list-trees

Conversation

@cedricvogel-leo

@cedricvogel-leo cedricvogel-leo commented Nov 5, 2026 •

Copy link
Copy Markdown
Collaborator

First part of P34: TreeView + B (lists as trees), as decided. «Baum» (A) and the IDE link (C) follow later.

What

Rule (generic): a non-empty list whose first element is a string is a tree. That string is the node's label, and the other elements are its children: a subtree where they meet the rule too, otherwise a leaf. Labels appear without quotes and string leaves with quotes, so ['+', 'a', 'b'] reads as an operator with two text values. The list's text always stays as it is, with the tree below it.

Where:

  • Variables panel (Ersetzung and Kasten, including the boxes' own panels): the tree below the value.
    • A tree wider than 48 characters doesn't fit the panel and gets the link «Baum zeigen», which opens it in the full-value dialog instead.
    • ⋮ Optionen has the switch «Listen als Baum zeigen». It's on when the feature is on.
  • Full-value dialog (P39): the tree below the value's text, scrollable if it's large.
  • REPL: a value that reads as a tree has a «Baum» button next to «Visualisieren», in the same style (no underline while it's open; aria-pressed says it's open). It shows the tree below the value in the transcript and hides it again. For parse(…) in the REPL, this is your point 6.6.
  • Not in the step values: a tree inside a code line is too big; that place belongs to «Baum» (A).

Off by default:

  • Capability listTree, embed feature listtree. Like coverage, it isn't part of «everything», so published courses don't change.
  • Without the feature there are no trees, no «Baum» button and no menu entry.
  • The IDE doesn't set the capability; that comes with C.

Limits:

  • A tree of more than 100 nodes, deeper than 10, or containing itself isn't drawn.
  • Labels and leaves longer than 24 characters are shortened in their middle.

Text alternative: each tree is an image for screen readers, described by the list's text.

TreeView (shared with A later):

  • Positions: our own port of van der Ploeg (2014), "Drawing non-layered tidy trees in linear time" (Software: Practice and Experience 44(12)): Reingold–Tilford for nodes of different widths. It's in src/treeLayout.ts (about 200 lines), follows the Java code in the paper's appendix and is cited there.
    • It includes d3-flextree's two changes to that code, so the positions are the same:
      • positions are a node's centre, not its left edge;
      • a subtree first moves next to its left sibling, also to the left, before the lower rows push it apart.
    • Without them, positions differed from flextree's: a short leaf next to a sibling with wide children stayed up to half a character further away than needed.
    • Every node is one row high, with the same node widths and 2-character gap as before. A parent is centred over the span from its first child's left edge to its last child's right edge.
    • d3-flextree is a devDependency only: the oracle in test:layout. Nothing of it or of d3-hierarchy is in dist/, dist-embed/ or THIRD_PARTY_NOTICES.md.
  • Rendering stays ours: HTML nodes with widths in ch, SVG edges, --glp-* tokens only. Positions are in characters of the code font and in em, so a tree scales with the font around it, also with size=large.
  • Narrow view: a tree wider than its place (console, dialog, a narrow panel) scrolls inside TreeView.
    • It starts with its root in the middle, and stays centred while its place changes size, until it has been scrolled.
    • A hint at each edge where more is hidden: the tree fades into the background behind it (the colour is taken from the tree's container), with a plain grey ‹ or › centred over the tree's whole height. It has no border and no node colour. Each disappears once you've scrolled to its edge.
    • It takes the focus only when it is wider than its place, and scrolls with ← →, Home and End. The focus ring is the usual --glp-accent outline.

Layout measurement: width in characters

Tree Before (own layout) d3-flextree
breit 62 62
parse("2 + 3 * 4") = ['+', 2, ['*', 3, 4]] 13 10.5
parse("(2 + 3) * 4") = ['*', ['+', 2, 3], 4] 13 10.5
parse("-(1 + 2)") = ['-', ['+', 1, 2]] 8 8
parse("x + 1") = ['+', ['var', 'x'], 1] 10 10
parse("f(1, 2)") = ['f', 1, 2] 8 8
['+', ['*', 2, ['-', 7, 3]], ['/', ['+', 1, 2], 3]] 28 23
['+', ['+', ['+', 1, 2], 3], ['f', ['var', 'x']]] 20 17.5

The same node widths and gap (2 characters) apply in both columns. flextree is narrower on half of the trees, by 2.5 to 5 characters (13–19 %): those with a leaf beside a deeper subtree. It never comes out wider. The parse outputs are the course's parser.py.

The table's flextree column is now our port's, and test:layout checks that it equals flextree's.

Runtime:

  • session.py has _glpython_tree_of ({label, children} / {leaf}). The REPL result carries tree.
  • The visualizer notes the trees of the panel's values like P39's full values: step trees: {text: index}, result trees: [...], each tree stored once.

Screenshots at 1000 px and 420 px (console and dialog, in the LiaScript player with glpython-liascript#30) are in my report in the session.

Checks

  • New scripts/check-runtime-trees.py, added to test:runtime:
    • the rule (your examples, ['Anna', 'Ben'], ['f'], leaves of every kind, shortened texts);
    • no tree for other values, too big trees and a list containing itself;
    • the REPL's tree;
    • the visualizer's step trees, stored once, and no new full values.
  • scripts/check-console-browser.mjs, with features=listtree:
    • the panel tree's nodes in order and its text alternative;
    • no two nodes overlap, children sit below their parent, the tree stays within the panel;
    • a wide tree opens in the dialog;
    • the menu switch turns trees off and on;
    • the REPL's «Baum» opens and closes the tree, with no button for [1, 2].
  • New scripts/check-tree-layout.mjs (npm run test:layout, in CI):
    • our positions equal d3-flextree's, within 1e-6: on the eight trees of the table, with their measured widths, and on 500 random trees (0–4 children, depth ≤ 5, labels 1–12 characters, fixed seed);
    • no d3-* module in the source maps of dist/ and dist-embed/, and no flextree, d3-hierarchy or WTFPL in THIRD_PARTY_NOTICES.md. I checked that this fails when flextree is bundled.
  • Layout in the browser, for the eight trees in the table above, breit included, each opened with «Baum» in the REPL and measured in the DOM:
    • no two nodes overlap;
    • siblings are in order from left to right;
    • every parent is above its children and centred over them.
  • Narrow (420 px), in the console and in the dialog, for breit:
    • the tree is wider than its place, and its root is in the middle (±2 px);
    • both hints show, and the tree takes the focus;
    • the hints have no border, cover the tree's whole height, and have a colour none of its nodes has;
    • Home leaves only ›, → scrolls, End leaves only ‹.
  • The same check without the feature: no trees, no switch, no «Baum».
  • The P36 check's console text also leaves out the «Baum» buttons.
  • Locally, lint, test:runtime, test:protocol, test:hints, test:layout and test:console pass.

How it goes live / testing in a course

  1. Merge: nothing changes without the feature, so this is safe for published courses.
  2. Macro: glpython-liascript#30 adds listtree (off by default, like coverage). Merge it after this PR. A course that uses it needs a patch version bump.
  3. IDE: a tag only matters once the IDE turns it on (C).

Manual tests (desktop, and iPad with an external keyboard)

In a block with features=run,repl,visualize,listtree:

  1. Visualize baum = parse("2 + 3 * 4") with your parse. The variables show the text and below it the tree + → 2, * → 3, 4.
  2. Visualize t = parse("-(1 + 2)"), u = parse("f(1, 2)"), v = parse("x + 1"), w = parse('"a" + "b"'):
    • v: var with the leaf 'x';
    • w: + with 'a' and 'b'.
  3. A large tree (e.g. parse("(1 + 2) * (3 - 4) / f(5, 6, 7)")): «Baum zeigen» opens the dialog. Esc closes it.
  4. ⋮ Optionen → «Listen als Baum zeigen»: the trees go away, and choosing it again brings them back. Step back and forth with the keyboard (← →).
  5. In Kasten, auswerten(parse("2 + 3 * 4")) with your recursive evaluator: each box's baum parameter shows its subtree. On the iPad, check the space with nested boxes.
  6. REPL: parse("2 + 3 * 4") → «Baum» shows the tree and hides it again. [1, 2] has no «Baum».
  7. In a block without listtree: nothing of this appears.
  8. Narrow view (iPad in portrait, or the LiaScript page at about 420 px): REPL breit = ["f", ["g", "erstes", "zweites"], ["h", "drittes", "viertes"], ["k", "fünftes"]], then breit → «Baum».
    • The tree starts with f in the middle. Both edges fade out, each with a grey ‹ or ›, and the hints read as part of the frame, not as nodes.
    • Swipe or scroll to one end: that edge's hint goes away.
    • Visualize breit too: «Baum zeigen» opens the dialog, also centred, with hints.
  9. Keyboard (desktop, and iPad with an external keyboard): Tab to the wide tree. It shows a focus ring, and ← → move it, Home and End go to its ends. A tree that fits gets no Tab stop.
  10. «Baum» and «Visualisieren» in the console look the same, also while the tree is open.

Follow-ups (not fixed)

  • The width threshold for the panel (48 characters) is a fixed number, not the panel's real width. Tune it once you've tested with real parse output.
  • Height jumps: the next PR, before «Baum» (A), as agreed.
  • In a tree of three rows, the middle of its height is the middle row, so the arrows sit level with that row's nodes. The fade and the missing border keep them apart.

🤖 Generated with Claude Code

cedricvogel-leo and others added 3 commits November 5, 2026 15:45
A list whose first element is a string is drawn as a tree below its text:
in the visualizer's variables (switch in the options menu), in the
full-value dialog and, on request («Baum»), for a REPL value. The runtime
sends the trees (_glpython_tree_of); TreeView draws them with the package's
own layout. Behind capability listTree / embed feature listtree, off by
default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… narrow

The node positions now come from d3-flextree (van der Ploeg 2014, tidy
trees for nodes of different widths), which moves subtrees together where
their rows allow; rendering stays ours. A tree wider than its place scrolls
inside TreeView: it starts with the root in the middle, shows ‹ › where
more is hidden, and takes the focus for the arrow keys, Home and End. The
console's «Baum» is no longer underlined while open. Browser checks for the
layout of parser-like and uneven trees and for the narrow view.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
treeLayout.ts implements van der Ploeg (2014) itself, with d3-flextree's
changes (centres, a subtree first moved next to its sibling), so the
positions stay the same. d3-flextree is a devDependency for test:layout
only, which compares the positions on the course's trees and 500 random
ones and checks that nothing of d3 is in the builds or the notices. The
scroll hints are a fade with a plain arrow over the tree's whole height,
without a border or a node colour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cedricvogel-leo
cedricvogel-leo merged commit d9fedaa into main Nov 5, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant