Appearance
25 — errors
open
- disallow negative values entered by the user
- present the error overlay, with explanation "negative values are problematic" with the minus sign selected and a single delete it button (and as normal, return key triggers it)
- graceful handling of corrupted/older save files (deferred)
synopsis
- error overlay with "did you mean:" suggestion buttons, axis-aware hint highlighting
- throw-based error escape — bind_refs throws FormulaError, caught at call sites
- SO resolution by scoped name lookup (bind_refs + resolve_name + resolve_path)
- merge_refs: consecutive bare refs merge with underscores
- name normalization: spaces → underscores in constants, SO names, formula refs
- whitespace-tolerant dot parsing:
wall. heightparses likewall.height - fragment-aware span expansion:
bottom / drawer.ehighlightsbottom / drawer, suggestsbottom_drawer - single-suggestion Enter shortcut: when overlay has one button, Enter applies it with blink feedback
find SO
search algorithm — implemented in bind_refs + resolve_name
- match keyword and . keyword (existing bind_refs logic)
- match constant (existing constants.has check)
- match up the parent SO chain (resolve_name)
- match dotted path (resolve_path): first segment via resolve_name, rest walk children
- present no match error
merge_refs (tokenizer)
- consecutive bare refs merge with underscores: "foo bar.e" → ref("foo_bar", "e")
- dot-absorption (invalid attr folds back into name) — deferred, scoping makes it unnecessary
- path traversal (parent.child.attr) — tokenizer loops on dots, resolve_path walks children
error presentation
S_Error
structured error object, created at the exact failure site
ts
S_Error {
input: string // full formula text
error: Error // originating error
span: [number, number] // start, length of bad portion
message: string // human explanation
suggestions: string[] // actionable fixes
}algebra/Errors.ts
single owner of S_Error: type, factory, message intelligence, suggestions. all error sites route through Errors — no one else constructs S_Error.
methods:
bad_syntax(input, span, error)→ S_Error- wraps tokenizer/compiler errors with friendly language
- "Unexpected '&'. Operators: + - * /"
- delete-only when bad token is an operator or adjacent to an operator
classify(input, span, error)→ S_Error- routes compile/tokenize errors to the right factory method
- detects tail junk (error near end, non-value chars) → widens span backward, routes to
incomplete - detects "got 'end'" → routes to
incomplete - otherwise → routes to
bad_syntax
incomplete(input, span)→ S_Error- "Formula is incomplete, did you want to:" with delete/add-more buttons
unknown_so(input, span, name, self_id, candidates?)→ S_Error- walks parent chain listing sibling names, fuzzy-matches
- optional
candidatesparam for child-scoped suggestions (used by resolve_path) - dot-aware delete: collapses double-dot when span surrounded by dots
- "No object named 'foo', did you mean:"
unknown_attr(input, span, attr, object)→ S_Error- lists valid attributes
- "Unknown attribute 'top', did you mean:"
bare_so(input, name, name_span)→ S_Error- SO name used without attribute, highlights adjacent operator
- "The operator '/' cannot be applied to an object."
leading_dot(input, dot_span)→ S_Error- "Did you add '.' by mistake?" with delete button
unexpected_dot(input, dot_span, full_ref)→ S_Error- "Unexpected '.' here." — for
.x.bottom_drawer.epatterns
- "Unexpected '.' here." — for
cycle(input, chain)→ S_Error- "This formula creates a loop: A.x → B.y → A.x"
error sources
each site calls Errors methods, passing input + span:
- tokenizer: catches bad char →
errors.classify(...) - compiler: catches bad token →
errors.classify(...)(routes to bad_syntax or incomplete) - bind_refs: resolve_name fails →
errors.unknown_so(...) - bind_refs: invalid attr →
errors.unknown_attr(...) - bind_refs: bare SO name (no attr) →
errors.bare_so(...) - bind_refs: leading dot →
errors.leading_dot(...) - bind_refs: unexpected dot after axis →
errors.unexpected_dot(...) - bind_refs: fuzzy fragment match →
errors.unknown_so(...)with expanded span - resolve_path: child segment fails →
errors.unknown_so(...)with child-scoped candidates - set_formula: cycle detected →
errors.cycle(...)
lifecycle
- created at error site, returned through set_formula
- stored in Errors (singleton) — not in component state
- Errors holds current S_Error per attribute; P_Attributes reads from Errors
- persists until user submits a formula that succeeds (Errors clears it)
- clearing the formula (empty string) also clears the error
cell states
- normal — no formula or valid formula
- error + overlay visible — red outline, overlay shown
- error + overlay dismissed — red outline only
focus / restore
- P_Attributes captures cell value on focus (before editing)
- on error: formula stays as user typed it (not reverted)
- user can keep editing and resubmit freely
overlay
- inline popover below the table, full width, darkred border, 8px corner radius
- error message centered, quoted text highlighted in darkred
- message format: "Unknown attribute 'foo', did you mean:"
- suggestion buttons: one per valid attribute letter, rounded 5px, white background
- "good hint" buttons slightly darker (#ddd): s, e, l + axis-matching attrs
- clicking a button replaces the full formula text and focuses with cursor at end
- auto-commit suggestions: most buttons commit immediately, "add more" focuses without commit
- single-suggestion Enter: when overlay has exactly one button, Enter applies it with 120ms blink
- dismiss: click outside or non-passive keypress (modifier/arrow keys don't dismiss)
- overlay splits table at error row: first table up to error, overlay between, second table after
- overlay border: 2px solid darkred, 8px margin top/bottom
- cell outline: 1.25px darkred, text selection background transparent
- reactive overlay: uses
$deriveddepending on tick, not{@const}snapshot
error flow
- bind_refs throws
FormulaErroron bad ref — escapes recursion cleanly - FormulaError extends Error, wraps S_Error (instanceof-safe catch guard)
- set_formula returns
S_Error | nullto callers - on error:
stores.tick()forces template re-evaluation, input re-focused with bad span selected - Enter key: preventDefault + blur (commits without moving to next cell)
- null guard in Attribute.deserialize for corrupted/older saves (returns value 0)
negative attributes on drag
user drags a selection dot → at least one dimension goes negative. spatial constraint violation, not a formula error.
options:
- (a) auto-correct: clamp to zero, or swap min/max
- (b) prevent: block the drag at zero, show feedback
reference
- S_Error type + factories + FormulaError class:
algebra/Errors.ts - bind_refs + is_valid_attr:
algebra/Constraints.ts - overlay + cell styling:
svelte/details/P_Attributes.svelte - null guard:
types/Attribute.ts
done
- error presentation to user (formula compile errors, cycle detection, etc.)
- find_so: resolve SO references by name → id in bind_refs
- constant names: space → underscore on blur (P_Constants commit_name)
- constant name input cell updates visually after replacement
- formula input: merge_refs normalizes "foo bar.e" → "foo_bar.e", cell updates
- SO names: space → underscore on scene load and paste/import (Engine.ts)
- resolve_name: walks parent chain checking siblings at each level, closest scope wins
- algebra/Errors.ts: S_Error type + factory methods + suggestion intelligence
- export from algebra/index.ts
- tokenizer/compiler: route errors through Errors.bad_syntax()
- set_formula: return S_Error | null (null == good)
- bind_refs: route through Errors.unknown_so() / unknown_attr()
- commit_formula: read S_Error from Errors (no component-level storage)
- onfocus: capture cell value for restore
- cell styling: red outline on error cell
- highlight bad span in cell (dark red text)
- overlay component: popover with dismiss behavior
- overlay: "restore" button wired to captured value
- Enter re-shows overlay on errored cell
- use throw error (FormulaError class, bind_refs throws, 4 call sites catch)
- move the overlay
- when the offending formula is NOT on the last row of the attr table
- split the table in two
- first table is the offending row and above
- second table is rows after the offending row
- overlay goes between them (easier to grok)
- split the table in two
- when the offending formula is NOT on the last row of the attr table
- whitespace-tolerant dot parsing in tokenizer (peek_past_spaces helper)
- bare SO detection: self-ref attrs validated, SO names flagged with bare_so error
- leading-dot and unexpected-dot detection in bind_refs
- greedy dot-prefix parsing:
.x.bottom_drawer.econsumed as single token - auto-commit suggestions (Suggestion type with commit flag)
- reactive overlay via $derived (replaces stale {@const} snapshot)
- overlay: modifier/arrow keys don't dismiss
- overlay: 8px gap, darkred border
- single-suggestion Enter shortcut with blink feedback (scoped CSS via :global)
- fuzzy fragment span expansion:
bottom / drawer.e→ span coversbottom / drawer - resolve_path offset-based span: targets exact failing segment in dotted paths
- dot-aware delete in unknown_so suggestions
- delete buttons renamed to "delete it"
- nearby_names made public for cross-module fuzzy matching
- delete buttons renamed to "delete it"
- name uniqueness: constants and SO names must all be
- unique
- not a reserved name (e, s, l, x, y, z, X, Y, Z, w, d, h)
- need a new error type : NamingError
- rename FormulaError -> AlgebraError
bugs fixed
- invalid attributes (e.g. "foo.eaddf") silently passed through — added is_valid_attr() check
- bind_refs was throwing raw errors (crashed under HMR) — now throws FormulaError (instanceof-safe)
- red border not showing after error — missing stores.tick() in error path
- Enter key moved focus to next cell — added preventDefault + blur
- deserialization crash on null attribute data — null guard returns value 0
- dead CSS (.error-restore) and dead function (restore_formula) cleaned up
- pre-existing warnings fixed: Separator reactive prop, appearance CSS, unused selectors
- path traversal: bottom_drawer.front.Z resolves through SO hierarchy
- bottom_drawer.front.Zs fails badly → path traversal implemented
- bottom_drawer.e\ crash → tokenizer.tokenize() in commit_formula was unprotected, now caught with error overlay
- bottom_drawer.e\ isn't and should be flagged as an error → tokenizer throw in commit_formula now caught
- "bottom_drawer.e - " -> bad fail! → extract_span now detects "got 'end'" errors, highlights trailing operator instead of entire formula
- "bottasdfom_drawer.e" fails badly → unknown_so now falls back to showing all nearby SO names when fuzzy match finds nothing
- bottom_draweewawar.frontg.dd -> description is wrong
- says no object named "bottom_draweewawar.frontg" but that is confusing, should say no object named "bottom_draweewawar"
- stale overlay after auto-commit: apply_suggestion dismissed overlay before commit_formula could update it
- stale overlay suggestions:
{@const}snapshot didn't re-evaluate — switched to$derivedwith tick dependency - resolve_path error targeted full dotted name instead of failing segment — now throws directly with correct segment
- resolve_path child suggestions showed siblings instead of children — added candidates param
- delete button not dot-aware: left double dots when removing mid-path segment
-
bottom_drawer / .esilently accepted — added is_valid_attr check for self and parent refs in bind_refs -
.bottom_drawer.e→ "No object named ''" — detect leading-dot pattern, throw helpful error -
.x.bottom_drawer.efails badly — greedy dot-prefix consumption,unexpected_doterror at second dot -
.xbottom_drawer.e→ unhelpful error — broadened leading-dot detection for any obj starting with. - tokenizer strict dot-adjacency rejected
wall. height— addedpeek_past_spaceshelper, whitespace tolerance - bare SO name (
bottom_drawer) silently passed as self-ref attr — detect via resolve_name, report withbare_so -
bottom_drawer / .eoperator on object — bare_so highlights adjacent operator, offers delete - modifier/arrow keys dismissed overlay — now only non-passive keys dismiss
- overlay gap: 8px margin top/bottom
- overlay border: darkred
- single-suggestion Enter shortcut with blink feedback
-
bottom / drawer.eshowed confusing "No object named 'bottom'" with useless suggestion — fuzzy fragment match expands span to coverbottom / drawer, suggestion replaces entire fragment -
bottom_drawer.drawer.ehighlighted wrong "drawer" (insidebottom_drawer) — resolve_path now computes span offset within dotted path - "bottom_drawer / .e" should (doesn't) fail
-
bottom_drawer.e + * 3highlighted entire formula instead of just*— extract_span regex now matchesgot 'X'errors (was only matchingtoken 'X') -
bottom_drawer.e + * 3offered operator replacement suggestions — now delete-only when bad token is an operator or adjacent to one -
bottom_drawer.e. +showed' 'as error — classify detects tail junk, widens span, routes to incomplete - error span mismatch: span computed against normalized string but shown against raw input — input value now updated to normalized before error display
auto-normalization (commit_formula)
pre-tokenization cleanup:
- collapse multiple dots (with or without spaces):
.. .→. - remove dot before operator:
.+or. +→+(or+) - strip trailing dots
post-untokenize cleanup:
-
+ -or+ -→-(collapse plus-minus to minus) -
- +or- +→-(collapse minus-plus to minus) - unary minus glued to value:
* - 3→* -3(only after operator or start/paren, not after value)
proposal: throw for error escape
Reintroduce throw in bind_refs to escape the recursive AST walk on error, instead of threading Node | S_Error unions through every recursive call.
FormulaError class
New Error subclass in algebra/Errors.ts, wraps S_Error:
ts
export class FormulaError extends Error {
s_error: S_Error;
constructor(s_error: S_Error) {
super(s_error.message);
this.s_error = s_error;
}
}changes
-
bind_refsreturn type:Node | S_Error→Node - error sites inside
bind_refs:return errors.unknown_so(...)→throw new FormulaError(errors.unknown_so(...)) - remove
is_error()type guard - remove 3
is_errorchecks inbind_refsrecursion (unary, binary cases) - 4 call sites wrap in
try/catch (e) { if (e instanceof FormulaError) ... }:-
set_formula— storese.s_errorin Errors, returns it -
rebind_formulas— skips formula -
evaluate_formula— returns null -
rename_sd_in_formulas— skips formula
-
name uniqueness
current state
- uniqueness enforced at creation (Engine auto-generates unique names) but not at commit — user can manually type a duplicate or reserved name
- two commit sites:
D_Parts.svelte:commit_name()(SO names),P_Constants.svelte:commit_name()(constant names) - no cross-pool check: an SO name can collide with a constant name (breaks formula resolution)
validation sites
D_Parts.svelte:commit_name()— validates before settingso.nameP_Constants.svelte:commit_name()— validates beforeconstants.rename()
rules
- SO names and constant names share one namespace (formulas reference both by bare name)
- reserved names: e, s, l, x, y, z, X, Y, Z, w, d, h (attribute letters)
- empty/whitespace-only names already handled (trimmed, rejected if empty)
validation
no Error subclass — plain function returns string | null (null = valid, string = error message):
ts
function validate_name(name: string, self_id: string): string | null {
if (reserved_names.has(name)) return `'${name}' is a reserved attribute name.`;
if (is_duplicate(name, self_id)) return `'${name}' is already in use.`;
return null;
}caller checks return, reverts name if non-null, shows message. validation is a gate in the commit function, not an exception.
decision
on bad name → show error overlay (reuse S_Error pattern). revert name, show overlay with message.