Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
07c6456
ci(develop): Run the fast ctest suite on develop pushes
libraz Jul 18, 2026
a9c2133
ci(develop): Build the fast-test job in Debug to avoid runner OOM
libraz Jul 18, 2026
ffe7bf6
ci(develop): Give the fast-test job 60 min for the cold-cache build
libraz Jul 18, 2026
41f71da
ci(develop): Cap fast-test build parallelism to avoid OOM
libraz Jul 18, 2026
6927f3e
chore: Drop per-file license and copyright headers
libraz Aug 5, 2026
10b4ff4
feat(eval): Spill range-returning builtins and centralize implicit in…
libraz Aug 5, 2026
e0625e4
fix(eval): Correct number-format rounding and text-function edge cases
libraz Aug 5, 2026
6204b86
fix(eval): Harden numeric edge cases in math and statistics builtins
libraz Aug 5, 2026
59a5807
fix(eval): Guard date-serial range and broadcast array arguments in d…
libraz Aug 5, 2026
b04bd3d
feat(eval): Answer CELL() width, color, prefix and format from the re…
libraz Aug 5, 2026
7c7e615
perf(eval): Pool recalc layer workers and fix dependency-graph bookke…
libraz Aug 5, 2026
6688e9c
feat(utils): Add a configurable log sink and minimum severity level
libraz Aug 5, 2026
28dc222
fix(parser): Enforce the depth limit on flat chains and fix reference…
libraz Aug 5, 2026
f455fb7
fix(cf): Render a full DataBar when the min and max thresholds are equal
libraz Aug 5, 2026
1d0a5a1
fix(workbook): Propagate structural edits across every referencing model
libraz Aug 5, 2026
6342fb7
fix(io): Preserve unmodelled OOXML parts and fix read-path edge cases
libraz Aug 5, 2026
820bd00
fix(cli): Centralize file I/O and harden the eval, dump and recalc co…
libraz Aug 5, 2026
e76d1f6
feat(io): Emit XLSB styles and degrade gracefully on unsupported form…
libraz Aug 5, 2026
9b2d4e3
fix(pivot): Honour profile and report layout in the C API layout proj…
libraz Aug 5, 2026
24b2ef4
feat(c_api): Add phonetic, iterative, extended-font and XLSB result e…
libraz Aug 5, 2026
2700f97
feat(bindings): Expose phonetic, vertAlign and comment status across …
libraz Aug 5, 2026
00c7577
test(oracle): Recapture goldens and enforce divergence and coverage g…
libraz Aug 5, 2026
feeeac1
fix(parity): Skip the cross-language harness instead of passing with …
libraz Aug 5, 2026
122a5da
ci: Fail on stale flake allowlist entries and retune the ctest label …
libraz Aug 5, 2026
8b7b62f
docs: Document the CLI commands and restate the WASM size ceilings
libraz Aug 5, 2026
f8d9fb5
feat(core): Add borrowed cached values, an enumeration revision and a…
libraz Aug 5, 2026
066b979
feat(parser): Parse 3-D whole-column and whole-row spans and widen in…
libraz Aug 5, 2026
c78fe93
perf(eval): Track whole-axis references as range dependencies and sco…
libraz Aug 5, 2026
8d72403
fix(eval): Return #NUM! for non-finite results and rework the distrib…
libraz Aug 5, 2026
562e120
feat(io): Emit a shared-string table and escape control characters in…
libraz Aug 5, 2026
b75b33b
refactor(io): Drop the per-archive cumulative extraction cap from Zip…
libraz Aug 5, 2026
13ced89
fix(io): Complete the XLSB worksheet prefix, Ptg classes and dynamic-…
libraz Aug 5, 2026
cc5f8f7
feat(pivot): Sort items by a data field's aggregate when SortSpec::by…
libraz Aug 5, 2026
3a1e2bc
feat(cli): Add a paginate subcommand and precompute pagination track …
libraz Aug 5, 2026
5c8cebd
feat(c_api): Add pagination, batch style insertion and load diagnostics
libraz Aug 5, 2026
afe99b0
feat(bindings): Expose pagination and error display names across WASM…
libraz Aug 5, 2026
9e23679
build: Register the new sources and gate the bytecode VM behind FORMU…
libraz Aug 5, 2026
7670522
fix(eval): Route every ArrayValue allocation through a bounds-checked…
libraz Aug 5, 2026
64208ac
fix(eval,io): Give the evaluation and load-time arenas byte ceilings
libraz Aug 5, 2026
9f29a72
fix(io): Escape control characters per XML context in the OOXML writer
libraz Aug 5, 2026
7eba1e8
refactor(io): Share one raw-XML retention seam across the part readers
libraz Aug 5, 2026
e3b46aa
feat(core): Rework row storage, bulk range reads and structural edits
libraz Aug 5, 2026
104b0e6
feat(io): Retain XLSB worksheet-tail records and their sheet relation…
libraz Aug 5, 2026
9d0ba72
fix(eval): Order cyclic component members by address before iterating
libraz Aug 5, 2026
e068efa
fix(eval): Cap the depreciation schedule length in DDB, DB and VDB
libraz Aug 5, 2026
03a03cb
test(fuzz): Add a portable Ptg seed format to the XLSB fuzzer
libraz Aug 5, 2026
0eb2732
build: Register the new unit suites and add the source seam guards
libraz Aug 5, 2026
7f6c45d
chore(python): Add the uv lockfile for the Python package
libraz Aug 5, 2026
6694b0e
perf(io): Parse the shared-string and pivot-record parts in place
libraz Aug 5, 2026
ff66cad
perf(io): Let the workbook solely own the passthrough payload
libraz Aug 5, 2026
112f2ec
test(io): Resolve shared strings in the SAX corpus parity adapter
libraz Aug 5, 2026
f2ae8e3
docs: Document the WASM worksheet parsing memory profile
libraz Aug 5, 2026
9db7889
feat(core): Launch recalc workers without throwing on OS refusal
libraz Aug 5, 2026
5aab264
feat(capi): Expose an estimated workbook memory footprint
libraz Aug 5, 2026
990555e
feat(node): Report the workbook footprint as V8 external memory
libraz Aug 5, 2026
cdd0a86
test(io): Cover XLSB external-book names in the reader
libraz Aug 5, 2026
a496a62
fix(eval): Count every area an INDIRECT reference string names
libraz Aug 6, 2026
bc2147e
fix(eval): Spill lambda-helper errors per output slot
libraz Aug 6, 2026
94879b7
fix(eval): Detrend before detecting FORECAST.ETS seasonality
libraz Aug 6, 2026
2ce02ea
fix(eval): Report a finite F statistic for a perfect-fit LINEST
libraz Aug 6, 2026
df041f6
feat(eval): Honour a total_depth of 2 in GROUPBY and PIVOTBY
libraz Aug 6, 2026
c772f64
test(oracle): Retire the divergences closed by the eval fixes
libraz Aug 6, 2026
65b2d41
fix(parser): Consume a malformed UTF-8 lead byte in the tokenizer
libraz Aug 6, 2026
8051859
fix(io): Include <algorithm> in the OOXML writer
libraz Aug 6, 2026
5198f65
fix(eval): Evaluate the INDIRECT call that AREAS is asked to count
libraz Aug 6, 2026
5653be9
fix(eval): Restate the outer key on GROUPBY and PIVOTBY subtotal rows
libraz Aug 6, 2026
3bdf3fe
test(oracle): Refresh the goldens from Excel 365 ja-JP 16.111.2
libraz Aug 6, 2026
ab156ce
test(fuzz): Make the sanitizer set overridable and unify the XLSB corpus
libraz Aug 6, 2026
e15fe15
build(bench): Gate the Brotli wire size in the WASM size report
libraz Aug 6, 2026
41c17cc
fix(eval): Read number-format colour names in the UI locale
libraz Aug 6, 2026
347f2c4
chore(catalog): Realign the colour, lambda-error and ETS probes
libraz Aug 6, 2026
1119467
docs(readme): Restate the CLI, pivot-cache and verification sections
libraz Aug 6, 2026
d4b2239
docs(release): prepare release v0.9.7
libraz Aug 6, 2026
56c7b6a
fix(build): Repair the GCC libstdc++ build of two translation units
libraz Aug 6, 2026
a479c3f
test(concurrency): Bind the GETPIVOTDATA record loop by reference
libraz Aug 6, 2026
6eef22b
fix(eval): Clamp the digit count before converting it to int
libraz Aug 6, 2026
8907b61
ci(flakes): Drain the expected-flake allowlist to empty
libraz Aug 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
14 changes: 9 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,17 +79,21 @@ jobs:
# under -O0 + --coverage every bench is 5-10x slower and the
# gate trivially fails. Bench is also excluded from the local
# fast-suite definition in tests/bench/CMakeLists.txt.
run: ctest -LE "SLOW|LOAD|BENCH" --output-on-failure --timeout 60 -j 4
run: ctest -LE "SLOW|BENCH|TSAN" --output-on-failure --timeout 60 -j 4

- name: Check failures against allowlist
run: |
ctest --test-dir build-coverage --show-only=json-v1 > build-coverage/ctest-tests.json
if [ -f build-coverage/Testing/Temporary/LastTestsFailed.log ]; then
python3 tools/ci/check_test_failures.py \
--failed build-coverage/Testing/Temporary/LastTestsFailed.log \
--allowlist tools/ci/expected_flakes.txt
failed=build-coverage/Testing/Temporary/LastTestsFailed.log
else
echo "ctest produced no LastTestsFailed.log; assuming success"
failed=build-coverage/no-failures.log
: > "$failed"
fi
python3 tools/ci/check_test_failures.py \
--failed "$failed" \
--allowlist tools/ci/expected_flakes.txt \
--ctest-json build-coverage/ctest-tests.json

- name: Capture and filter lcov tracefile
run: |
Expand Down
71 changes: 69 additions & 2 deletions .github/workflows/develop-ci.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
# Formulon develop-branch CI workflow.
#
# Develop-branch checks layered on top of ci.yml so iteration on `main`
# stays fast. Currently a single job:
# Develop-branch checks layered on top of ci.yml so a red `develop` is
# caught before it is fast-forwarded onto `main`:
#
# - format : `make format-check` — clang-format (C++), ruff (Python),
# and biome (.mjs / .d.ts) dry-runs. Hard gate (binary check).
# - native-fast : Debug build + the fast ctest suite, gated by the
# same allowlist ci.yml uses (tools/ci/expected_flakes.txt). ci.yml's
# native job runs only on `main`, so without this a develop push gets
# no C++ test signal and a broken test can sit undetected until the
# main promotion. This mirrors the fast half of that gate without the
# coverage instrumentation, so it stays minutes not hours. Debug
# (-O0) matches ci.yml's build type and `make build`; a Release build
# OOM-kills the 2-core runner when compiling the heavy TUs in
# parallel.
#
# Pattern mirrored from ../mygram-db's develop-ci.yml: develop-only
# checks live here, main keeps the minimal build+test signal in ci.yml.
Expand Down Expand Up @@ -58,3 +67,61 @@ jobs:

- name: format check
run: make format-check

native-fast:
name: Build and fast test (native, Linux)
runs-on: ubuntu-latest
# 60 to match ci.yml's native job. The first run on an empty build
# cache does a full cold Debug compile of the whole test suite on the
# 2-core runner, which is slow; subsequent runs reuse the cached
# build/ dir and only recompile changed TUs.
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v7

- name: Install build tooling
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
build-essential cmake ninja-build libbrotli-dev brotli

- name: Cache CMake build directory (debug)
uses: actions/cache@v6
with:
path: build
key: debug-build-${{ runner.os }}-${{ hashFiles('CMakeLists.txt', 'Makefile', 'cmake/**', 'CMakePresets.json') }}
restore-keys: |
debug-build-${{ runner.os }}-

- name: Configure (debug preset)
run: cmake --preset debug

- name: Build
# Cap parallelism at 2 (the runner's physical core count).
# Bare `--parallel` fans out to all logical CPUs and links the
# heavy Debug test/bench binaries concurrently, which exhausts the
# 7 GB runner and gets the compilers OOM-killed (SIGTERM / 143).
run: cmake --build build --parallel 2

- name: Run fast tests
id: ctest
continue-on-error: true
working-directory: build
# Same label exclusion as ci.yml's native job: SLOW is the
# minutes-to-tens-of-minutes tier; BENCH and TSAN run out of band.
run: ctest -LE "SLOW|BENCH|TSAN" --output-on-failure --timeout 60 -j 4

- name: Check failures against allowlist
run: |
ctest --test-dir build --show-only=json-v1 > build/ctest-tests.json
if [ -f build/Testing/Temporary/LastTestsFailed.log ]; then
failed=build/Testing/Temporary/LastTestsFailed.log
else
failed=build/no-failures.log
: > "$failed"
fi
python3 tools/ci/check_test_failures.py \
--failed "$failed" \
--allowlist tools/ci/expected_flakes.txt \
--ctest-json build/ctest-tests.json
164 changes: 163 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,167 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.9.7] - 2026-08-06

### Added

- `formulon paginate`, a CLI subcommand that prints the resolved print areas,
row and column page breaks, and page count for one sheet. It is backed by
`fm_workbook_paginate` with an owned `fm_pagination_t`, and reaches WASM,
Node and Python as `paginate`.
- Workbook memory-footprint estimate: `Workbook::approximate_memory_bytes()`
and `fm_workbook_memory_usage` report an `O(cells)` pressure signal covering
the cell store, the shared-string storage every `Text` value borrows from,
the passthrough part payloads and the workbook metadata. The Node addon
reports the delta to V8 as external memory on create, load, recalc and
handle destruction, and exposes `memoryUsage()`.
- `fm_styles_add_batch` installs and deduplicates fonts, fills, borders, cell
xfs and number formats in one call, ordering the tables so an xf can
reference indices produced by the same batch.
- `fm_error_display_name` (`errorDisplayName` / `error_display_name`) for the
Excel literal of a cell error code, and `fm_workbook_xlsb_read_diagnostics`
for the undecoded formula and defined-name counters captured during an XLSB
load.
- Phonetic text, iterative-calculation settings, extended font records
carrying `vertAlign`, and `fm_workbook_save_xlsb_with_result`. All are
additive, so the existing ABI is unchanged. The binding surface gains
`getCommentResult`, conditional-format visual rules, differential formats,
comment enumeration and pivot cache metadata alongside them.
- XLSB writer: a native `xl/styles.bin` emitter, round-tripped row/column
layout, merged rectangles and the `date1904` flag, the mandatory worksheet
prefix with view flags, zoom and frozen panes, and an `xl/metadata.bin`
carrying the dynamic-array entry. Worksheet-tail records the model does not
express — conditional formatting, data validation, hyperlinks, auto-filter,
print setup, breaks and the drawing / table part references — are retained
verbatim along with their sheet relationships.
- OOXML writer interns literal text cells into a generated
`xl/sharedStrings.xml` and writes those cells as `t="s"`.
- Parser accepts a 3-D whole-column (`Sheet1:Sheet3!A:A`) or whole-row span,
and treats a space before a quoted sheet name or a parenthesized range as
the intersection operator.
- `GROUPBY` and `PIVOTBY` honour a `total_depth` of ±2, emitting one subtotal
row per outer group with the outer key restated in the first key column.
Pivot items sort by a data field's aggregate when `SortSpec::by_field` is
set.
- A configurable process-wide structured-log sink and minimum severity level.

### Fixed

- Number-format colour names are read in the UI locale: the ja-JP profile
accepts the localized names and the indexed `[色N]` form, while the English
`[Red]` / `[ColorN]` spellings surface `#VALUE!` exactly as Excel does.
- Dynamic-array and lambda semantics: `BYROW` / `BYCOL` spill an errored slice
into its own output cell instead of collapsing the call, `REDUCE` and `SCAN`
hand the body an errored cell verbatim so an `IFERROR` guard can recover,
and every `ArrayValue` allocation routes through a bounds-checked seam that
validates each axis against the Excel grid.
- Numeric and statistical edge cases: `T.INV`, `F.INV` and `BETA.INV` invert
by bisection over an expanding bracket, `YIELD` and `ODDFYIELD` clamp to the
yield domain `PRICE` accepts, `DDB` / `DB` / `VDB` cap the schedule length,
`LINEST` reports a finite F for a perfect fit, `FORECAST.ETS` detrends
before detecting seasonality, and non-finite results surface as `#NUM!`.
- Number-format rounding and text functions: a shared rounding helper replaces
ad hoc rounding in `FIXED`, `DOLLAR`, `BAHTTEXT` and the numeric renderer,
the 32,767 UTF-16 unit cap is enforced in `SUBSTITUTE`, and the half-to-full
katakana voicing tables are deduplicated.
- Date builtins reject serials outside Excel's `0`..`9999-12-31` range and
broadcast array arguments cell by cell.
- `AREAS` evaluates the `INDIRECT` call it is asked to count, so a resolution
counts as one area and a failure propagates as that error.
- Parser: the depth limit is validated against the completed AST so a flat
left-associative chain is covered, a malformed UTF-8 lead byte is consumed
instead of spinning the tokenizer forever, and `$`-bearing identifier runs
such as `A$$1` are rejected.
- Structural edits propagate across every referencing model — hyperlink
locations, data-validation formulas, pivot-cache sources, conditional-format
and table ranges — and formulas are re-indexed when an edit rewrites a
defined name.
- OOXML read path preserves unmodelled parts: chart, dialog and macro sheets
come through as opaque sheets, package-level and per-sheet relationships of
unrecognised types are re-emitted with zip-slip validation, and workbook and
worksheet elements the model does not express survive the next save
verbatim. XML-invalid C0 controls are escaped per context.
- XLSB Ptg codec emits reference-class Ptgs for cell and range arguments and
value-class Ptgs for function results, decodes the `PtgMem*` markers, and
encodes `BrtColor` with `fValidRGB` in bit 0.
- Pivot layout projection through the C API honours the workbook's Excel
profile and the selected Compact, Tabular or Outline report layout, so the
default ja-JP profile emits localized labels instead of the legacy English
grid.
- Cyclic component members are ordered by address before iterating, so the
Gauss-Seidel solver commits in a fixed order across standard-library
implementations rather than following DFS pop order.
- A DataBar rule whose min and max thresholds are equal renders a full bar.
- CLI: `eval`, `dump` and `recalc` share one atomic file-I/O path, exit codes
collapse to `{0, 1, 64}`, and `eval` runs through the read-only array C API
so `=A1+1` sees an empty `A1`.
- Recalc workers launch through `launch_thread`, which returns
`Expected<Thread, Error>` instead of terminating when the OS refuses a
thread; the pool keeps whichever workers started and falls through to serial
evaluation when none do.
- The evaluation and load-time arenas carry byte ceilings, so a hostile
formula degrades to a per-cell error instead of growing until the process or
the WASM host page aborts.

### Performance

- Whole-axis references are tracked as compact rectangle dependencies instead
of promoting the formula to volatile, and Tarjan runs over the induced
subgraph of dirty cells rather than the workbook-wide graph.
- Recalc layer workers are pooled behind a barrier-synchronized
`LayerWorkerPool` for the whole parallel pass instead of being spawned and
joined per layer.
- Row storage became `RowCells`, a contiguous run beginning at the row's first
populated column, so memory scales with content rather than used width.
`Sheet::read_range` appends a whole rectangle under one lock acquisition.
- The shared-string and pivot-record parts, the OOXML worksheet parse and the
metadata shell parse run through an in-place XML loader, halving peak parse
memory, and the workbook solely owns the passthrough payload instead of
mirroring it.

### Changed

- The bytecode compiler, optimizer and VM compile only when
`FORMULON_BUILD_VM` is on (defaulting to `FM_BUILD_TESTING`), so release
CLI, WASM and binding binaries no longer carry the experimental pipeline.
- `ZipReader` drops the per-archive cumulative extraction cap; the zip-bomb
guard narrows to the per-entry size, entry-count and compression-ratio caps.
- Per-file license and copyright headers are removed; the terms live in the
top-level `LICENSE`.

### Testing

- Divergence and coverage governance: `divergence_check.py` validates every
entry against a real case, suite, alias or documented non-oracle scope, and
`golden_coverage_check.py` fails when a declared case has no golden. Each
secondary-oracle golden file is registered as its own ctest entry so one
allowlist exception can no longer mask every failure.
- Goldens recaptured against Excel 365 ja-JP 16.111.2, with the
`cross_sheet_refs`, `intersect_operator`, `iterative_calc` and
`spill_collision` suites added.
- The cross-language parity harness returns ctest's skip code when fewer than
two channels are active, instead of reporting success on one.
- An XLSB libFuzzer target with a portable Ptg seed format, and source-seam
guards that fail when a second `ArrayValue` allocation site or raw-XML
retention implementation appears.

### Build / CI

- A `native-fast` job runs the fast ctest labels on develop pushes, so a red
develop surfaces before the promotion to main.
- The WASM size report gates the Brotli wire size (768 KiB hard, 640 KiB soft)
on equal footing with the uncompressed size; a host without `brotli` on
`PATH` skips that half instead of failing it.
- CI fails on `expected_flakes.txt` entries that no longer name a registered
test.

### Documentation

- The READMEs document the CLI commands, both WASM size ceilings, and the
WASM worksheet parsing memory profile.

**Detailed Release Notes**: [GitHub Release](https://github.com/libraz/formulon/releases/tag/v0.9.7)

## [0.9.6] - 2026-07-19

### Added
Expand Down Expand Up @@ -269,7 +430,8 @@ See the
[GitHub release page](https://github.com/libraz/formulon/releases/tag/v0.9.0)
for the full auto-generated change list.

[Unreleased]: https://github.com/libraz/formulon/compare/v0.9.6...HEAD
[Unreleased]: https://github.com/libraz/formulon/compare/v0.9.7...HEAD
[0.9.7]: https://github.com/libraz/formulon/compare/v0.9.6...v0.9.7
[0.9.6]: https://github.com/libraz/formulon/compare/v0.9.5...v0.9.6
[0.9.5]: https://github.com/libraz/formulon/compare/v0.9.4...v0.9.5
[0.9.4]: https://github.com/libraz/formulon/compare/v0.9.3...v0.9.4
Expand Down
29 changes: 25 additions & 4 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.20)
project(formulon VERSION 0.9.6 LANGUAGES CXX)
project(formulon VERSION 0.9.7 LANGUAGES CXX)

# ---------------------------------------------------------------------------
# Language standard and global settings
Expand Down Expand Up @@ -40,12 +40,17 @@ option(FM_BUILD_FUZZ "Build libFuzzer harnesses (clang only)" OFF)
option(FM_ENABLE_COVERAGE "Enable gcov/lcov" OFF)
option(FM_USE_SYSTEM_DEPS "Prefer system-installed third-party deps" OFF)
option(FM_ENABLE_PTHREAD "Enable multithreaded recalc" ON)
option(FORMULON_BUILD_VM "Build the experimental bytecode VM (tests default to ON)" ${FM_BUILD_TESTING})
# Tree-walker / VM parity harness: when ON, every `evaluate(...)` call also
# compiles the AST through the bytecode pipeline and asserts the VM result
# matches the tree-walker's. Defaults to OFF so production builds carry no
# overhead; the parity sweep test (`vm_parity_test.cpp`) flips it on.
option(FORMULON_VM_PARITY_CHECK "Enable tree-walker / VM parity assertions" OFF)

if(FORMULON_VM_PARITY_CHECK AND NOT FORMULON_BUILD_VM)
message(FATAL_ERROR "FORMULON_VM_PARITY_CHECK requires FORMULON_BUILD_VM=ON")
endif()

include(FormulonWarnings)
include(FormulonCommonDeps)

Expand Down Expand Up @@ -110,8 +115,10 @@ endif()
# OOXML writer, and parser sources that have landed so far; new modules are
# appended here as they are implemented.
set(FORMULON_CORE_SOURCES
src/utils/a1_column.cpp
src/utils/double_format.cpp
src/utils/structured_log.cpp
src/utils/thread_launch.cpp
src/value.cpp
src/sheet.cpp
src/workbook.cpp
Expand All @@ -135,6 +142,7 @@ set(FORMULON_CORE_SOURCES
src/io/ooxml/cell_ref_writer.cpp
src/io/ooxml/emission_plan.cpp
src/io/ooxml/relationship_writer.cpp
src/io/ooxml/shared_strings_writer.cpp
src/io/ooxml/sheet_xml_builder.cpp
src/io/ooxml/workbook_xml_builder.cpp
src/io/ooxml/zip_part_writer.cpp
Expand Down Expand Up @@ -163,6 +171,7 @@ set(FORMULON_CORE_SOURCES
src/io/xlsb/sheet_writer.cpp
src/io/xlsb/sst_writer.cpp
src/io/xlsb/styles_reader.cpp
src/io/xlsb/styles_writer.cpp
src/io/xlsb/writer.cpp
src/cf/cf_evaluator.cpp
src/cf/cf_helpers.cpp
Expand All @@ -188,10 +197,8 @@ set(FORMULON_CORE_SOURCES
src/print/page_setup.cpp
src/print/print_area.cpp
src/print/pagination.cpp
src/eval/array_alloc.cpp
src/eval/coerce.cpp
src/eval/compiler.cpp
src/eval/optimizer.cpp
src/eval/vm.cpp
src/eval/utf8_length.cpp
src/eval/text_ops.cpp
src/eval/rng.cpp
Expand Down Expand Up @@ -335,6 +342,7 @@ src/eval/aggregate_lazy.cpp
src/c_api/parts/pivot_internal.cpp
src/c_api/parts/pivot_layout.cpp
src/c_api/parts/pivot_table.cpp
src/c_api/parts/pagination.cpp
src/c_api/parts/sheet_layout.cpp
src/c_api/parts/sheets_ui.cpp
src/c_api/parts/styles.cpp
Expand All @@ -346,6 +354,17 @@ src/eval/aggregate_lazy.cpp
src/c_api/generated/workbook_counts.cpp
)

# The bytecode compiler/optimizer/VM is an experimental parity harness, not
# the production evaluator. Keep it out of release CLI, WASM, and binding
# binaries unless a test or an explicit developer build asks for it.
if(FORMULON_BUILD_VM)
list(APPEND FORMULON_CORE_SOURCES
src/eval/compiler.cpp
src/eval/optimizer.cpp
src/eval/vm.cpp
)
endif()

add_library(formulon_core OBJECT ${FORMULON_CORE_SOURCES})

# Engine core depends on miniz (zip/deflate), pugixml (XML), pcre2
Expand Down Expand Up @@ -403,6 +422,8 @@ if(FM_BUILD_CLI)
src/cli/eval_cmd.cpp
src/cli/recalc_cmd.cpp
src/cli/dump_cmd.cpp
src/cli/paginate_cmd.cpp
src/cli/file_io.cpp
src/cli/render.cpp
)
add_executable(formulon_cli ${FORMULON_CLI_SOURCES})
Expand Down
Loading