Skip to content

solve-engine 2.40.0

Choose a tag to compare

@LiamRiddell LiamRiddell released this 25 Sep 13:37
fb41a15

A note can now check itself, ask what a line would say with a different input, and explain where a number came from. Plain decimals and whole numbers past 2^53 are exact, and units multiply, divide and cancel the way numbers do. Twenty-one features land together with the fixes from reviewing them, including a cross-feature review of the whole release before it was cut.

This is a minor release. Nothing a host calls has been removed or renamed. The answers that move are listed under Answers that change: each was wrong before, and is now right or a named error.

A note that checks and explains itself

Checks (#506). A line starting check states something the note must keep true. It passes quietly and fails loudly when an edit breaks it, and only a line written with check counts (#594); ≈ and within make it approximate, and a host reads the pass and fail count from parseDocument and evaluateDocument. A check compares the way the comparison operators do, so an exact decimal, money and a whole number past 2^53 are checked exactly, and a failure shows its two sides to as many places as it takes to tell them apart.

line 2.39 2.40
check 1 + 1 == 2 parse error ✓
check 22/7 ≈ pi within 0.1% parse error ✓ (differs by 0.04%)
check 2^53 + 1 > 2^53 parse error ✓
check 1.845 == 1.85 parse error check failed: 1.845 is not equal to 1.850

What-if and sweeps (#505). line 4 with deposit = 150000 re-runs line 4 as though an input were different, through every line in between, and line 4 for rate from 3% to 6% step 1% lists its answers across a range. The note itself is untouched, and a host gets the same through engine.whatIf(text, overrides).

deposit = 100000                         100,000
rate = 5%                                5.00%
loan = 400000 - deposit                  300,000
loan * rate                              15,000
line 4 with deposit = 150000             12,500
line 4 for rate from 3% to 6% step 1%    [9,000, 12,000, 15,000, 18,000]

Explain and trace (#522). explainLine now derives conversions, function calls and finance phrases, through an explain hook any package can implement. inputs of line N (or engine.traceLine(n) for a host) lists the lines a result came from.

line steps
5 km in miles 1 km is 0.621371 miles; 5 times 0.621371, 3.11 miles
sqrt(16) + 2 the square root of 16, 4; 4 plus 2, 6

Sections and tags (#508). total of section "Travel" totals the lines under a heading by its name, and total by tag breaks the note down by every tag at once, with each tag's share.

Numbers that are exact

Exact decimals (#511). A number written with a decimal point keeps the exact decimal it was written as, the way money always has, so a comparison agrees with the answer on screen. What an ordinary answer shows does not change. Irrational results (roots, logarithms, trigonometry, pi), scientific notation and units other than money stay in floating point.

expression 2.39 2.40
0.1 + 0.2 == 0.3 false true
1.1 * 1.1 == 1.21 false true
0.1 + 0.2 - 0.3 5.55e-17 0
floor((0.7 + 0.1) * 10) 7 8

Exact large integers (#526). Whole numbers past 2^53 stay exact through arithmetic, powers, factorials and the integer functions.

expression 2.39 2.40
2^53 + 1 9,007,199,254,740,992 9,007,199,254,740,993
3^40 12,157,665,459,056,929,000 12,157,665,459,056,928,801

Honest edge values (#510). Special angles are exact, and a function outside its domain says so rather than answering NaN.

expression 2.39 2.40
sin(180 degrees) 1.22e-16 0
log(-1) NaN log(-1) has no real value: log is only defined for positive numbers.

Number theory (#514): isprime, nextprime, modpow, modinv, factor(360) as 2^3 * 3^2 * 5, 10 choose 3 (120) and 5! (120).

Significant figures (#515): 1234567 to 3 sf is 1,230,000, and as engineering and as compact give the other two short forms.

Seeded randomness (#523): a random seed line, or the engine's random option, makes every roll, pick, shuffle and identifier the same on every run and every machine.

Units, money and data

Unit algebra (#513). Units multiply, divide, raise to powers and cancel the way numbers do, so an area is an area and a price per unit cancels its unit. A product no unit can hold is refused by name.

expression 2.39 2.40
5 m * 3 m 15.00 m 15.00 m²
6 kWh * $0.30/kWh error: Undefined variable: kWh $1.80
3 kg * $5/kg 15.00 USD/kg $15.00
1 / (2 m) 0.50 m 0.50 /m

Cash flows (#519): npv of -1000, 300, 400, 500 at 10% is -21.04 and irr of -1000, 300, 400, 500 is 8.90%; payback of gives the period the flows repay in. The first flow is today.

Table lookups and bands (#507): column "cost" for "food" reads a table in the note by its row's label, and 45,000 through bands above applies a table of bands, such as a tax schedule, to an amount.

Pasted text (#520): total of numbers in "paid 12.50 and 7.25" is 19.75, and amounts in, match, matchcount and field read figures, patterns and JSON fields out of pasted text, on a matcher that cannot backtrack.

Live values (#512): every live value carries its provider and when it was fetched, through arithmetic, and a line ending frozen keeps its answer and the date it was fixed.

Solving, zones, places and distributions. solve(cos(x) = x, x) is 0.74, integral(x^2, x, 0, 3) is 9 and limit(sin(x)/x, x, 0) is 1 (#516). One time can be shown in several zones with the hours they share (#518), places are measured by latitude and longitude (#521), and the inverse normal, binomial, Poisson and Student's t distributions are available (#517).

For hosts

  • Reference-aware editing (#524): find references, go to definition, hover, a rename that edits only real references, and line N references that stay on their lines when lines are inserted or deleted.
  • Did you mean (#509): an unknown name suggests the real names it is close to, and the suggestion is carried on the error for a one-click fix.
  • Reading a line changes nothing (#559, #566, #567): highlighting, completion and explainLine never run a line's effects, and highlight spans are measured on the line as written.
  • The incremental evaluator agrees with a fresh pass (#555, #560, #565, #569), and every built-in package is exported from solve-engine/packages (#556).
  • A result goes through JSON.stringify (#598): a value with an exact decimal or a large integer writes its digits as a string, so a host can log, store or post it.
  • Renumbering moves what-if and sweep targets (#596), and inputs of line N lists what a section total and a table read (#597).

Answers that change

Each of these was a confident wrong answer in 2.39.

expression 2.39 2.40
"11:00 PM" + 2 13 Text and a number cannot be added: + joins text only to other text.
(1, 2) in miles 0.00 miles A bracketed list has no single amount to convert to miles.
combination(5.5, 2) 10 combination counts whole things: 5.5 is not a whole number.
10, total above, 5, total above 25 on the last line 15, since a subtotal is left out
12.3 kg at $0.15/kg $1.84 $1.85
-$5 $-5.00 -$5.00
round(-2.5) -2 -3, a half away from zero as to 0 dp already rounded
(-8)^(1/3) NaN -2
sin(1 m) 0.84 sin takes an angle or a plain number, not a length
hypot(3 m, 400 cm) 400.01 5.00 m
ceil(-0.5) -0 0
9.81 m/s² error: Undefined variable: s² 9.81 m/s²

The fixes also cover multi-word units, unit powers and placement, tolerances written as a percentage, normalcdf and normalpdf with a mean and standard deviation, tan at a right angle, ranges that span a blank line or heading, text counted by the characters a reader sees, and highlighting. Each has its own before/now table in the changelog.

Known issues

The review before release found these, which this release does not fix. Each is being fixed for the next patch:

  • A goal seek over a line that holds a sweep re-runs the whole sweep on every probe, which on the incremental path can take seconds and a large heap (#604). The value pool also keeps every value an incremental pass creates (#605), and inputs of line N writes a very large value out in full before its size check (#606). parseDocument is not affected.
  • A sweep that runs out of its budget names one step as the cause rather than the sweep as a whole (#607).
  • A what-if written with :name adds the assignment instead of substituting it (#608); write the bare name, line 2 with price = 300, until then.
  • A one-character first line reads the rest of the document through parseDocument (#609).
  • The ESM build throws "process is not defined" on import where Node's globals are missing, as in a browser tab or a module worker loaded without a bundler (#610). This was true of 2.39.0 as well; a bundler that defines process.env.NODE_ENV is not affected.

Verification

  • npm run verify:ci passes: 11,890 tests across 544 suites, test:temporal in Europe/London, America/New_York and Pacific/Auckland, and the bundled-consumer contract (smoke:bundled, test:consumer).
  • All 1,393 documented examples are live, proven blocks that the build evaluates.
  • Each feature shipped with an A/B of outputs against the previous build, with every difference classified as intended. The last of those A/B runs covered 34,709 cases for exact decimals.
  • Before the release was cut, a cross-feature review of main ran a 40,400-case fuzz of every generator and about 150 lines through both document passes. It found three bugs where this release's features met (#581 to #583) and seven older gaps (#584 to #588, #590, #592), and a design review found five more in surfaces new to this release (#594 to #598), all fixed here.
  • The benchmark gate passes, and the VM dispatch loop sits 14,542 bytes below V8's optimisation ceiling (#575).