Skip to content

solve-engine 1.0.1

Choose a tag to compare

@LiamRiddell LiamRiddell released this 11 Aug 20:26
e6458bc

A crash fix. One method, two paths into it, and a reason the fuzzer could not see either.

What was happening

EngineError: Unexpected end of input escaping into Obsidian and breaking the editor mid-edit, reported after clearing a document that contained hello =.

tryCompileExpression() answers "does this compile" with a boolean, and LanguageService calls it for every visible line on every keystroke to decide what to highlight. A throw from it does not land in a caller that is looking for one; it reached CodeMirror's transaction dispatch.

The trigger is not an edge case. total = is what every assignment looks like for the moment between typing the = and typing the value, so this was reachable by typing an assignment at ordinary speed.

Input Before Now
total = throws false
hello = throws false
" throws false
der( throws false
hello = 5 true true

Two paths, not one

The symbolic grammar parses its own operand sub-ranges, and ran ahead of the try/catch guarding the main parse, so an assignment with an empty right-hand side threw straight out of prepareExpression(). Every other failure mode in that method already returned a structured { kind: 'error' }; this one alone could throw. It now returns the same 'parse' result, which makes compileExpression() consistent as well.

The lexer throws on an unterminated string, before the parser is reached at all. Fixing the grammar alone left " still throwing, so tryCompileExpression() now enforces its own published contract rather than trusting every stage below it to agree. A catch costs nothing on the path that does not throw, so the cheap "no" answer the method exists to give stays cheap.

evaluateExpression() is unchanged and still throws. It is documented @throws {EngineError}, and only the boolean probe was wrong.

Why 2.6 million fuzz cases missed it

The oracle counts a thrown EngineError as a pass. That is correct for the @throws API it drives, and tryCompileExpression() is the one entry point with a stricter contract, so the invariant being asserted was weaker than the contract being published. A green fuzz run meant the oracle's invariant held, not that the engine was right.

The expression oracle now asserts that contract on every case, so the whole existing corpus exercises it with no new generator, CLI surface or corpus of its own. Adding it fired immediately and shrank two further reproducers, " and der(, out of inputs that looked nothing like the reported one. Both are fixed here and committed to the corpus, which replays on every test run.

Verification

  • The regression spec fails 6 of 23 without the fix and passes 27 of 27 with it
  • 6,776 tests across 284 suites, no failures
  • npm run verify green, including the bundled-consumer and sideEffects contracts
  • 250,000 fuzz cases at seed 20260811 with zero findings; the seed that previously produced findings in every block now produces none

Also in this release

RELEASING.md documents the path from pull request to registry: why a merge cannot publish, why publishing a GitHub Release is the deliberate irreversible act, and the failures this repository has actually hit rather than the ones it might.

A changeset consumed at 1.0.0-beta.7 had been left on disk and would have folded an already-published entry into this changelog. Removed, and the check for it is now written down.