Formulon is a headless, Excel-compatible calculation engine — a C++17 core that defaults to the Windows Excel 365 (ja-JP) behavior profile, with every known divergence explicitly tracked against Excel oracle data. The same engine is packaged for the browser (WebAssembly), for Python, and for native command-line use, so a workbook recalculates to the same values wherever it runs.
No Excel installation, no Microsoft runtime, no COM automation required. The WASM build runs in browsers and Node, and the Python package runs the same WASM core through wasmtime; native CLI binaries ship for darwin-arm64, linux-x64, and linux-arm64.
npm install @libraz/formulon # JavaScript / TypeScript (WASM)
pip install formulon # PythonCLI binaries are available from GitHub Releases.
- Checked against real Excel. The runtime default is
win-365-ja_JP, and profile-specific oracle suites pin observed Excel behavior. Formula results are pinned against Mac Excel 365 (ja-JP); pivot tables and print layout are pinned against Windows Excel 365 (ja-JP), because creating PivotTables by automation requires Windows COM. Both come from a verified Microsoft 365 install. Outputs are checked for bit-level parity against golden data regenerated from the real product; every accepted divergence (transcendental ulp drift, volatile-function snapshots, Excel quirks where Formulon deliberately keeps a saner answer) is recorded case-by-case intests/divergence.yamlwith a reason and the last verified Excel build. - One C++ core, identical results everywhere. The browser, Python, and CLI builds all ship the same engine rather than separate calculation logic, so there is no second implementation for results to drift against.
- WASM size budget. CI enforces a 3.25 MiB uncompressed and 864 KiB Brotli hard ceiling, and reports 3.00 MiB / 832 KiB soft ceilings. Brotli is the wire size that actually binds, so it gates on equal footing. Run
make size-checkto measure the current artifact. - Small dependency set. Engine deps:
miniz(zip/deflate),pugixml(XML + XPath 1.0),PCRE2(Excel-compatible regex forREGEX*),double-conversion(Grisu3 shortest-roundtripdtoa). Linear algebra, UTF-8 handling, and most number coercion are in-tree. - C++ written for auditability.
Expected<T, Error>error handling, RAII,-fno-exceptions -fno-rtti, Google C++ style.
Anywhere a spreadsheet needs to be computed without booting Excel:
- running
.xlsxworkbooks headlessly in batch jobs or data pipelines, - evaluating Excel-style formulas inside a web application, in the browser,
- embedding calculation into internal tools, bots, or notebooks,
- validating formulas and migrating legacy spreadsheets.
For a worked example of embedding the engine, see formulon-cell, a browser spreadsheet UI built on @libraz/formulon. It also serves as an integration test, exercising the npm package end to end in a real browser.
Formulon deliberately does not cover:
| Area | Reason |
|---|---|
| VBA execution | Security. vbaProject.bin is preserved byte-for-byte, never executed. |
Legacy .xls (BIFF8, Excel 97–2003) |
Out of scope for Excel 365 compatibility. |
| Chart / drawing rendering | Belongs to a rendering layer, not the engine. |
| PowerQuery (M) / DAX | Separate engine, separate problem domain. |
| Pivot cache regeneration | The stored pivotCacheRecords snapshot is preserved as-is, never rebuilt from the source range. PivotTable results are evaluated on demand through the API. |
| Spreadsheet UI | Rendering belongs to the caller. The engine exposes what a UI needs to drive it — viewport recalc (partialRecalc), conditional-format evaluation over a range, spill info. |
These are permanent non-goals, not "not yet." The scope is finite on purpose.
| Surface | Name | Notes |
|---|---|---|
| npm | @libraz/formulon |
WASM ESM module, type definitions included. Node 22+, browsers, workers. The default build is single-threaded and needs no cross-origin isolation; @libraz/formulon/threads adds worker threads for recalcParallel. |
| PyPI | formulon |
Python 3.9+ py3-none-any wheel that bundles formulon_capi.wasm plus a pure-Python wrapper. pip resolves the platform-specific wasmtime runtime. |
| GitHub Releases | formulon-<version>-<platform-arch>.tar.gz |
Standalone CLI binaries (eval, recalc, dump, paginate) for darwin-arm64, linux-x64, linux-arm64. |
Every surface computes the same results from the same input. All three inflate each worksheet part into memory whole before parsing it (the zip reader caps this at 100 MiB per entry, 256 MiB per load); the WASM builds — the npm and PyPI packages — then always build a DOM tree from that buffer. The native CLI switches to a streaming parser for worksheets past 256 KiB instead, which skips building the DOM tree — that implementation costs binary size the WASM budget does not have — but still holds the same inflated XML buffer first, so its peak memory is not a fixed window either. Sheets are read one at a time on every surface, so the peak is per worksheet, not per workbook; the practical ceiling on WASM is the host's 32-bit address space. Results are identical either way.
After placing a release binary on PATH, use eval for a scalar formula,
recalc to write a recalculated workbook, dump for a text snapshot, and
paginate to resolve the print area, page breaks, and page count.
formulon eval '=SUM(1,2,3)'
formulon recalc input.xlsx -o output.xlsx
formulon recalc --threads 4 input.xlsx -o output.xlsx
formulon dump output.xlsx --formulas
formulon paginate output.xlsx --sheet 0All four commands accept -- to end option parsing. Put command options
before it, then pass exactly one positional formula (eval) or input path
(recalc, dump, paginate); this also allows a relative path beginning
with -, for example formulon dump --sheets -- -input.xlsx.
recalc accepts .xlsx or .xlsb for input and output. It writes its success status
to stderr as formulon: recalc: ok, wrote M bytes to 'OUT'; pass --quiet to
suppress that status line. Load/save loss warnings — both XLSB and OOXML —
remain visible under --quiet. By default it recalculates serially;
--threads N opts into the parallel SCC scheduler (0 auto-detects, 1
stays on the caller thread, and 2..8 sets a worker cap) and reports
per-pass telemetry in the status line.
All 523 catalogued Excel functions are recognized, but recognition is not the same as full Excel-compatible execution. The function catalog exposes availability explicitly; make function-status reports the current split.
The two top-level rows are exclusive and sum to the full 523; the environment-bound row is a subset of the 508 real implementations, called out separately because a fixed golden cannot fully describe it — it is not an additional category (508 + 15 = 523, not 525).
| Availability | Count | Meaning | Examples |
|---|---|---|---|
| Real implementation | 508 | Evaluates inside the normal calculation engine and is covered by unit and/or oracle tests. | Math, statistics, lookup, text, dynamic arrays |
| ↳ of which environment-bound | 2 | A real implementation whose result depends on host or workbook state, so a fixed golden cannot fully describe it. Counted within the 508 above. | INFO, CELL |
| Unavailable stub | 15 | Requires external services, network I/O, COM providers, or OLAP connections that Formulon does not embed; returns a fixed error. | PY, WEBSERVICE, STOCKHISTORY, IMAGE, RTD, TRANSLATE, DETECTLANGUAGE, COPILOT, CUBE* |
104 oracle categories are defined. The formula and conditional-formatting tracks regenerate from Mac Excel 365 ja-JP; the workbook track regenerates from Windows Excel 365 ja-JP, and its goldens carry a capture identifier that pins every suite to a single verified Microsoft 365 session. Current local verification:
| Check | Result |
|---|---|
ctest -LE "SLOW|BENCH|TSAN" — make test, the PR gate |
all passing |
ctest -LE "BENCH|TSAN" — make test-slow, adds the SLOW tier |
all passing |
| Primary formula oracle | 4546/4546 passing, 125 documented skips |
| Conditional-formatting oracle | 23/23 |
| Workbook oracle (pivot + print) | 73/73 passing, 9 documented skips |
| Imported third-party engine corpus (cross-check) | 12510/12510 passing, 168 documented divergences |
Three labels partition the CTest suite: SLOW (minutes-scale integration and concurrency cases), TSAN (thread-sanitizer runs), and BENCH (microbenchmark regression checks, whose threshold is tunable, so they run on demand). Everything else is the unlabeled fast tier that CI gates on; there is no separate load-test tier. The libFuzzer harnesses also carry the SLOW label, but they exist only in a build configured with -DFM_BUILD_FUZZ=ON (make fuzz), not in a default build or in CI. They need a Clang that ships the libFuzzer runtime, which the Apple toolchain does not, so macOS needs a separate LLVM. AddressSanitizer is off by default there as well — it deadlocks in its own shadow-memory setup against recent macOS dynamic linkers — so a macOS fuzz run detects crashes, timeouts and undefined behaviour but not heap corruption.
Every skip is an explicit divergence, host-service dependency, volatile/environment-bound case, or driver limitation, not a silent stub. Of the 523 catalogued functions, 519 satisfy all six closure conditions (behaviors_declared / cases_cover_behaviors / golden_present / divergence_documented / not_in_pilot / behavior_drift); the remaining 4 (ARRAYTOTEXT, FILTERXML, GETPIVOTDATA, PHONETIC) fail only behaviors_declared — their behavior taxonomy is under-specified. JIS closes as a declared alias of DBCS: Excel rewrites that ja-JP formula-bar spelling before it stores or evaluates a formula, so no oracle case can name it, and the closure harness resolves the alias to the function it defers to rather than taking the declaration on trust.
Beyond formula results, pivot tables and print areas / pagination have a dedicated workbook oracle track, captured through a WSL2 → Windows COM bridge. Its nine remaining skips are all the same Excel quirk: at a print scale or zoom of 50% or less, Excel's page-break preview emits column auto-breaks that do not follow geometric pagination, so the observed break set stops shrinking with the scale and grows again at 25%. Each skipped case records the Microsoft 365 observation it was measured against.
New workbooks use the win-365-ja_JP formula profile by default; callers can switch with the profile-id API (mac-365-ja_JP, win-365-ja_JP). English-locale profiles are intentionally not exposed until matching EN oracle data and verified locale-specific behavior are available.
The OOXML reader/writer round-trips sheets, styles, conditional formatting, comments, hyperlinks, merges, data validations, defined names, tables, and pivot tables; an MS-XLSB reader/writer covers cell values, styles, cross-sheet 3-D references, and common tokenized formulas, with array-constant literals and post-2007 "future function" IDs still limited compared to the OOXML path. Workbook operations are available through the C ABI and language bindings; the CLI deliberately exposes only eval, recalc, dump, and paginate.
Feedback, issue reports, and oracle divergence reports are very welcome.
The fastest way to help right now is to donate Excel oracle data from your locale. If you run Excel 365 anywhere other than Mac ja-JP, one command (make oracle-contribute) drives Excel, captures goldens, and walks you through the PR. See CONTRIBUTING.md for the full flow and the rationale for why this is community-driven.