From 0d6c85a345ea33c806190b38e2759fd43d6e679c Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 4 Sep 2026 05:50:01 +0000 Subject: [PATCH] Re-run the papercuts against Ghost HEAD, and record what was fixed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every Ghost item in docs/papercuts.md re-executed against a build of the interpreter at ghost c31c79d, since the document's whole value is that it reports what the interpreter does rather than what SPEC.md says. Three no longer reproduce - outward assignment, loop-variable capture and block scoping, fixed upstream as one decision - along with the `list.length` hazard. They are marked fixed and kept, not deleted, since the architecture here is shaped around them and the record of why matters more than the current list. Block scoping was the one worth checking rather than assuming: this document names `Dock.arrange()` as depending on the old behaviour, and Ghost documents the change as breaking. It is unaffected - `arrange()` binds `area` and `taken` before the `switch`, so the destructuring inside each case rebinds them through the walking assignment that landed in the same change. The full 132-case suite passes against that build. The remaining items now carry the SPEC section Ghost tracks each one under and the priority it was given there, so this document and Ghost's §15 stay readable against each other. CLAUDE.md keeps the three fixed rules, marked, because they still hold against a released Ghost (the fixes are post-1.0.0-beta.3) and writing code that works under both costs nothing. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01EGsNMRiKwWmvoizxD619zA --- CLAUDE.md | 24 +++++++++++++------- docs/papercuts.md | 58 +++++++++++++++++++++++++++++++++++------------ 2 files changed, 60 insertions(+), 22 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 32015f9..1ced9cb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -33,25 +33,33 @@ calls into — widgets that belong to an application read it, the framework does ## Ghost rules this codebase is shaped around These were verified by running them, not read from the spec. See `docs/papercuts.md` for -the full list and what would fix each one upstream. +the full list, what would fix each one upstream, and the priority Ghost has assigned it. + +Three of them — outward assignment, loop-variable capture, and block scoping — are **fixed +in Ghost after `1.0.0-beta.3`** and are marked below. They are kept as rules because they +still hold against a released Ghost, and because writing code that works under both costs +nothing: state on instances and `make…` factories are correct either way. Drop them once +this repository requires a Ghost that carries the fixes. - **`and` / `or` do not short-circuit.** Both operands are always evaluated, so `x == null or x.field` still dereferences a null `x`. A null check that guards a dereference is its own `if`, never folded into the same expression. This has crashed this app in production; it is the single easiest mistake to make here. -- **A function cannot assign to a variable outside itself.** Module-level mutable state - does not work; state lives on instances. Mutating an object (`map.set`, `list.push`, - `this.field =`) is fine. -- **Closures cannot capture a loop variable.** Anything built per-item in a loop needs a - `make…` factory around the closure. +- **A function cannot assign to a variable outside itself.** *(Fixed in Ghost post-beta.3.)* + Module-level mutable state does not work; state lives on instances. Mutating an object + (`map.set`, `list.push`, `this.field =`) is fine. +- **Closures cannot capture a loop variable.** *(Fixed in Ghost post-beta.3.)* Anything + built per-item in a loop needs a `make…` factory around the closure. - **Call sibling methods as `this.method()`.** A bare call loses the receiver. - **A function held in a field cannot be called as `this.field(...)`** — that parses as a method call and method lookup does not see fields. Bind it to a local first. - **End a statement with `;` when the next line opens with `[` or `(`.** Ghost has no newline rule, so the next line otherwise continues the previous expression. This bites every destructuring assignment. -- **Blocks do not introduce a scope.** An assignment inside an `if` or a loop writes to - the enclosing function. +- **Blocks do not introduce a scope.** *(Fixed in Ghost post-beta.3 — blocks scope now, + but assignment also walks outward, so a name bound before the block is still reached. + `Dock.arrange()` was checked and is unaffected.)* An assignment inside an `if` or a loop + writes to the enclosing function. - **`/` always produces a float.** Layout arithmetic goes through `Rect` or `snap()`. - **Circular imports are a hard fault.** Keep shared types in leaf modules. - **Import by full path from the project root** (`import { Rect } from diff --git a/docs/papercuts.md b/docs/papercuts.md index b17ee70..82a5a53 100644 --- a/docs/papercuts.md +++ b/docs/papercuts.md @@ -22,7 +22,19 @@ Severity is from the point of view of someone writing a GUI toolkit: ## Ghost -### `and` / `or` do not short-circuit — *high, shipped a crash, looks exactly like the languages it's borrowing from* +**Status, reviewed against Ghost at `c31c79d` (2026-09-04).** Every item below +was re-run against a build of the interpreter at that commit. Three of them — +outward assignment, loop-variable capture, and block scoping — no longer +reproduce; they are kept as a record, marked *fixed*, rather than deleted. The +rest still reproduce exactly as written. + +Ghost now carries this list as `SPEC.md` §13.13–§13.24, and ranks the open items +against each other in `SPEC.md` §15; the `§` reference on each heading below +points at its entry there. Note that the three fixes landed *after* the +`1.0.0-beta.3` tag, so code running against a released Ghost still needs the +workarounds this repository is built around — see `CLAUDE.md`. + +### `and` / `or` do not short-circuit — *high, shipped a crash, looks exactly like the languages it's borrowing from* — Ghost §13.21, **priority 1** ```ghost target = null @@ -56,7 +68,7 @@ check has already returned before the dereferencing side is ever reached. as similar to. If a non-short-circuiting form is wanted for some reason, it should not be the operator spelled the same way `&&`/`||` are taught. -### A function cannot assign to a variable outside itself — *high, silent* +### ~~A function cannot assign to a variable outside itself~~ — *fixed*, Ghost §13.13 ```ghost scale = 1 @@ -74,7 +86,12 @@ in this toolkit lives on an instance. **Would fix it:** assignment walks the scope chain when the name already exists, or an explicit declaration keyword makes the shadowing intentional. -### Closures created in a loop cannot capture the loop variable — *high, silent until called* +**Fixed.** Ghost took the first option: assignment now rebinds the nearest existing name in +the enclosing chain, and declares locally only when the name is bound nowhere. The state-on- +instances rule this repository is built around is no longer forced, though nothing here +needs changing for it. + +### ~~Closures created in a loop cannot capture the loop variable~~ — *fixed*, Ghost §13.14 ```ghost // BROKEN: every handler raises `name error: name is not defined` when it runs @@ -89,14 +106,26 @@ closure in a `make…` factory. This is the most common shape in UI code. **Would fix it:** a per-iteration binding, the way `let` behaves in JavaScript. -### Blocks do not introduce a scope — *high, and the spec says otherwise* +**Fixed.** Both loop forms now bind their control variable once per iteration, so the +closure above captures `'pencil'` and `'eraser'` as written. The `make…` factories here are +no longer load-bearing, though they remain correct. + +### ~~Blocks do not introduce a scope~~ — *fixed*, Ghost §13.15 An assignment inside an `if`, `switch` or loop body writes to the enclosing function scope. `Dock.arrange()` depends on this; it is also how a stray temporary leaks across a long method. `SPEC.md` §8.3 states that blocks each introduce an enclosed environment. They do not. -### Everything at the top level is exported — *high, design* +**Fixed, and it did not break `Dock.arrange()`** — checked, because Ghost documents this as +the one breaking change in the batch. Blocks scope now, but assignment walks outward at the +same time (the two were fixed as a single decision), and `arrange()` binds both `area` and +`taken` *before* the `switch`, so the `[taken, area] = ...` inside each case still rebinds +the outer pair rather than declaring a local one. The whole 132-case suite passes against +Ghost at `c31c79d`. What no longer works is a name whose *first* assignment is inside a +branch — `if (c) { result = 1 }` then reading `result` — which this codebase does not do. + +### Everything at the top level is exported — *high, design* — Ghost §13.16, **priority 5** A file cannot have a private helper beside a public class, and importing a module drags its own imports' names along. One-class-per-file makes this survivable rather than solving @@ -105,7 +134,7 @@ it. **Would fix it:** an explicit `export`, with today's behaviour as the fallback for files that declare nothing. -### A method's own name shadows a same-named import, in every method of its class — *high, shipped a crash* +### A method's own name shadows a same-named import, in every method of its class — *high, shipped a crash* — Ghost §13.22, **priority 2** ```ghost import "lumen:font" // binds `font` to the module @@ -142,7 +171,7 @@ would not help here — this is member resolution order, not visibility. A class shadowing an import warrants at least a compile-time warning, since nothing else marks the two as unrelated. -### A function held in a field cannot be called through the field — *mid, loud* +### A function held in a field cannot be called through the field — *mid, loud* — Ghost §13.23, **priority 6** ```ghost this.guard = function (studio) { return true } @@ -162,32 +191,32 @@ test(studio) Found by `tests/core.gs` failing on `Command.isEnabled()`. It makes callbacks-as-fields — a normal shape for a command, a validator, a comparator — quietly awkward. -### No statics: no named constructors, no class constants — *mid* +### No statics: no named constructors, no class constants — *mid* — Ghost §14 decision 5, **declined** `Rect.fromBounds(...)` and `Button.HEIGHT` are both impossible, so factories become free functions (`asepriteDark()`) and constants move onto the theme. Workable, but it is the single thing that most distorts library shape. -### Calling a sibling method by bare name loses the receiver — *mid, and the spec says otherwise* +### Calling a sibling method by bare name loses the receiver — *mid, and the spec says otherwise* — Ghost §13.17, **priority 3** `describe()` inside another method raises ``name error: `this` can only be used inside a class`` the moment the callee touches `this`, though `SPEC.md` §8.8 says bare sibling calls are supported. Always write `this.describe()`. The error also points at the callee rather than the call. -### A field and a method may share one name — *mid* +### A field and a method may share one name — *mid* — Ghost §13.18, **priority 4** With both defined, `x.thing` reads the field and `x.thing()` calls the method, silently. Worth a diagnostic. -### Division always promotes to float — *mid* +### Division always promotes to float — *mid* — Ghost §12 `floorDiv`, **priority 8** Correct, and a tax on every line of pixel layout. `Rect` and `snap()` exist partly to pay it once. **Would fix it:** `math.floorDiv(a, b)` — `//` is taken by comments. -### Import ergonomics — *mid* +### Import ergonomics — *mid* — Ghost §13.19, **priority 10** `import "path" as name` (whole module) and `import name from "path"` (a named export) look nearly identical and mean different things. Search paths are global and accumulate as @@ -198,7 +227,7 @@ imports by full path from the project root. Also: a file's imports resolve relative to the file the interpreter was *handed*, which is why `tests/core.gs` is launched through a root-level `test.gs`. -### Reserved words are unusable as method names *and* at call sites — *mid* +### Reserved words are unusable as method names *and* at call sites — *mid* — Ghost §13.24, **priority 7** `use` is a keyword (it pulls traits into a class), so a method called `use` will not parse — and neither will `cursors.use('arrow')`, which is a call on an unrelated object that @@ -216,7 +245,8 @@ the sting. - `continue` is a keyword, so a tool's middle verb is `drag`. - No `const`. - No `%=`; `++` is postfix only. -- `length()` is a method — `list.length` hands back the function with no error. +- ~~`length()` is a method — `list.length` hands back the function with no error.~~ + *Fixed*: it now raises a proper `property error`. - A line beginning with `[` or `(` continues the previous statement, so a destructuring assignment needs a `;` on the line above it. This bit two code samples in the tutorial before they were run.