Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
8ccd98d
Changes needed for doenet integration
siefkenj Jul 31, 2026
cdc5343
Changes needed for Doenet
siefkenj Aug 1, 2026
02293bf
Updates for Doenet
siefkenj Aug 4, 2026
640d32c
More soundness fixes
siefkenj Aug 4, 2026
08bd4dc
Trig identity fixes
siefkenj Aug 4, 2026
537eca3
Formatting + fixes
siefkenj Aug 4, 2026
970c1c3
Update plans
siefkenj Aug 4, 2026
7a18c9c
Fixes for Doenet
siefkenj Aug 4, 2026
264be80
Root fixes
siefkenj Aug 5, 2026
593dbe6
Doenet fixes
siefkenj Aug 5, 2026
7f8365c
Prevent overflow on bad input
siefkenj Aug 5, 2026
9aedf60
Updates
siefkenj Aug 6, 2026
348a603
Updates
siefkenj Aug 6, 2026
1d8fe27
Merge branch 'doenet' of github.com:siefkenj/math-expressions into do…
siefkenj Aug 6, 2026
e783efc
Improve simplification
siefkenj Aug 6, 2026
66a3bc1
Fixes
siefkenj Aug 6, 2026
69bdc22
Added eval many
siefkenj Aug 6, 2026
0fd94a2
Minor fixes
siefkenj Aug 6, 2026
db8de8e
evaluate fixes
siefkenj Aug 6, 2026
2d4f1f7
canonicalize order before evaluation for consistent float behaviour
siefkenj Aug 6, 2026
bba9cb9
Better folding
siefkenj Aug 6, 2026
10488e9
Rename function to be more explifit about appraoch
siefkenj Aug 6, 2026
ad05f87
Fixes for doenet
siefkenj Aug 6, 2026
ae13c2d
Minor fixes
siefkenj Aug 7, 2026
d6a394b
Updates
siefkenj Aug 7, 2026
c473321
Merge branch 'doenet' of https://github.com/siefkenj/math-expressions…
siefkenj Aug 7, 2026
9c4704f
Fix rounding
siefkenj Aug 7, 2026
8d2d9fd
Fixes
siefkenj Aug 7, 2026
d0737c3
Fixes
siefkenj Aug 7, 2026
8fa8c9b
Changes for doenet
siefkenj Aug 7, 2026
e39fcff
Changes for doenet
siefkenj Aug 8, 2026
d96465d
Fix floor function
siefkenj Aug 8, 2026
ae7da6d
Review edits
siefkenj Aug 8, 2026
f71c4a0
Test fixes
siefkenj Aug 8, 2026
a139491
Minor updates
siefkenj Aug 8, 2026
2be5d93
Adopt new printer conventions in latex round-trip test expectations
siefkenj Aug 8, 2026
cd72603
Complete the assumptions engine
siefkenj Aug 8, 2026
033d9ae
Merge branch 'doenet' of github.com:siefkenj/math-expressions into do…
siefkenj Aug 8, 2026
42aa602
Add js-compat failure summary
siefkenj Aug 8, 2026
708bb57
Small fix
siefkenj Aug 9, 2026
a5d1867
Move JS tree/assumption math into Rust bindings (Phase A)
siefkenj Aug 9, 2026
133ce6f
Move the assumptions logic into the Rust core (Phase B)
siefkenj Aug 9, 2026
087c3ab
Move the polynomial / Groebner engine into Rust (Phase C)
siefkenj Aug 9, 2026
4192de7
Record the outcome of the JS-to-Rust migration
siefkenj Aug 9, 2026
d75c045
Updates
siefkenj Aug 9, 2026
0cd36fa
Matrix normalizations
siefkenj Aug 9, 2026
386d57e
Ast fixes
siefkenj Aug 9, 2026
d294d00
Fixes
siefkenj Aug 9, 2026
4fc358a
More compat
siefkenj Aug 9, 2026
faa7c09
Simplify fixes
siefkenj Aug 9, 2026
615d3be
Minor fix
siefkenj Aug 9, 2026
a9f0991
Updates for tests
siefkenj Aug 9, 2026
c64881f
Merge branch 'doenet' of github.com:siefkenj/math-expressions into do…
siefkenj Aug 9, 2026
680d6ad
Formatting
siefkenj Aug 9, 2026
0155902
Fix some failing tests
siefkenj Aug 10, 2026
f84577c
Review
siefkenj Aug 10, 2026
0dfcd24
Test fixes
siefkenj Aug 10, 2026
685c0be
Updates
siefkenj Aug 10, 2026
6914f0c
Test fixes
siefkenj Aug 10, 2026
0f41710
Minor fixes
siefkenj Aug 10, 2026
08472b6
Merge branch 'doenet' of github.com:siefkenj/math-expressions into do…
siefkenj Aug 10, 2026
62c5f20
Test fixes
siefkenj Aug 10, 2026
6831717
More fixes
siefkenj Aug 11, 2026
7082f8a
Improve simplify routine
siefkenj Aug 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,34 @@ concurrency:
cancel-in-progress: true

jobs:
rust-lint:
name: Lint Rust Code
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v6

# Modeled on DoenetML's "Lint Rust Code" job: install a minimal
# stable toolchain with the rustfmt + clippy components, then gate
# on formatting and a warning-free clippy. Uses rustup directly
# (not the devcontainer) so the lint job stays fast and independent
# of the wasm/Node toolchain the build/test jobs need.
- name: Install toolchain
run: |
rustup set profile minimal
rustup toolchain install stable --component rustfmt --component clippy
rustup default stable

# `--all` covers the whole workspace (the wasm crate is not a
# default member; see rust-test).
- name: Cargo fmt
run: cargo fmt --all -- --check

# `--workspace --all-targets` lints the wasm crate and the test /
# example targets too, not just the default-member library.
- name: Cargo clippy
run: cargo clippy --workspace --all-targets -- -D warnings

rust-build:
name: Rust build
runs-on: ubuntu-latest
Expand Down
9 changes: 9 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,12 @@ lto = "fat"
codegen-units = 1
panic = "abort"
strip = "debuginfo"

# Note on `panic = "abort"` and diagnosability (DOENET_INTEGRATION item 2): it
# was long assumed that this line is why a wasm panic reaches the browser as a
# bare `RuntimeError: unreachable`. It is not. std runs the panic hook *before*
# aborting, so the message survives this profile intact — what was missing was
# a hook, since the default one writes to a stderr that goes nowhere on
# wasm32-unknown-unknown. One is now installed unconditionally in the wasm
# crate; measured cost 1,958 bytes. There is no separate diagnostic profile
# because a separate profile turned out to buy nothing.
110 changes: 110 additions & 0 deletions active-plans/ASSUMPTIONS_ENGINE_PLAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# Assumptions engine — completion plan

Goal: close the remaining feature gaps in the assumptions engine so
`packages/math-expressions-js-compat/spec/slow_assumptions.spec.ts` passes.

## Status

| stage | failing | note |
|-------|--------:|------|
| start of session | 565 / 845 | |
| after Phase 1 (done) | **234 / 845** | default-assumptions binding fix |
| target | 0 | |

## Phase 1 — default assumptions source (DONE, −331)

`lib/assumptions/element_of_sets.ts` built its predicates over a module-level
`EMPTY = new wasm.Assumptions()`, and `handleFor(undefined)` returned it. The spec
calls `is_real(me.fromText("x+y"))` with **no** second argument, expecting the
global store populated by `me.add_assumption(...)`. Every no-argument query
therefore answered "unknown".

Fix: `handleFor(undefined)` now returns `Context.assumptions` (the live handle).
The `EMPTY` fallback was additionally made lazy — as written it forced the wasm
load during module evaluation, the same hazard documented at
`lib/math-expressions.ts:1251`.

This proved the Rust reasoner was already correct for the bulk of these cases
(`is_real(x+y)` with `x,y ∈ R` returns `true` when handed the right store); the
failures were a binding defect, **not** a reasoning-depth gap.

## Remaining work — 234 failures in six groups

### Group A — negated assumptions (16) · Rust
`variable_facts` in `assumptions/infer.rs` only reads `Expr::Relation`; it ignores
`Expr::Not`. Needed: negation-normalize each stored fact before interpreting —
`not(x>0)` ⇒ `x≤0`, `not(x≥0)` ⇒ `x<0`, `not(x≠0)` ⇒ `x=0`, `not(x=0)` ⇒ `x≠0`,
with double-negation elimination (`not(not(p))` ⇒ `p`).

### Group B — arithmetic reasoning gaps (~96) · Rust
`sum 8 · subtraction 10 · product 22 · quotient 14 · power 42`. Confirmed rules:
- **Zero factor**: any factor known `= 0` makes the product exactly zero
(`nonneg`/`nonpos` true, `positive`/`negative`/`nonzero` false). `combine_mul`
currently demands `real` before any sign reasoning and has no zero short-circuit.
- **Complex closure**: `x,y ∈ C` ⇒ `x+y`, `x·y`, `x^y` complex. `combine_pow`
never sets `complex` from a complex base/exponent.
- **Sum with a known-zero term**: `x ∈ C, x≠0, y=0` ⇒ `x+y` nonzero and complex.
- **Powers**: `x ∈ R, y>0` ⇒ `x^y` complex; `x ∈ R, x≠0` ⇒ `x^y` nonzero.
Power is the largest single bucket — expect several sub-rules.

### Group C — function domain facts (6) · Rust
`x ∈ C` ⇒ `sin(x)`, `sqrt(x)`, `exp(x)`, `abs(x)` are complex. `apply_facts`
currently derives facts only from a **real** argument, so a complex argument
yields nothing.

### Group D — literal & operator evaluation (8) · Rust
- `sin(0)` ⇒ integer/real (needs constant folding of the argument).
- `sqrt(-4)` ⇒ complex true, real false.
- `-2.2/(5-5)`, `(-6+6)/(5-5)` ⇒ division by zero: every predicate false.
- Non-numeric nodes — tuple `(5,2)`, relation `5=3` — ⇒ every predicate false
(currently `Facts::unknown()` via the catch-all arm).

### Group E — `get_assumptions` structural rebuild (~103) · TypeScript
`interval containment 64 · element interval 16 · derived 5 · add/get misc 8 · misc 1`.
Today `Context.get_assumptions()` ignores its argument and returns a **Context**,
so the spec's `ordered_trees_equal(...)` is always false. Required semantics:
- Accept `"x"` or a nested-array form `[["x"]]` / `[["a","b"]]`; return an **AST**
(or `undefined` when nothing is known).
- **Orientation**: facts are re-stated with the queried variable on the left
(`x<a` stored ⇒ `get_assumptions("a")` yields `a>x`).
- **Transitive closure** over `=` and `<`/`≤` (`x<a, a<b, b<c` ⇒ for `x`:
`x<a and x<b and x<c`), including the mixed strict/non-strict case.
- **Interval membership expansion**: `x ∈ (a,b)` ⇒ `x>a and x<b`; all four
bracket forms, `containselement`, and the negated forms as `or`-disjunctions.
- **Subset/superset expansion**: `(a,b) subset (c,d)` ⇒ `a>=c and b<=d`, across
all 64 bracket/negation combinations (the single largest sub-bucket).

Implement in the compat lib over the existing `_assumptionTexts` list rather than
widening the wasm ABI: the required shape is a JS-API concern, and keeping it in
TS avoids a rebuild cycle. The Rust store stays the source of truth for predicates.

### Group F — robustness (5) · TypeScript
The first row of each `sum/product/quotient/power` table has `input[0] === undefined`;
`me.add_assumption(me.from(undefined))` throws
`Cannot read properties of undefined (reading 'length')`. Adding an undefined or
empty assumption must be a no-op.

## Sequencing

Phases run **sequentially, not in parallel**: the Rust phases require rebuilding
`vendor/wasm`, which would change behaviour underneath a concurrently-measuring
TypeScript phase.

1. **Phase 2 — Rust engine** (Groups A–D, ~126 tests). Files: `src/assumptions/`
(`infer.rs`, `facts.rs`, possibly a new negation helper). Rebuild via
`packages/math-expressions-js-compat/build-wasm.sh`.
2. **Phase 3 — TypeScript** (Groups E–F, ~108 tests). Files:
`lib/math-expressions.ts` (+ a new `lib/assumptions/` helper module).

## Verification & risk

- Per phase: `npx vitest run spec/slow_assumptions.spec.ts`.
- **Regression risk (Rust)**: `tests/assumptions.rs` and `tests/assumptions_corpus.rs`
validate the engine against the JS oracle, and `infer.rs` deliberately mirrors
several JS conservatisms (no interval arithmetic in sums; odd powers of
negatives unsigned). Run `cargo test` for the whole crate after Phase 2 — a
"smarter" rule that contradicts the oracle is a regression, not an improvement.
- **Regression risk (suite-wide)**: full `npx vitest run` after each phase.
Baseline to beat: **940 failing** (see `COMPAT_TEST_FAILURE_SUMMARY.md`).
Judge by name-level diff, never by aggregate counts.
- Per repo convention, split any file that grows past ~200 lines into a subfolder.
Loading