From 848daa74f8103c2128bd1484aa295e64f7f6e5f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Vogel?= <104342347+cedricvogel-leo@users.noreply.github.com> Date: Thu, 5 Nov 2026 00:41:58 +0100 Subject: [PATCH] Visualizer: no step for "-x" where it reads as its value; console check without buttons "-x" with x = 7 reads "-7" once x is replaced, so the minus records no step of its own; "--7" and "-True" keep theirs. The browser check's console text leaves out the entries' Visualize buttons, which land next to a ">>>" line or on a line of their own depending on the width. Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 2 +- README.md | 4 +++- scripts/check-console-browser.mjs | 10 +++++++++- scripts/check-runtime-lists.py | 15 ++++++++++++++- src/runtime/visualizer.py | 8 +++++++- 5 files changed, 34 insertions(+), 5 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index be0b37f..df17007 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -55,7 +55,7 @@ There is no unit test framework. The automated checks are `test:protocol` (plain - Values are shown through `_glpython_safe_repr`, which writes types as learners do (`int`, not ``); the program's own output keeps Python's repr. - It abbreviates long values (`[1, 2, 3, 4, 5, …, 100]`; levels in `_GLPYTHON_ABBREVIATION_LEVELS`) and notes each full text. The next recorded step gets `fullValues` (`{shown text: index}`), the trace result `values` (`{text, cut, length, kind}`). The UI (`fullValuesUpTo`, `ValueText`, `LocalsValue`, `FullValueDialog`) opens one on click. - A name indexed in a subscript (`dauer` in `dauer[1]`, `_glpython_indexed`) gets no step of its own, so `dauer[1]` is one step to the element. - - A display (`[…]`, `(…)`, `{…}`) gets no step of its own (`_glpython_record_display`), only where its value reads differently (unpacking, duplicates); nor does a negative number literal (`_glpython_is_negative_number`). Every step must change something. + - A display (`[…]`, `(…)`, `{…}`) gets no step of its own (`_glpython_record_display`), only where its value reads differently (unpacking, duplicates); nor does a negative number literal (`_glpython_is_negative_number`), or `-name` where it already reads as its value. Every step must change something. Worker messages: - `run` → `result`/`error`; while it runs, `output` messages (`{id, text}`, `id` undefined for a program, the input's id for a REPL input) stream what was printed, at most 50 per 50 ms. The final message brings the whole output again. diff --git a/README.md b/README.md index 548e4e3..4c5a88b 100644 --- a/README.md +++ b/README.md @@ -142,7 +142,9 @@ no steps at all, nested at any depth and with negative numbers (`-3`, `-2.5`): only the parts that change have steps: `[1 + 2, 3, 4] → [3, 3, 4]`, `["+", 1, ["*", n, 3]] → ["+", 1, ["*", 2, 3]]`. A display keeps its step only where its value reads differently: unpacking (`[*a, 2] → [1, 2]`) and a set or -dict whose duplicates go (`{1, 1} → {1}`). +dict whose duplicates go (`{1, 1} → {1}`). Likewise `-x` with `x = 7` is one +step, to `-7`; the minus has a step of its own only where it changes what is +shown (`-x` with `x = -7` reads `--7`, then `7`). A long value is abbreviated, in the step and in the variables panel alike, keeping its first elements and its last one: `[1, 2, 3, 4, 5, …, 100]`. diff --git a/scripts/check-console-browser.mjs b/scripts/check-console-browser.mjs index 8be01b3..745f117 100644 --- a/scripts/check-console-browser.mjs +++ b/scripts/check-console-browser.mjs @@ -167,7 +167,15 @@ try { await page.waitForFunction(() => window.ready, null, { timeout: 120000 }) return page } - const consoleText = (frame) => frame.locator('.console').innerText() + // The transcript as it reads, without the entries' Visualize buttons: next + // to a ">>>" line or on a line of their own, depending on the width. + const consoleText = (frame) => frame.locator('.console').evaluate((console) => { + const buttons = [...console.querySelectorAll('.repl-entry-visualize')] + for (const button of buttons) button.style.display = 'none' + const text = console.innerText + for (const button of buttons) button.style.display = '' + return text + }) const programLine = (frame) => frame.locator('.repl-line--program .repl-input') // A REPL input, once the REPL is free again (until then it keeps the text). const enterRepl = async (frame, text) => { diff --git a/scripts/check-runtime-lists.py b/scripts/check-runtime-lists.py index aca88e3..3411d3d 100644 --- a/scripts/check-runtime-lists.py +++ b/scripts/check-runtime-lists.py @@ -121,13 +121,26 @@ def steps_on_line(result, line): ("a = 5", "w = [a, 2]", [("a", "5")], "[5, 2]"), ("x = 7", "w = [[1, 2], x]", [("x", "7")], "[[1, 2], 7]"), ("n = 2", 'w = ["+", 1, ["*", n, 3]]', [("n", "2")], "['+', 1, ['*', 2, 3]]"), - ("n = 2", "w = (n, -n, {n: [n]})", [("n", "2"), ("n", "2"), ("-2", "-2"), ("n", "2"), ("n", "2")], "(2, -2, {2: [2]})"), + ("n = 2", "w = (n, -n, {n: [n]})", [("n", "2"), ("n", "2"), ("n", "2"), ("n", "2")], "(2, -2, {2: [2]})"), ("", "w = [len([1, 2]), 3]", [("len(...)", "2")], "[2, 3]"), ]: result = trace(f"{setup}\n{line}\n") expect(f"one step each: {line}", evals_on_line(result, 2), expected) expect(f"the assignment: {line}", [label for kind, label, _ in steps_on_line(result, 2) if kind == "exec"], [f"w = {value}"]) +# "-x" is one step, to x's value, where that already reads as the result; +# where it doesn't ("--7", "-True", "-(a + b)"), the minus has its step. +for setup, line, expected in [ + ("x = 7", "w = -x", [("x", "7")]), + ("x = 2.5", "w = -x", [("x", "2.5")]), + ("x = -7", "w = -x", [("x", "-7"), ("--7", "7")]), + ("x = True", "w = -x", [("x", "True"), ("-True", "-1")]), + ("a = 1\nb = 2", "w = -(a + b)", [("a", "1"), ("b", "2"), ("1 + 2", "3"), ("-3", "-3")]), +]: + result = trace(f"{setup}\n{line}\n") + row = setup.count("\n") + 2 + expect(f"unary: {setup}; {line}", evals_on_line(result, row), expected) + # Where the value reads differently from the display, the display has its # step: unpacking, and duplicates in a set or dict. Unpacking as in Python. for setup, line, expected in [ diff --git a/src/runtime/visualizer.py b/src/runtime/visualizer.py index a53fdc6..332f478 100644 --- a/src/runtime/visualizer.py +++ b/src/runtime/visualizer.py @@ -611,7 +611,13 @@ def _eval_unaryop(node, scope): if _glpython_is_negative_number(node): return value symbol = _GLPYTHON_OP_SYMBOLS.get(type(node.op), "?") - _glpython_record(node, "eval", f"{symbol}{_glpython_safe_repr(operand, 40)}", _glpython_safe_repr(value), _glpython_current_func()) + text = f"{symbol}{_glpython_safe_repr(operand, 40)}" + value_text = _glpython_safe_repr(value) + # "-x" with x = 7 already reads "-7" once x is replaced: no step that + # changes nothing (but "-x" with x = -7 reads "--7", which becomes 7). + if isinstance(node.op, ast.USub) and isinstance(node.operand, ast.Name) and text == value_text: + return value + _glpython_record(node, "eval", text, value_text, _glpython_current_func()) return value def _eval_boolop(node, scope):