Skip to content

view: fix the must-fix findings of the #2057 review - #2059

Merged
shunichironomura merged 8 commits into
feature-uifrom
address-reviews
Oct 15, 2026
Merged

shunichironomura merged 8 commits into
feature-uifrom
address-reviews

Conversation

@shunichironomura

Copy link
Copy Markdown
Member

Warning

This content was written by an AI agent and must be verified by a human developer. After human verification, this alert may be removed.

Summary

Stacked on #2057 (feature-ui), which should merge first.

This PR fixes the review findings that block #2057: one regression in the core and six critical view bugs, one commit each. Because the view is experimental, the review triaged its findings by impact:

  • A finding blocks the merge if it costs users who never open the view.
  • A finding blocks the merge if it silently corrupts sources or patches, or shows wrong values or wrong wiring.
  • Everything else is left for later: edits that fail and roll back, races, performance on large inputs, hardening, and cleanup.

The fixes:

  • The core no longer pays for the view. The language server projected a view model on every analysis. The browser engine did the same on every prepareProject, including the playground's. The projection was also quadratic: it added 0.9 s per analysis at 4,000 declarations in a release build.
    • The server now projects only for a client that declares experimental.graphcal.viewModelChanges.
    • The engine projects only through the new prepareViewProject, which web/view uses.
    • Each file's line index is built once.
  • Inline dag declarations stay out of the sheet. Template declarations had become root nodes whose ids collided with the root's own declarations, so the sheet wired readers to the wrong node. Only the root DAG is local now, and the projection rejects duplicate ids and dangling dependencies.
  • Patches place \ No newline at end of file correctly. The marker was emitted once, after the last hunk. git apply then either joined two lines or rejected the patch. The tests round-trip patches through git apply.
  • A table cell edit stays inside its own declaration. After a table written on one line, an edit could rewrite the next declaration's table. That edit compiled, so it was accepted. Edits now search only within the compiler's range for the expression, and tables on one line or shared by several declarations are refused. This is a stopgap until the compiler reports a range for each cell.
  • A nonzero value never shows as 0 in a cell. Values below 1e-6 rendered as 0 or -0. Very small and very large magnitudes now use an exponent.
  • No pane claims a bound include's values. A pinned include pane showed its module evaluated alone, but wired those values to the include's different ones. The model's node instance is now { name, bound }. A bound include cannot be pinned yet and gets no cross wires; this is also a stopgap.
  • A sheet opened after an edit reflects the edit. Modules that were off screen during an edit kept their old models. A new ModelCache marks them stale and re-projects them before they are opened, pinned or requested.

Also on this branch is a commit from another session: "web/view: VS Code edits name their copy and wait for the extension's answer". It pairs with vscode-graphcal's address-reviews branch.

Wire format: the view model's node instance changed from a string to { name, bound }. version stays at 0, since the view is experimental.

Known limitation, unchanged by this PR: the outputs of inline DAG calls (@dag(args)::out) appear in no node's deps. The graph IR has no call instances, and graphcal graph shares this gap.

Checks:

  • Run locally and passing:
    • cargo test for the graphcal-project, graphcal-lsp and graphcal-wasm libraries, and cargo test -p graphcal --test view.
    • clippy (all features and no default features) and cargo doc for the changed crates.
    • cargo fmt --check and just reading-order.
    • vp check and vp test in web/view.
  • Not run locally:
    • just hawk: the local cargo-hawk does not match the pinned toolchain. CI's public-surface job covers it.
    • The full workspace suite.

AI assistance

Breaking changes

  • No breaking changes
  • Breaking changes; the breaking change label is applied

shunichironomura and others added 8 commits October 14, 2026 20:45
The language server projected a view model on every analysis and the
browser engine on every prepareProject, the playground's included, though
only a view reads it; the projection also rebuilt a file's line index per
declaration, quadratic in the file (4,000 declarations: +0.9 s per
analysis in release).

Analysis now projects only for a client that declares
experimental.graphcal.viewModelChanges (ViewModelDemand), the engine only
through the new prepareViewProject, which web/view uses, and a prepared
project without a view says so instead of "has no view". Each file's line
index is built once.

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

The webview keeps each source copy by project and revision, sends both with
an edit, and takes the edit into its copy only once the extension answers
that it applied it; a refused edit leaves the copy as it was and shows why.
Two projects' `src/main.gcl` no longer share one copy.

Pairs with vscode-graphcal's address-reviews branch, whose extension checks
the revision against the document before applying an edit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The projection took every source module of the root file as local, the
inline dag templates included: their params and nodes became valueless
root nodes whose leaf-name ids collided with the root's own (two dags'
`out`, or a root `out` beside a template's), so the sheet's id-keyed
maps dropped one and wired readers to the wrong declaration.

Only the root DAG is local now; a template is read through its instances,
which the view already shows as externals. The projection checks that
every node id is unique and every dependency names a node, and fails
instead of returning a model the view would miswire.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The diff dropped each line's newline and put one "\ No newline at end of
file" after the last hunk, whichever line it belonged to. git applies that
marker to the line before it, so an edit far from a file's newline-less
end joined two lines (`param p4: …;param p5: …;`), and a changed last
line, or a change of the final newline alone, gave a patch git rejects.

Lines now keep their newline when compared, so `b` and `b\n` differ,
and the marker follows each newline-less line on its own side; an empty
side's hunk starts at line 0. Tests round-trip patches through git apply.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A cell edit took the declaration's table to run from its line to the
first later line holding only `}`. After a table closed on one line, that
was the next declaration's table: editing `a[x]` rewrote `b`'s `x`, which
compiled, so the edit was accepted, entered the copied patch, and in VS
Code reached the document.

The edit now searches only the rows strictly inside the compiler's range
of the expression, which must begin with `table[`; a table on one line,
or one shared by several declarations (its axes name a slot tuple), is
refused rather than guessed at. A stopgap: the compiler is to report each
cell's range.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A cell rounded the magnitude to four significant digits and then printed at
most six decimals, so anything under 1e-6 became 0 (4.7e-9 → 0, -3e-8 →
-0) and a value near 1e-6 lost its digits; large values printed in full
(1e308 as 309 digits).

Below 1e-3 and from 1e6 the four digits take an exponent instead. Editing
still starts from the full magnitude.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A pinned include pane showed its module evaluated alone, though the
include passes arguments: `include lib(gain: 2.0) as p` read `p::out = 2`
while the pane "lib.gcl as p" showed `out = 1`, wired to that 2 as its
producer.

The view model's `instance` is now `{ name, bound }`, bound when the
include binds a param or a Static parameter. A bound include cannot be
pinned yet (its menu entry says so), and no cross wire joins a bound
include's value to a pane. A stopgap until the view can project an
instance with its arguments.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An edit re-projected only the modules on screen; every other module kept
its model from before the edit, and opening or pinning it later showed
those values (b = 1 after a, which b reads, became 3), with declarations,
axes and source ranges as old as the values.

The host's models now live in a ModelCache that marks every model the
edit did not re-project as stale and never returns a stale one as current.
Asking for, opening or pinning a stale module projects it again from the
current sources first; a reset makes the models as read current again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 14, 2026
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