Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ RUN apt-get update \
libsdl2-2.0-0 \
python3 \
python3-pil \
time \
x11-apps \
xauth \
xdotool \
Expand All @@ -43,7 +44,7 @@ RUN apt-get update \

# Full EigenScript source at the pinned tag; build the gfx variant (the
# binary hard-links to src/eigenscript, which finds lib/ next to it).
ARG EIGS_REF=v0.34.0
ARG EIGS_REF=v0.35.0
RUN git clone --depth 1 --branch "${EIGS_REF}" \
https://github.com/InauguralSystems/EigenScript.git /opt/eigenscript \
&& make -C /opt/eigenscript gfx CC=gcc
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,11 @@ jobs:
with:
runCmd: |
set -e
EIGENSCRIPT=eigenscript bash tests/test_lint.sh
EIGENSCRIPT=eigenscript bash tests/test_smoke.sh
EIGENSCRIPT=eigenscript bash tests/test_lab.sh
EIGENSCRIPT=eigenscript bash tests/test_orbit_hist.sh
EIGENSCRIPT=eigenscript bash tests/test_bifurcation.sh
EIGENSCRIPT=eigenscript bash tests/test_orbit_oracle.sh
EIGENSCRIPT=eigenscript bash tests/test_orbit_mouse.sh
EIGENSCRIPT=eigenscript bash tests/test_bif_mem.sh
74 changes: 74 additions & 0 deletions FINDINGS.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,15 @@ now forcing the chart/plot widget (with eigen-sheet#26 and EigenMiniSat#76):
needed are xy series in data coordinates, axis placement at data zero,
markers, and pan/zoom interaction. For `eigenscript-ui-toolkit-engineer`.

**RESOLVED upstream — EigenScript#819 (PR #824), shipped in v0.35.0.** `chart`
was generalised in place into a data-coordinate x-y plot: per-series `x` lists,
`fixed_aspect`, labelled `vline`/`hline`/`point` markers, widget-owned
drag-pan/wheel-zoom, incremental `add_xy`/`chart_trim`, and clipping by
construction. dynamics is its first production consumer — the bifurcation view
(slice 3) is built on it with no consumer-side workarounds. The phase portrait
keeps its `canvas` on purpose: it needs a custom trail renderer, which is what
the canvas escape hatch is for. Residual friction is F-DYN-14.

## F-DYN-10 — lib/ui gap: viz widget surfaces hardcode their colours instead of reading theme keys

`_render_chart` / `_render_bar_chart` / `_render_waveform_view` in
Expand All @@ -184,6 +193,14 @@ xy chart existed today, it could not have taken the orbit-lab palette. Wants
theme keys (e.g. `plot_bg`, `plot_grid`, `plot_border`) read at render time.
For `eigenscript-ui-toolkit-engineer`.

**RESOLVED upstream — EigenScript#820 (PR #824), shipped in v0.35.0.** The viz
surfaces now read `plot_bg` / `plot_grid` / `plot_axis` / `plot_border` /
`plot_series` (and `wave_*`) off the active theme, falling back to the built-in
defaults for a theme dict that predates them. `orbit_theme.eigs` maps all five
onto the same palette entries the phase-portrait canvas reads, so chrome and
data surface are themed from the one module — verified in real pixels by the
committed screenshot.

## F-DYN-11 — lib/ui gap: the wheel event carries no cursor position

`ev.x` / `ev.y` on a `wheel` event are the scroll **deltas** (#569), and the
Expand Down Expand Up @@ -222,6 +239,63 @@ still cannot corrupt the rest of the tree), or say so in the `canvas` doc
comment next to the `on_mouse` / `on_wheel` notes. For
`eigenscript-ui-toolkit-engineer`.

## F-DYN-13 — the temporal assignment history is unbounded and arms on dead code → upstream EigenScript#827

**This one froze the box.** Building the bifurcation view (dynamics#20 rung 1,
slice 3), the new window grew **3.9 MB per rendered frame** — 258 MB at frame
30, 529 MB at frame 100, 859 MB at frame 300, then SIGSEGV against a
`ulimit -v` cap. Every correctness oracle was green throughout.

Bisected to `load_file of "physics.eigs"`, and inside it to a single token:
`frame_velocity`'s `prev of x`, in a demo helper the window **never calls**.
The compiler arms `g_trace_hist` from a whole-program scan, and the history
table it turns on (`src/trace.c`, `HistoryEntry`) is append-only with **no cap**
and holds a **reference to every assigned value**. `lib/ui`'s chart allocates a
coordinate pair per plotted point per frame (4,000 of them here), so every one
of those was pinned forever.

Minimal repro, no gfx and no lib/ui — a `prev of` inside a function that is
never called, versus the same program without it:

```
prev_off N=100000 PEAK_KB 3456 prev_on N=100000 PEAK_KB 37248
prev_off N=400000 PEAK_KB 3328 prev_on N=400000 PEAK_KB 140416
```

Flat versus linear in the iteration count. `record_history of 0` restores flat.

Note this is the *other half* of F-DYN-1: that finding recorded the silent
non-numeric coercion of the same builtin, and its "non-findings" note that
`prev` works "provided `record_history` is never called" is now qualified —
`prev` works, and arms an unbounded table for the whole program while doing it.

**In this repo:** `orbit.eigs`'s `_run` calls the documented `record_history of 0`
for the lifetime of a window session (a lab window asks no temporal questions)
and restores the previous setting on exit. `tests/test_bif_mem.sh` carries a
planted fault that removes that call and must go red, so when #827 lands the
opt-out can come out and the gate still holds. For
`eigenscript-runtime-engineer` / `eigenscript-trace-tape-engineer`.

## F-DYN-14 — lib/ui: `chart` allocates a list per plotted point per frame → upstream EigenScript#828

Surfaced as `chart`'s first production consumer. `_render_chart`'s per-sample
loop calls `_chart_map`, which returns a fresh 2-element list, for every sample
of every series on every frame — even when the data has not changed. Measured
~24.5 µs/point/frame (4,000 points ≈ 98 ms/frame, 8,800 ≈ 190 ms), so a static
diagram renders at ~10 fps and essentially all of it is allocation, not drawing.

RSS is flat, so this is not a leak in the widget (verified separately at 4,000
points over 30/100/300 frames: 130.9 / 131.1 / 131.2 MB, both styles, with and
without markers and fixed bounds) — it is throughput and allocation pressure,
and it is what turned F-DYN-13 from a slow drip into an OOM.

Also filed there: `chart_marker of ["vline", x, y, label, color]` makes the
caller invent a `y` that a vertical line ignores (and vice versa for `hline`),
which is silently accepted if wrong.

**Not** a blocker: F-DYN-9's ask (an x-y plot widget) is fully answered — the
API needed no changes to carry this view. For `eigenscript-ui-toolkit-engineer`.

---

## Non-findings (verified working — recorded to avoid re-investigating)
Expand Down
49 changes: 48 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ forcing functions), each exercising a different observer sub-surface:
actually respond is a separate question, answered by
`tests/test_orbit_mouse.sh` — real xdotool input into the real window,
verified by decoding the rendered pixels.
The window has a **second view** — press `b` or the toggle at the foot of
the control column — showing the bifurcation diagram of the logistic map
(below). Same window, same theme, same app loop; the phase portrait is what
it opens on, so nothing above changed.
Run: `eigenscript orbit_main.eigs` (needs a gfx-capable build: `make gfx`
in the EigenScript repo). Palette lives in `orbit_theme.eigs`
(DeslanStudio in-place theme-apply pattern).
Expand All @@ -97,21 +101,64 @@ is untouched):

![orbit lab zoomed](docs/orbit-lab-zoom.png)

- **logistic map / bifurcation view** (`logistic.eigs` + the orbit lab's second
view) — **built** (fleet UI ladder rung 1, slice 3). The damped oscillator is
**linear**: it has no bifurcation to draw, and neither does anything else in
this repo, so rather than fake a diagram the lab gained the standard system
that does — `x -> r x (1 - x)`. It is here because its cascade is *known*,
which makes the picture checkable: `tests/test_bifurcation.sh` compares the
plotted period-1 and period-2 branches against the closed forms `1 - 1/r` and
`(r+1 ± sqrt((r-3)(r+1)))/(2r)` (they agree to **8e-16**), locates the
doublings by bisection and puts them on `r = 3`, `r = 1 + sqrt(6)` and
`r = 3.5440903` to within 3e-4, recovers the first Feigenbaum ratio as
**4.7502** (true 4.7514), and pins the period-3 window opening exactly at
`r = 1 + sqrt(8)`. The four vertical markers on the plot are drawn where that
algebra says they are — the pitchfork lands on the line, it is not fitted to
it. This is the first production consumer of lib/ui's x-y `chart` widget
(EigenScript#819), which closed FINDINGS F-DYN-9/F-DYN-10.

![bifurcation view](docs/bifurcation.png)

The sweep is 200 parameter columns x 20 asymptotic samples = 4,000 points,
after discarding 600 transient iterations — deliberately modest, because the
widget costs ~24 µs per point per frame and more columns buy resolution, not
physics.

**It ships with a memory gate.** `tests/test_bif_mem.sh` runs the real window
at 30 / 100 / 300 frames under a `ulimit -v` cap and fails on a ceiling
breach *or* on RSS that grows with the frame count — in the steady-state
shape and in a toggle-the-view-every-frame shape, which pins that N view
switches cost exactly one sweep, one series and four markers. It is validated
by two planted faults. This is not decoration: the first working build of
this view grew **3.9 MB per frame** and died at 859 MB, and every correctness
oracle stayed green the whole time. The cause was a runtime bug, not the
sweep — see FINDINGS F-DYN-13 / EigenScript#827.

Forcing-function findings (runtime gaps surfaced while building) are logged in
[FINDINGS.md](FINDINGS.md) — most have graduated to upstream fixes
(#255/#256/#280/#375); a calling-convention edge remains open.
(#255/#256/#280/#375, and #819/#820 which closed the two lib/ui plot gaps);
a calling-convention edge remains open, and building this rung surfaced two
more: EigenScript#827 (unbounded temporal assignment history — the one that
froze a box) and #828 (chart render allocation).

## Develop locally

```sh
eigenscript dynamics.eigs # parse + run the entry point
bash tests/test_lint.sh # --lint clean across every .eigs in the repo
bash tests/test_smoke.sh # stage as a consumer would and import
bash tests/test_lab.sh # run the standalone lab programs
bash tests/test_orbit_hist.sh # trajectory history: bounded live, complete on dump
bash tests/test_bifurcation.sh # bifurcation oracle: plotted data vs closed-form algebra
bash tests/test_orbit_oracle.sh # UI oracle: headless vs UI-stepped byte-diff
bash tests/test_orbit_mouse.sh # mouse + render-decode oracle (real input, real pixels)
bash tests/test_bif_mem.sh # memory gate: peak RSS capped and flat in frame count
```

Every one of those that can fail silently is validated with a **planted fault**
— a deliberately broken copy that the checker must reject — so a green run is
evidence the checker still discriminates, not just that nothing threw.

CI builds EigenScript from source on Linux (the gfx variant, under Xvfb) and runs
every one of those scripts on every push and PR (see
`.github/workflows/test.yml`).
Expand Down
Binary file added docs/bifurcation.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion dynamics.eigs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
# names stay private. This 0.1.0 seed is the equilibrium-relaxation primitive;
# the physics ζ-sweep, Conway, and iterative-solver modules build on it.

VERSION is "0.1.0"
VERSION is "0.1.0" # lint: allow W001 -- exported package surface

# Relax a value from `start` toward `target` by geometric steps of factor
# `rate` (0 < rate < 1), stopping when the observer reports the trajectory has
Expand Down
Loading