Skip to content

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. height parses like wall.height
  • fragment-aware span expansion: bottom / drawer.e highlights bottom / drawer, suggests bottom_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 candidates param 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.e patterns
  • 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

  1. normal — no formula or valid formula
  2. error + overlay visible — red outline, overlay shown
  3. 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 $derived depending on tick, not {@const} snapshot

error flow

  • bind_refs throws FormulaError on bad ref — escapes recursion cleanly
  • FormulaError extends Error, wraps S_Error (instanceof-safe catch guard)
  • set_formula returns S_Error | null to 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)
  • 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.e consumed 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 covers bottom / 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 $derived with 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 / .e silently 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.e fails badly — greedy dot-prefix consumption, unexpected_dot error at second dot
  • .xbottom_drawer.e → unhelpful error — broadened leading-dot detection for any obj starting with .
  • tokenizer strict dot-adjacency rejected wall. height — added peek_past_spaces helper, whitespace tolerance
  • bare SO name (bottom_drawer) silently passed as self-ref attr — detect via resolve_name, report with bare_so
  • bottom_drawer / .e operator 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.e showed confusing "No object named 'bottom'" with useless suggestion — fuzzy fragment match expands span to cover bottom / drawer, suggestion replaces entire fragment
  • bottom_drawer.drawer.e highlighted wrong "drawer" (inside bottom_drawer) — resolve_path now computes span offset within dotted path
  • "bottom_drawer / .e" should (doesn't) fail
  • bottom_drawer.e + * 3 highlighted entire formula instead of just * — extract_span regex now matches got 'X' errors (was only matching token 'X')
  • bottom_drawer.e + * 3 offered 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_refs return type: Node | S_ErrorNode
  • error sites inside bind_refs: return errors.unknown_so(...)throw new FormulaError(errors.unknown_so(...))
  • remove is_error() type guard
  • remove 3 is_error checks in bind_refs recursion (unary, binary cases)
  • 4 call sites wrap in try/catch (e) { if (e instanceof FormulaError) ... }:
    • set_formula — stores e.s_error in 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 setting so.name
  • P_Constants.svelte:commit_name() — validates before constants.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.