Skip to content

feat(wasm-workbook): expose a typed cell-value method alongside the existing JsValue one - #997

Merged
hhimanshu merged 2 commits into
mainfrom
feat/tagged-evalresult-flow
Sep 3, 2026
Merged

hhimanshu merged 2 commits into
mainfrom
feat/tagged-evalresult-flow

Conversation

@hhimanshu

@hhimanshu hhimanshu commented Sep 3, 2026 •

Copy link
Copy Markdown
Member

What this closes

@truecalc/core's EvalResult (the discriminated-union { type: "number", value: ... } shape) has always been the WASM surface's canonical value representation, but @truecalc/workbook's get()/resolved() only ever exposed it serialized to a JSON string that callers had to JSON.parse() themselves — with no static type on the other end. Types flowed left (Rust) to nowhere useful on the right (the JS/TS consumer had to re-derive the shape by hand or trust a hand-written .d.ts).

This PR closes that gap: get()/resolved() gain typed counterparts, getTyped()/resolvedTyped(), that marshal a real EvalResult across the WASM ABI via tsify instead of returning a JSON string — so the type flows all the way from the Rust Value enum to a real generated TypeScript type on the consumer's side, left to right, with no manual re-parsing or hand-maintained shape declarations in between.

What changed

  • New internal crate truecalc-wasm-value (crates/wasm-value, publish = false, rlib only): hosts EvalResult, SparklineSpecResult, and the Value -> EvalResult mapping that used to live only in crates/wasm/src/lib.rs. crates/wasm now re-exports these instead of defining them, so @truecalc/core's public API is untouched (see verification below).

  • crates/wasm-workbook: adds value_to_eval_result (an arm-by-arm mapping from truecalc_workbook::Value, a distinct type from truecalc_core::Value, onto the shared EvalResult), plus two new #[wasm_bindgen] methods:

    • getTyped(sheet, a1) -> GetResult | undefined — typed counterpart of get()
    • resolvedTyped(sheet, a1) -> ResolvedResult | undefined — typed counterpart of resolved(), anchor included

    Both return undefined (not get/resolved's JSON null) for a missing cell — the idiomatic optional shape for a typed WASM return.

  • get() and resolved() themselves are completely unchanged — same JSON-string return, same shape, same behavior. This is purely additive.

Why it's additive / non-breaking

Verified two ways, not just asserted:

  1. @truecalc/core's generated .d.ts is byte-identical except for one added doc-comment sentence. I built crates/wasm with wasm-pack build --target web at both origin/main and this branch's tip and diffed the two truecalc_wasm.d.ts files — the only change is a sentence added to EvalResult's doc comment explaining it's now shared with @truecalc/workbook. No type, field, or method changed.
  2. @truecalc/workbook's generated .d.ts diff is purely additive. Same before/after build comparison for crates/wasm-workbook: every existing type, method signature, and exported WASM binding is untouched. The diff adds only the new EvalResult/SparklineSpecResult/GetResult/ResolvedResult types and the two new getTyped/resolvedTyped methods (plus their corresponding new jsworkbook_getTyped/jsworkbook_resolvedTyped low-level WASM exports).

Runtime verification (real cell values, before vs after)

Built both origin/main and this branch's crates/wasm-workbook for the web target and drove both through Node (via initSync with the raw .wasm bytes) against a workbook with a number, text, bool, a #DIV/0! error, and a spilling ={1,2;3,4} array formula:

  • get()/resolved() output is byte-for-byte identical before and after this PR on every cell (confirms zero behavior change to the untouched methods).
  • getTyped()/resolvedTyped() match JSON.parse(get()).value / JSON.parse(resolved()).value field-for-field for every scalar and the error case (key-order differences aside, which don't apply to real typed objects).
  • The one intentional divergence: a 2-D array. get()'s existing JSON shape nests raw JS arrays ([[1,2],[3,4]]); getTyped() returns the documented recursive array-of-array EvalResult shape ({type:"array",value:[{type:"array",value:[...]}]}) — consistent with every other value kind instead of a special-cased raw array. This is by design, documented in EvalResult's own doc comment, and confirmed working for the spill-anchor and spilled-cell (anchor field) cases.

Test coverage

  • crates/wasm-workbook/tests/typed_value.rs (new, 15 tests, native cargo test): exhaustive per-Value-variant coverage for getTyped/resolvedTyped — number, text, bool, date, zoned, error (with and without diagnostic message), empty, sparkline, and the 2-D spill-anchor/spilled-cell array cases — built via truecalc_workbook directly and loaded through JsWorkbook::fromJSON since several of these variants have no JS-level set() literal form.
  • crates/wasm-workbook/tests/wasm_surface.rs (extended, 1 new wasm_bindgen_test): the one thing the native tests above can't exercise — marshaling through the real #[wasm_bindgen]-generated ABI wrapper, not just the inherent Rust method.

Verification run independently for this PR

  • cargo fmt --all -- --check: pre-existing repo-wide drift unrelated to this diff (2453 hunks across untouched files, confirmed by diffing against files this PR doesn't touch); every file this PR adds or touches is fmt-clean on its own. CI's fmt job is a ratchet on changed files only, which this satisfies.
  • cargo clippy --workspace -- -D warnings: clean.
  • cargo test --workspace --exclude truecalc-python: all green (truecalc-python fails to link locally only because this machine is missing a Python 3.9 dev library — a pre-existing local-environment gap, not something this PR touches; CI's own Test step excludes the same crate for an unrelated, permanent reason — its extension-module build can't link libpython in a test binary).
  • cargo deny check: clean.
  • wasm-pack build crates/wasm --target web and wasm-pack build crates/wasm-workbook --target web: both build clean.

For the reviewer

The diff touches 4 crates but the actual new logic is small: one new tagged-value-to-EvalResult mapping function (value_to_eval_result in crates/wasm-workbook/src/lib.rs) and two thin #[wasm_bindgen] wrapper methods around existing inner.get/inner.resolved calls. The truecalc-wasm-value crate split is a pure code-motion (cut from crates/wasm/src/lib.rs, pasted with no logic changes) to let both WASM packages share one EvalResult definition instead of hand-rolling two copies of the same tagged union.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

hhimanshu and others added 2 commits September 3, 2026 21:13
…string get/resolved

Extracts EvalResult (and the Value -> EvalResult mapping) out of
crates/wasm into a new internal crate, truecalc-wasm-value, so
@truecalc/core and @truecalc/workbook share one tagged-value shape
instead of each hand-rolling it. crates/wasm re-exports the type
unchanged; crates/wasm-workbook adds its own arm-by-arm mapping from
truecalc_workbook::Value (a distinct type from truecalc_core::Value).

wasm-workbook cannot depend on wasm directly to reuse EvalResult:
wasm-bindgen keeps every #[wasm_bindgen] free function as an export
root regardless of whether the consuming crate calls it, so linking
wasm as a library would leak wasm's own evaluate/validate/createEngine/
etc. into wasm-workbook's compiled artifact and public API (verified
via wasm-objdump export-table diff). A shared, wasm-bindgen-free crate
avoids that.

JsWorkbook gains getTyped/resolvedTyped, returning the real EvalResult
marshaled across the WASM ABI via tsify instead of the JSON string
get/resolved return today. get/resolved are byte-for-byte unchanged
(same JsValue/JSON-string return, doc comment only) so nothing
currently JSON.parse()-ing them breaks; the new methods are a
purely additive surface for callers to migrate to.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WfhRbND7tjFJ4JeGZ9gHL5
…f added

The prior commit's review concluded the repo's fmt CI check was a
pre-existing, unrelated failure and not a blocker. That conclusion was
wrong for this PR's own new code: .github/scripts/fmt-changed.sh is a
per-file ratchet, not a whole-workspace check — it grandfathers files
that were already fmt-dirty at the merge base (crates/wasm/src/lib.rs
qualifies) but enforces fmt unconditionally on added files and on
files that were previously clean. crates/wasm-value/src/lib.rs is a
brand-new file and crates/wasm-workbook/src/lib.rs was clean at
origin/main, so the local rustfmt's struct-literal-expansion style
applies to both and the script fails on exactly the new EvalResult
enum and the two new value_to_eval_result match arms this feature
added. Verified by actually running
`bash .github/scripts/fmt-changed.sh origin/main` before and after.

Ran `--fix` scoped to just the two failing files (not the grandfathered
crates/wasm/src/lib.rs, to avoid reformatting unrelated pre-existing
code) and confirmed the ratchet script now reports "No new formatting
drift." Full local CI suite re-run clean: cargo clippy -D warnings (0
issues), cargo test --workspace --exclude truecalc-python (4216
passed), cargo build --target wasm32-unknown-unknown for both wasm and
wasm-workbook, and wasm-pack test --node for both crates (4 + 2 passed).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WfhRbND7tjFJ4JeGZ9gHL5
@hhimanshu hhimanshu self-assigned this Sep 3, 2026
@github-actions

github-actions Bot commented Sep 3, 2026 •

Copy link
Copy Markdown
Contributor

Test Coverage by Category

Category Unit Tests Google Sheets Conformance Property Cases Total
Array 42 552/552 ✓ 1,000 (2×500) 1,594
Database 35 182/182 ✓ 3,500 (7×500) 3,717
Date 373 418/418 ✓ 2,500 (5×500) 3,291
Engineering 245 886/888 ⚠ 5,500 (11×500) 6,633
Filter 11 81/81 ✓ 4,500 (9×500) 4,592
Financial 149 1,208/1,208 ✓ 2,000 (4×500) 3,357
Info 0 256/256 ✓ 4,500 (9×500) 4,756
Logical 121 267/267 ✓ 3,500 (7×500) 3,888
Lookup 69 393/393 ✓ 1,000 (2×500) 1,462
Math 545 2,006/2,006 ✓ 8,000 (16×500) 10,551
Operator 87 251/251 ✓ 7,500 (15×500) 7,838
Parser 83 93/93 ✓ 4,000 (8×500) 4,176
Query 37 — — 37
Statistical 529 3,191/3,191 ✓ 5,000 (10×500) 8,720
Text 327 803/804 ⚠ 4,000 (8×500) 5,131
Timezone 47 — — 47
Volatile 0 — 3,500 (7×500) 3,500
Web 29 59/59 ✓ 6,000 (12×500) 6,088
Total 3,090 10,646/10,649 66,000 (132×500) ~79,739

✓ = 100% passing · ⚠ = known deviation · The ~79,739 total counts formula evaluations (each conformance row and each property case = 1). GitHub Checks reports 4,207 Rust test functions: 3,090 unit + 159 property functions (shown as cases above) + 958 conformance/integration.

@hhimanshu
hhimanshu merged commit ce75619 into main Sep 3, 2026
9 checks passed
@hhimanshu
hhimanshu deleted the feat/tagged-evalresult-flow branch September 3, 2026 10:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant