Skip to content

Refactor to LET — PR 1: tracer (spec 0008)#206

Merged
jimmytacks merged 5 commits into
mainfrom
issue-202-refactor-tracer
May 28, 2026
Merged

Refactor to LET — PR 1: tracer (spec 0008)#206
jimmytacks merged 5 commits into
mainfrom
issue-202-refactor-tracer

Conversation

@jimmytacks
Copy link
Copy Markdown
Collaborator

@jimmytacks jimmytacks commented May 27, 2026

Closes #202

Tracer-bullet slice of spec 0008 — /Refactor. End-to-end loop on the simplest case: a non-LET formula with cell refs and ranges (including spill refs), the dialog with rename / Include / reorder, and write-back via Formula2. Existing LETs are refused with a "coming in PR 2" message.

Summary

  • Shared infra: FormulaRef gains IsSpilled (equality + hash + DisplayAddress) so A1 and A1# dedupe as distinct refs. CellRefExtractor.BuildFormulaRef populates it; CellRefExtractor.Rewrite falls back from spilled → non-spilled when the spilled key isn't in the lookup, preserving /Gather's PR 5 token-collapse behaviour. CellGraphWalker normalises the spill flag out of cell-level precedents (graph identity already lives on CellRef; spill-ness on WalkedCell.HasSpill).
  • Engine: RefactorEngine.Refactor(formula, activeSheet) returns auto-named bindings + the synthesised LET. Recompute(...) re-runs with per-row state (rename / Include drop / reorder).
  • UI: RefactorToLetWindow — read-only formula header, inputs list with Include checkbox + rename + drag/Alt-Up-Down reorder, live preview, Save/Cancel. Per-row + cross-row name validation gates the Save button.
  • Slash command: /Refactor registered in LambdaPopup; RefactorCommand mirrors GatherCommand's threading + diagnostic pattern, writes back via Formula2.

Test plan

  • dotnet build addin/lambda-boss.slnx — clean
  • dotnet test addin/lambda-boss.Tests — 921 passing (up from 900: 13 new RefactorEngineTests + 8 new CellRefExtractorSpillTests)
  • Manual Excel smoke: =IF(A1<10, IF(A1>2, SUM(B1:B5), 0), SUM(B2:B6))/Refactor → Save — confirmed by Tim that the resulting LET evaluates identically
  • Reviewer: run a /Gather over a chain containing a spilling anchor to confirm the walker normalisation didn't regress anything subtle

Reference

🤖 Generated with Claude Code

jimmytacks and others added 5 commits May 27, 2026 13:55
Spec defines the /Refactor slash command and the four-PR delivery
slicing. Plan details engine/UI split, reuse from /Gather, and the
PR-by-PR scope.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
FormulaRef gains a bool IsSpilled field with equality + hash + DisplayAddress
rendering so A1 and A1# dedupe as distinct refs — spec 0008's /Refactor
needs them as separate LET bindings. CellRefExtractor.BuildFormulaRef
populates the flag from the existing (?<spill>#) regex group.

/Gather is preserved through two seams: CellGraphWalker normalises the
spill flag out of cell-level precedents (graph identity already lives on
CellRef, and spill-ness on WalkedCell.HasSpill), and CellRefExtractor.Rewrite
falls back from a spilled key to its non-spilled equivalent when the
spilled key isn't in the lookup — so /Gather's existing PR 5 behaviour of
collapsing A1# tokens to the anchor's binding name still holds.

The two existing CellRefExtractor tests that pinned the old "A1 and A1#
dedupe to one ref" rule are updated to reflect the new spec'd behaviour.
A new CellRefExtractorSpillTests file covers the IsSpilled propagation
and the two rewriter paths.

All 908 existing tests stay green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
End-to-end loop on the simplest case: a non-LET formula with cell refs
and ranges (including spill refs). The author runs /Refactor on the
active cell, picks names + reorders in the dialog, and saves — the
synthesised =LET(...) replaces the cell formula via Formula2.

Engine (RefactorEngine + RefactorTypes):
- Refactor(formula, activeSheet) extracts via CellRefExtractor.Extract,
  dedupes by FormulaRef (which now distinguishes spill from anchor),
  assigns inputN auto-names, rewrites via CellRefExtractor.Rewrite, and
  emits the synthesised LET via FormulaFormatter.AppendLet.
- Recompute(formula, activeSheet, rows) re-runs with the dialog's
  per-row state — rename, Include drop, reorder.
- Existing LETs refuse with a "coming in PR 2" diagnostic. PR 2 removes
  this branch.

UI (RefactorToLetWindow):
- Read-only original-formula header, inputs list with Include checkbox,
  rename TextBox, RHS preview, drag/Alt-Up-Down reorder, and a live
  preview pane. Save returns the synthesised LET via SavedFormula.
- Per-row name validation via ExcelNameValidator plus cross-row
  uniqueness; Save is disabled until every included row is valid.

Slash command (RefactorCommand):
- Reads the active cell's formula via Formula2 (dynamic-array safe),
  runs the engine, opens the dialog on the popup UI thread, and writes
  the saved LET back. Mirrors GatherCommand's threading + diagnostic
  pattern.
- Registered in LambdaPopup.BuildCommandRegistry() under /Refactor.

Tests (RefactorEngineTests): single ref, multi-ref dedupe, range,
spill-distinct, sheet-qualifier dedupe, cross-sheet distinct, refused-LET,
no-refs literal, rename, Include drop, reorder, the spec's worked
example, and a round-trip helper that asserts every synthesised LET
parses via LetParser AND feeds cleanly through LetToLambdaBuilder.

All 921 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Nullable warnings (4 sites): net48's BCL doesn't carry the
[NotNullWhen(false)] annotation on string.IsNullOrEmpty /
IsNullOrWhiteSpace, so the analyzer can't narrow after those guards.
The parameters legitimately need to stay nullable (public/defensive
APIs that accept workbook input). Fix at the access site via the
canonical null-forgiving idiom — either `value!` after the guard, or
an `is { } local` pattern-match where it reads more naturally:

- EditLambdaCommand.TryParseLambdaCall: formula! at FindMatchingClose
- ExcelNameValidator.Validate: re-bind to a non-null local `n`
- LetNameSanitizer.Sanitize: text!.Trim()
- GatherEngine: r.NameOverride is { Length: > 0 } nameOverride

Redundant coalesce: CellRef.GetHashCode coalesced Sheet to "" before
hashing as a defensive measure for Microsoft.Bcl.HashCode's net48
null-intolerance. Sheet is annotated non-nullable, so the coalesce
is dead code — kept the ExternalWorkbook coalesce (genuinely nullable).

Polyfill namespace warning: NetFrameworkPolyfills.cs deliberately
lives in `System.Collections.Generic` so the extension method is
discoverable wherever that namespace is in scope (implicit via
Directory.Build.props). Added `// ReSharper disable once CheckNamespace`
plus a comment explaining the intent.

Unused fields: RefactorToLetWindow stored _activeSheet and
_originalFormula in fields but never read them — the constructor
already pipes them straight into the UI controls and the recompute
closure captures activeSheet via the wrapping command. Dropped both
fields and the activeSheet constructor parameter; updated the
RefactorCommand call-site accordingly.

All 921 tests still green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jimmytacks jimmytacks merged commit d8e0099 into main May 28, 2026
1 check passed
@jimmytacks jimmytacks deleted the issue-202-refactor-tracer branch May 28, 2026 12:13
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.

Refactor to LET — PR 1: tracer (non-LET formulas, refs + ranges + spill)

1 participant