Last Updated: 2026-09-08 (0.22.0)
For detailed phase completion status and roadmap, see plan/roadmap.md
New in 0.22.0 (2026-09-08) — the 0.21.0 dogfood's follow-through (#646, #647, #648, #650); twelve Breaking: entries, each a behaviour fix toward Excel or the contract:
- ✅ Named cell styles survive every write (#610) —
cellStyleXfs,cellStyles,tableStyles,colorsand the stylesextLstride through the in-memory writer verbatim (PreservedStyleParts), everycellXfkeeps itsxfId, source cellXfs are registered positionally;"written verbatim in element text (#611) - ✅ Unbounded
--stream viewstreams (#635) — csv/json/markdown as aPayload.Streamed, row by row, inside the--jsonenvelope too; the 1M × 41 dogfood dump completes in a 384 MB heap.xl schema --jsonpublishes each verb'sstream(o1/backend/refused) andrefusedWith(#638); the shared-string table is a value (ExcelIO.loadSharedStrings) parsed once per run and shared withcell(#640) - ✅ Formula semantics per Excel — a structural delete writes
#REF!for the deleted reference alone (#629);COUNT/COUNTA/COUNTBLANKerror semantics (#630); the seven modern error values (#SPILL!…#GETTING_DATA) asCellErrorcases plusERROR.TYPE(#630); a single cell in a range slot asRangeForm.CellandSUMIF/AVERAGEIFsizingsum_rangetorange(#631);OFF_GRID_REFwarning,--strict-gated, for references dragged off the grid (#628); AutoFit measures cached values, never formula text (#613) - ✅ CLI contract cleanup —
INVALID_ARGUMENT(#617) andNAME_NOT_FOUND(#626) typed errors;--helpon stdout, exit 0, as an envelope under--json(#620); oneNo such filediagnostic for every verb (#621);diffcompares caches with akindper changed cell (#607);filter --limit 0= no limit and a{matched, shown, truncated, limit, rows}document (#639);stats/viewtakeB:B/3:3spans and an emptystatsis a result withNO_NUMERIC_VALUES(#641); positional-path hint (#619);Excel.readSheetreturnsXLResult[Sheet]and everySheetNotFoundnames its candidates (#615);rename-sheetrefusal punctuation (#644);xl-agenttraces record the sandbox exit code (#622)
New in 0.21.0 (2026-09-08) — agent-first Wave 2a (ADR-017) plus the dogfood fixes it surfaced:
- ✅ The
Editalgebra (#582) —com.tjclp.xl.ops.Edit, 49 cases, one vocabulary for every batch op and mutating verb;Edit.applyAll(all-or-nothing),Edit.plan(semantic dry-run),Edit.validate,Edit.lower/Patch.toEdits,EditSchema; a refusingFormulaSupport.textOnlyand xl-evaluator'sEvalFormulaSupport;wb.edit/sheet.editin the prelude; seven laws as ScalaCheck properties; the CLI's fill/copy/sort/clear/autofit/group kernels moved intoSheet - ✅
RowCodecrecords (#590) —final case class Order(...) derives RowCodec;readRows/readRowsByHeader/columnHeaders/columnOf/putRows/putRowsWithHeader/putTable; typedRowCodecError; ADR-008 amended - ✅
CellRecord+SheetSource(#585) — one cell projection behind every read verb from two strategies (loaded workbook, O(1) streaming); typed--jsonforsearch/stats/cell/filter;viewwithout a range shows the used range;filter --stream; a property law pins byte-equal payloads from both sources - ✅ Scripting completions (#589) —
Excel.writeChecked(compute only the uncached formulas, write, return theRecalcResult),readSheet,readMetadata,modifyR,orExit/exitMessage(the CLI'srenderDiagnostic),Sheet.collapseRows/Cols+expandRows/Cols - ✅ Contract as a CI gate (#592) — golden runner, generated-docs drift and
ContractSpecas a job,scripts/smoke-cli-contract.shagainst the assembly JAR and every native binary; the xl-agent grader reads--jsonenvelopes only and locks the skill zip to the binary's release - ✅
_xlfn.remainder (#577, #588) — CF/DV/defined-name formulas throughFormulaStorage;xlfn-missinglint;FunctionFlags.volatile - ✅ Dogfood fixes — an edit no longer withdraws caches of unparseable formulas it cannot reach (
ReferenceScan/DependencyGraph.editCone, #606); whole-column/row forms (RangeForm) survive print, drag and structural edits, error literals parse (TExpr.ErrorLit), off-grid drags write#REF!(#612); typedRESOURCE_LIMIT/MEMORY_PRESSUREunderMemoryGuardinstead of a rawOutOfMemoryError(#636);--stream searchstops at--limitwithtotalExactand--total(#637); typed, locatedrename-sheetrefusals (FORMULA_ERROR,SheetRenamer.renameLocated) and quoted sheet qualifiers incell(#608, #609) - ✅ Platform shims (ADR-016 Wave 25 A1, #542) —
java.security/java.awt/java.util.zipeach behind one file (platform.Sha256,render.TextMeasure), byte-identical output; ASCII-only column letters; the nativexlreports its build version viaBuildInfo; ADR-016 itself deferred with re-entry triggers
New in 0.20.0 "Contract" (2026-09-07) — the agent-first CLI and scripting contract (ADR-017 wave 1) on top of the Excel-parity and calculation-integrity waves:
- ✅ One CLI contract — a 0/1/2/3 exit table (ok / findings / usage / failed), errors and
Warning[CODE]:notices on stderr, one seven-key--jsonenvelope for every verb, one sheet rule (Resolve) on every path, globals anywhere on the command line, a registry behind all 32 batch ops, andxl schema/batch --schema/functions --jsonpublishing the surface; 86 goldens pin what an agent sees - ✅
describe,audit,deps— orient, find what is broken (cached errors, uncached/unparseable formulas, cycles, unresolved names), trace a number; backed byQualifiedGraph/WorkbookAudit/WorkbookSummary, also in the prelude - ✅ Excel-parity wave (#556, #561, #562, #564, #565, #480) — post-2007 functions stored as
_xlfn.NAMEand LET/LAMBDA parameters as_xlpm., dates render as serials in text positions, DATE normalizes overflow, AND/OR fold ranges, error-literal criteria match error cells,^is left-associative - ✅ Calculation integrity (#499, #563, #507, #504, #508, #569–#572) — uncached inputs evaluated before use, stale caches withdrawn after failures, scenario-table skips reported, authored formulas validated
- ✅ Library —
Sheet.putAt/styleAt/mergeAt/commentAt,Workbook.named, boundedARefnavigation, typed reads see cached formula values (#477),RecalcOptions,rename-sheetrewrites references (#559), property-only rows emitted once (#558), case-insensitive sheet-name uniqueness - ✅ Toolchain — Scala 3.9.0 LTS, Temurin 25 pinned in
.mill-jvm-version, Claude Code remote sessions (#554);calcChain.xmldropped on dirty writes with acalc-chain-stalelint (#555)
New in 0.19.3 "Namesake" (2026-08-14) — defined-name resolution indexed:
- ✅ Defined-name lookup is O(1) (#535/GH-536) — name resolution was a linear scan of the name table and dynamic-name classification ran it per sheet × name (O(sheets × names²) per recalc); a production 96,384-name model went from >33 min (unfinished) to 8.2s, its
iterate="1"original converges in 2/300 rounds in 8.4s, a 126,510-name sibling exhausting 400 rounds completes in 27s. Lazy per-metadataDefinedNameIndex, first-declared-wins, per-code-point case folding ≡equalsIgnoreCase, property-tested against the scan it replaced. Follow-ups: #537 (iterative-path costs), #538 (mutation-API case sensitivity)
New in 0.19.2 "Fixpoint" (2026-08-08) — recalculation & seeding integrity (wave 24) + the perf stack + two lint corruption classes:
- ✅ Recalculation 7–37× faster (#521, #523, #524) — linear Kahn core, memoized range-edge expansion, aggregate-fold memoization w/ single-flight, stack-safe Tarjan; 9,900×SUM(5000): 47.9s→1.30s, 200k book: 95s→13.8s; SIGTERM now kills a mid-recalc process in ~2s (#519) and
recalc --parallel Nships equivalence-gated (#520) - ✅
external-ref-danglinglint (#525/#527) — dangling external-workbook ordinals (the cross-workbook sheet-transplant class; a field incident lost 935 formulas to Excel repair while lint read clean); #526 tracks the adoption/remap API - ✅
defined-name-invalidlint (#528/#530) — same-scope defined names colliding under Excel's case/width/kana-insensitive comparison (g/g, html/HTML — legacy fossils Excel removes, sometimes silently), 255-char limit, whitespace/control chars; suffix-less MS externalLinkPath rel types whitelisted (#529) - ✅ Opt-in wave-parallel recalculation (#520) —
Workbook.recalculateParallel(N)/xl recalc --parallel Nevaluates independent static waves concurrently, pins one clock generation, and folds results deterministically; dynamic references remain in the sequential evaluate-last bucket and iterative books retain their sequential SCC fixpoint - ✅ One pass = the global fixpoint (#492, #491) — iterative recalculation walks the SCC condensation in dependency-first order instead of splitting into preOrder / one flat Jacobi / postOrder, so no read can fall back to a stale cache and re-recalculating a correct multi-SCC circular book no longer corrupts it.
RecalcResult.cycles/.unconverged/.certifiedreport per-component verdicts - ✅ Cycles warm-start from numeric caches (#469) — zero-seeding no longer wipes valid caches of mutually-ISERROR-guarded pairs
- ✅ Data-table what-if lanes evaluate their precedent cone (#493, #494) — acyclic grids no longer seed silently FLAT, guarded XIRR corners seed real rates, and a guard resolving to its error arm surfaces as
ErrorGuardFiredinstead of banking its text arm - ✅ Structural edits refuse to tear a data-table interior (#495) — was a silent degrade to constants that
data-table-torncould not see - ✅ Cache-safe CLI writes (#468, #481, #496) — dirty-cone-scoped recalc,
--no-recalc/--preserve-caches,--strictexit codes on write verbs,batchhonoring declaredcalcPr - ✅
####for overflowing numbers in the raster (#459) — was a leading-digit clip that rendered a plausible wrong number - ✅ 115 functions (#476 — SEARCH, N, HYPERLINK; #511 — IFNA, NA, ISNA, completing the error-guard family); the error guards now see an error cached inside a formula cell (#512 —
ISERRORover a recalculated formula cell answered FALSE); hidden rows/cols inview(#474); streaming numFmt parity (#475); VLOOKUP/HLOOKUP date keys (#488)
New in 0.19.1 (2026-08-04) — field hardening, waves 23 + 23b (#478/#487):
- ✅ Circular-book data-table seeding (#453) — the seeder fixpoints cycle members per axis combination under the book's CalcPr instead of pinning them at loaded caches (grids no longer seed silently FLAT);
seedDataTablesReport()+ IterativeCalc overloads - ✅
RecalcResult.converged/iterationsUsed(#454) — maxIter exhaustion distinguishable from convergence; CLI recalc honors declaredcalcPr iterate="1"(#461) - ✅ Reprint integrity (#455, #484) — structural edits preserve associativity parentheses and serialize Excel's file form; scripting
<f>leading-=canonicalized +formula-leading-equalslint (#456) - ✅ Evaluator wrong-value classes dead (#466, #467) — text-operand criteria (
"<>x",">m", non-blank"<>") evaluate with Excel semantics; MATCH/XLOOKUP comparator is total (blanks match nothing, DateTime-by-serial, cell-ref lookup values dereference) - ✅ Write-integrity guards (#470, #471, #472, #473) —
copy(sheets=…)reductions honored under preservation; fresh-write style plane re-registered (numFmt by code, dxfs carried); structural edits refuse past-bounds shifts and rewrite general defined names - ✅ Native image opens name-bloated books (#457) — JAXP limits lifted (62k–110k definedName banker files); lint accepts
xlPathMissingexternal links (#458)
New in 0.19.0 (2026-08-03):
- ✅ Column/row default styles (#445) —
<col style=>/<row s= customFormat="1">emit on both writer backends (StyleIndex-remapped like cell styleIds) AND parse back, so read→modify→write keeps source column styles;Sheet.withColumnStyle/withRowStyleauthor the sheet-wide-body-font-without-Normal mechanism - ✅ Sheet view modes (#446) —
SheetView.view(normal/pageBreakPreview/pageLayout) +zoomScaleNormal/zoomScaleSheetLayoutView/topLeftCell, set-or-remove with foreign values riding preservation - ✅ Excel-canonical XML forms (#448) — integral
sz, 17-sig-digit plain tints (tint="0"omitted), bare gray125, derivedoutlineLevelRow/Colsummary attrs; theme-index swap fixed — SAX path + comments wrote Dark2 as Light2 viaslot.ordinal - ✅ DateTime arithmetic (#449) —
=end-startday counts,=date+30offsets, and MIN/MAX/COUNT/SUM over date columns evaluate viadateTimeToExcelSerial(the writer's conversion); result is a serial Number, booleans stay skipped in aggregates - ✅ Data-table lints +
xl recalc --tables(#442) —data-table-torn(5 tear classes incl. del-flagged records) +data-table-unseeded(autoNoTable doctrine), DOM/SAX finding-identical, O(1) streaming;recalc --tablesseeds after recalculation, default pinned-cache path byte-identical - ✅ ca/aca + del1/del2 fidelity (#435) — plain-formula calc flags on
FormulaKind.Normal(aca, ca)survive every path (source-breaking:FormulaKind.Normal()); input-deleting structural edits keep the record del-flagged with caches intact
New in 0.18.0 (2026-07-29):
- ✅ Two-variable Data Table authoring (#419) —
sheet.dataTable(range, rowInput, colInput)authors native<f t="dataTable">records with autoNoTable-safe cache seeding (design-panel-verified against Excel fixtures); the house sensitivity engine is authorable - ✅ AutoFilter + outline grouping CLI (#432, #421) —
xl autofilter <range>/--clear,group-rows/group-cols/ungroup-*+ 5 batch twins (32 ops); authored filters shift under structural edits - ✅ Underline enum (#423, breaking w/ deprecated bridge) — singleAccounting/doubleAccounting round-trip typed; configurable Normal font (#425) —
WorkbookMetadata.defaultFonton both backends - ✅ CELL() (#424) — filename/address/row/col arms, volatile (109 functions); name→name chains in range slots (#411)
- ✅ Pie per-slice colors (#418) —
--series-colors→ typedSeries.pointFills→<c:dPt>fills
New in 0.17.0 (2026-07-29):
- ✅ numFmt parity everywhere (#408, #410) — StylePatcher delegates to NumFmt.builtInId (streamed Decimal/Percent/Currency ids 2/9/7 match in-memory); NumFmtFormatter built-in arms render via FormatCodeParser (PercentDecimal shows Excel's 15.60%)
- ✅ Batch values[] op-level format (#416) + usage errors name flags, lint positional file (#422)
- ✅ Sheet lifecycle bookkeeping (#434, #417) — localSheetId remap on remove/insert/reorder; removal-orphan chart/drawing part pruning (shared parts survive)
- ✅ Comment round-trip fidelity (#433) — no duplicate author prefix; run colors survive via raw rPr preservation
- ✅ sheetFormatPr write path (#426) + Sheet.named dynamic factory (#420)
New in 0.16.0 (2026-07-29):
- ✅ Array/data-table formula records survive rewrites (#430) —
<f t="array" ref>and<f t="dataTable" ref dt2D dtr r1 r2 del1 del2 ca/>are modeled per cell (FormulaKindonCellValue.Formula) and re-emit byte-exactly through dirty sheet regeneration on every writer (DOM, SAX, streaming); two-variable Data Tables no longer bake to static grids onput. DataTable caches are pinned (recalc/copy/eval never parse the derivedTABLE(...)display text); StructuralEditor shifts record payloads, degrades interior-tearing edits to cached constants, and setsdel1/del2when a delete removes an input cell (#435); plain-formulaca/acaride onFormulaKind.Normal(#435); CLI shows{=TABLE(A1,A2)}braces + JSONformulaKind, andputfrejects top-levelTABLE((authoring lands with #419); newformula-records.xlsxcorpus fixture rides every round-trip/parity law - ✅ Structural edits stop poisoning files (#427/#428/#429) — equals-free
<f>re-print with caches invalidated then re-baked by a global recalc before write (#352 contract); range shifts clamp at row 1,048,576/col XFD (full-axisA:A/1:1shapes preserved); Excel-authored dataValidations, print areas, tables, and preserved autoFilter shift with the edit via theSqrefShiftengine - ✅ Positional
putsmart detection (#431) — currency/percent/ISO-date/number/boolean detection at batch parity incl.--stream(with style-preserving detected formats);--no-detectopt-out; explicitformat:"date"on unparseable input errors instead of writing text dressed as a date - ✅ Bytes-read preservation parity (#412) —
SourceContent.OnDisk | InMemory;write(read(path)) ≡ write(readFromBytes(bytes))law-tested byte-identically (breaking:SourceContext.sourcePath: Path→content: SourceContent;fromFileunchanged) - ✅
xl lintextensions (#413) — chartsheet/dialogsheet order tables, externalLink r:id chain,[Content_Types]registration, O(1) SAX mode, andref-out-of-bounds(flags over-max ranges Excel refuses; previously reported clean) - ✅ Suite flake removed (#414) — the wall-clock single-pass race is now structural (100/100 under saturated load)
New in 0.15.0 (2026-07-17):
- ✅ numFmt round-trip fidelity (#404) —
<numFmts>declarations survive read→write verbatim (built-in-equal codes like"0.00%"no longer degrade toGeneral); totalNumFmt.formatCodeinverse; whole-code"General"renders correctly (incl.TEXT(n,"General")) - ✅ Range-date serial coercion (#405) — XIRR/XNPV date ranges and NETWORKDAYS/WORKDAY holidays accept raw serial Numbers; post-round-trip recalc no longer poisons returns blocks
- ✅ Variadic blank-arg parity (#395) + decoder coercion edges (#396) — direct blank refs ignored by COUNT/COUNTA/AVERAGE (
=AVERAGE(blank,blank)→#DIV/0!);decodeAsInt/decodeAsDatemirror ScalarCoercion (Empty/Bool/cached-formula arms) - ✅ Names + sheet-qualified refs in range-typed slots (#394) —
=VLOOKUP(x, named_table, 2), ad-hoc=XIRR(S!A1:B1,…), sheet-scoped names (=Model!case),./\in names; newRangeLocation.Name/TExpr.SheetNameRef; latent cross-sheet wrong-sheet bug fixed for INDEX/MATCH/XLOOKUP/XIRR/NPV - ✅ Chart series styling (#407) — every series emits type-appropriate
<c:spPr>(bar fills, line strokes, pie per-slicedPt) cyclingDefaultTheme.accents;Series.fill,chart add --series-colors,chartbatch op (27 ops),<c:tx>always emitted — LibreOffice renders chart output out of the box - ✅
xl lint(#397) — raw-zip validation of CT child order + r:id resolution (exit 0/1/2,--format json); preserved CT_Workbook children re-emit in schema position; hyperlink#normalization (#406)
New in 0.14.0 (2026-07-16):
- ✅ Excel error values as first-class results (#344) —
=1/0evaluates to#DIV/0!(IFERROR/ISERROR-catchable, cached ast="e"cells) instead of failing the formula; aggregates/logical folds/comparisons propagate per Excel policy (COUNT-family still skips); op-level#NUM!/#DIV/0!/#N/Acodes;#N/Adimension padding;"TRUE"/"FALSE"text coerces in conditions; host failures stay loud — the boundary is law-tested (RecalcResult.excelErrorsvserrors); design recorddocs/design/error-propagation.md - ✅ Full calcPr authoring (#400) —
calcMode/fullCalcOnLoad/calcIdjoin the iterate triple; the TJC house<calcPr>authors byte-exactly; last tjc-modeling zip patch retired - ✅ xl-agent robustness (#344) — bounded
pause_turnauto-resume with preserved container id, errored tasks counted in summaries, skill traces never overwritten by the engine fallback
New in 0.13.0 (2026-07-16):
- ✅ Defined-name resolution (#384) —
=IF(case=2,…),=entry_mult*ltm_ebitdaevaluate; sheet-scoped shadowing, name-chains with cycle guard, dependency-graph edges; was 926/1,571 probe rejections on a real LBO - ✅ Opt-in iterative recalculation (#373) —
recalculate(IterativeCalc(maxIter, maxChange))Jacobi-fixpoints declared cycles (circular debt schedules verify); calcPr authoring for scratch workbooks - ✅ Coercion parity (#385) + MROUND (#386) — serial Numbers in date positions, blanks as 0 in scalar numeric contexts (aggregates still skip); 108 registry functions
- ✅ Parser parity (#355, #374) — percent postfix operator with Excel precedence and byte-identical round-trip; leading unary plus preserved through print
- ✅ Appearance round-trip (#372, #382, #358) — freeze panes read into the model (incl. scrolled panes),
tabSelected,Sheet.tabColor; CLI:sheet-view,tab-color(theme syntax),page-setup,header-footer - ✅ Authoring API (#375, #379, #380, #361, #360) — data-validation dropdowns (typed + preserved), Patch comment/CF cases,
Align.textRotation,Column.parseruntime handles,Excel.writeRecalculated - ✅ CLI tooling (#356, #357, #324, #359) — batch
putfformat field, JSONformulafield,cf add/cf list, actionable rasterizer diagnostics with native-image awareness
New in 0.12.7 (2026-07-16):
- ✅ Property-only rows survive scratch writes (#381) — rows with only
RowProperties(no cells) emit<row>on the default backend; backend parity pinned by tests - ✅ Cached DateTime on formula cells (#378) —
=TODAY()/=EDATE()/=EOMONTH()caches serialize as Excel serials (t="n"+<v>) on all three writer backends; streaming writer gained cached-Textt="str" - ✅ Schema-valid comment rich runs (#383) —
<rFont>per CT_RPrElt (was styles-shape<name>); openpyxl opens XL-authored comments; reader accepts both dialects - ✅ Identity-named modified-sheet parts (#327) — no duplicate-zip-entry failure on Excel-reordered sources; physical writes, sheet rels, and workbook rels derive from one path
- ✅ Comment-removal package pruning (#328) — dropping a sheet's last comment prunes CT overrides, sheet rels, and legacyDrawing (resolved-path-aware, openpyxl dialect included)
- ✅ Default theme part for scratch theme colors (#387) —
Color.Themestyles/dxfs shipxl/theme/theme1.xml+ override + rel; RGB-only workbooks unaffected - ✅ Total recalculate() under financial divergence (#388) — IRR/XIRR/XNPV/NPV + TVM overflow class returns per-cell errors; workbook-evaluator NonFatal backstop guarantees totality
New in 0.12.6 (2026-07-15):
- ✅ External-workbook references (#353) —
[2]Book1!A1forms parse (dedicated AST node, exact printer round-trip, anchor-aware shifting), contribute no dependency edges, andrecalculate()pins their Excel-written caches verbatim while dependents compute from them; uncached external cells report a clear per-cell error instead ofUnexpectedChar([ - ✅ Batch recalculation +
xl recalc(#352) — cell-mutating batches end with one global recalculation (cached<v>forputfcells, errors surfaced in the summary), plus a latent fix: recalculated caches survive surgical writes of disk-read workbooks - ✅ Visible truncation (#351) —
view/searchreportshowing N of M rowswhen--limitclips (markdown trailer, stderr for csv,truncated/totalRowsin json);--limit 0= unlimited - ✅ DOCTYPE-tolerant core-part reads (#350) — benign
<!DOCTYPE>prologs stripped by a conservative scanner (parser stays locked down); parse errors carry line/column - ✅ Native-image diagnostics + arm64 (#349, #354) — Xerces message bundles registered (real parse errors on the shipped binary, release smoke test per platform); linux-arm64 native binaries join the release matrix
New in 0.12.5 (2026-07-13):
- ✅ Evaluator memoization + workbook-level recalculation (#346) — recursive uncached-reference evaluation is memoized per pass, and
recalculate()runs one topological order over the qualified (sheet!cell) graph: the recursive debt-schedule shape drops from hours (exponential in dependency paths) to milliseconds; cross-sheet cycles now report circular/blocked like same-sheet ones; cross-sheet aggregates over uncached formula cells compute correctly regardless of sheet order
New in 0.12.4 (2026-07-09):
- ✅ Elementwise error carriage (#337) — array comparisons/arithmetic carry
#DIV/0!/#REF!/#VALUE!per element (Left only on dimension mismatch, property-tested); aggregates fail loudly on carried errors (IFERROR-catchable) - ✅ Array-IF branch errors (#339) — unused-branch failures demote to discarded error elements per CSE
- ✅ Benchmark truncation diagnostics (#340, internal) — stop_reason capture, 32K output cap +
--max-tokens; divergence backlog in #344
New in 0.12.3 (2026-07-09):
- ✅ Excel comparison semantics (#335) — ordered comparisons follow Excel's total order (case-insensitive lexicographic text,
number < text < logical, dates by serial, empty coercion) in scalar and array paths - ✅ Array-aware IF + crash-free logical functions (#333, #338) — CSE broadcast for IF/IFS, AND/OR aggregate over arrays, NOT spills elementwise; the
MIN(IF(...))ClassCastException family is gone - ✅ Benchmark failure diagnostics (#334, internal) — per-case errors in reports, partial traces survive mid-run failures
New in 0.12.0–0.12.2 (2026-06-11):
- ✅ Typed charts (0.12.0, #222) — bar/line/pie via
Chart.bar/line/pie+Sheet.addChart(CLIchart add); typed-parse-or-Preserved hybrid read, structural-edit + rename reference tracking (see LIMITATIONS §12) - ✅ Embedded pictures (0.12.0, #221) —
Sheet.addImage/pictures/removeDrawing, three anchor forms, 7-format sniffing, sha-deduped media; non-picture drawings preserved (see LIMITATIONS §13) - ✅ Conditional formatting (0.12.1, #136) —
Sheet.conditionalFormatwith cellIs/expression/colorScale/dataBar/top10/text rules +Dxfdifferential formats; joins the generative round-trip law (library API; see LIMITATIONS §10) - ✅ LibreOffice interop (0.12.2) — editing LibreOffice-produced workbooks no longer corrupts them;
[Content_Types]/SST writer accounting hardened (#320–#323) - ✅ Codec
putpaths ~2.4x faster (0.12.1, #297)
New in 0.11.0 "Scripting" (2026-06-10):
- ✅ Scripting prelude
com.tjclp.xl.scripting.{*, given}— ONE import for scripts: core API + DSL + compile-time literals + formula evaluation + syncExcel+ streamingExcelIO+ smart detection (String.toFormatted) +.unsafeboundary - ✅ Range fill:
range := valueputs the value in every cell (Excel Ctrl+Enter semantics; previously a silent no-op) - ✅ Total ARef navigation:
down/up/right/left(default 1) for Either-free loops - ✅
Workbook.upsert(name, f)— total update-or-create counterpart ofupdate - ✅
Workbook.recalculate(clock)— total whole-workbook recalculation returningRecalcResult(cached workbook + per-sheet values + per-cellCellEvalErrors; cycles isolated, acyclic remainder still evaluates) - ✅
FormattedParsers.detect— total smart value detection (currency/accounting/percent/ISO date/number/boolean/text), promoted from CLI-internal code - ✅ Per-side borders + outlines:
CellStyle.borderTop/borderBottom/borderLeft/borderRightandrange.outlined(style[, color])via the newPatch.MergeBorder - ✅ Alignment indent:
CellStyle.indent(n) - ✅ Sheet view settings:
SheetView(showGridLines, zoomScale)onSheet.viewSettings(SVG renderer respects gridline suppression) - ✅ Print setup extensions:
PageSetupgainsheaderFooter,margins,printArea,repeatRows(sheet-scoped_xlnmdefined names; even/first headers + fitToPage tracked in #266) - ✅ xl-scripting skill (
plugin/skills/xl-scripting/) — SKILL.md + API reference + runnable recipes, compile-verified on CI - ✅ Anti-rot CI: examples job (
scripts/test-examples.sh) + skill-verify workflow (scripts/verify-skill-snippets.sh)
Core Features:
- ✅ Type-safe addressing (Column, Row, ARef with 64-bit packing)
- ✅ Compile-time validated literals:
ref"A1"andref"A1:B10" - ✅ Immutable domain model (Cell, Sheet, Workbook)
- ✅ Patch Monoid for declarative updates
- ✅ Complete style system (Font, Fill, Border, Color, NumFmt, Align)
- ✅ StylePatch Monoid for style composition
- ✅ StyleRegistry for per-sheet style management
- ✅ End-to-end XLSX read/write (creates real Excel files)
- ✅ Surgical modification (read → modify → write preserves unknown parts: charts, images, drawings, comments, and inline worksheet elements — dataValidations, sheetProtection, autoFilter — preserved through edits as of 0.10.0 / C1)
- ✅ Embedded images (#221):
Sheet.addImage(image, at | range | anchor)with natural-size sniffing (png/jpeg/gif/bmp),Sheet.pictures,removeDrawing; one-cell/two-cell/absolute anchors; shapes ride through asDrawing.Preserved; media sha-256 dedup; in-memory read/write only (see LIMITATIONS §13) - ✅ Typed charts (#222): bar (clustered/stacked/percent-stacked, column/horizontal), line, pie —
Chart/Series/DataRefmodel,Sheet.addChart/charts, CLIchart add+add-image; typed-parse-or-Preserved hybrid read (out-of-fence charts stay byte-preserved); structural edits + rename track chart references; value caches resolved from stored cells on write (see LIMITATIONS §12) - ✅ Structural editing (0.10.0): insert/delete rows & columns shift cells, merges, row/col properties, freeze panes, and rewrite all affected formulas (cross-sheet) with
#REF!generation - ✅ Named ranges & hyperlinks authoring (0.10.0):
DefinedNameandCell.hyperlinkare now serialized (previously read-only) - ✅ Hybrid write optimization (11x speedup for unmodified workbooks, 2-5x for partial modifications)
- ✅ Shared Strings Table (SST) deduplication
- ✅ Styles.xml with component deduplication
- ✅ Multi-sheet workbooks
- ✅ All cell types: Text, Number, Bool, Formula, Error, DateTime
- ✅ RichText support (multiple formats within one cell)
- ✅ DateTime serialization (Excel serial number conversion)
- ✅ Excel Tables (structured data ranges with headers, AutoFilter, and styling)
- ✅ True streaming I/O (constant memory, 100k+ rows)
Ergonomics & Type Safety:
- ✅ Given conversions:
sheet.put(ref"A1", "Hello")(no wrapper needed) - ✅ Batch put via varargs
Sheet.put(ref -> value, ...) - ✅ Formatted literals:
money"$1,234.56",percent"45.5%",date"2025-11-10" - ✅ String interpolation:
ref"$sheet!$cell",money"$$${amount}"with runtime validation - ✅ Compile-time optimization for literal interpolations (zero runtime overhead)
- ✅ CellCodec[A] for 9 primitive types (String, Int, Long, Double, BigDecimal, Boolean, LocalDate, LocalDateTime, RichText)
- ✅ Batch
Sheet.putwith auto-inferred formatting (formerputMixedAPI) - ✅
readTyped[A]for type-safe cell reading - ✅ Optics module (Lens, Optional, focus DSL)
- ✅ RichText DSL:
"Bold".bold.red + " normal " + "Italic".italic.blue - ✅ HTML export:
sheet.toHtml(ref"A1:B10") - ✅ Formula Parsing (WI-07 complete): TExpr GADT, FormulaParser, FormulaPrinter with round-trip verification and scientific notation
- ✅ Formula Evaluation (WI-08 complete): Pure functional evaluator with total error handling, short-circuit semantics, and Excel-compatible behavior
- ✅ Function Library: 108 built-in functions (aggregate, conditional, logical, text, date, financial, lookup, math, statistical, dynamic arrays), extensible type class parser, evaluation API. 0.10.0 added IFS, SWITCH, CHOOSE, LARGE, SMALL, RANK, PERCENTILE, QUARTILE, HLOOKUP, MAXIFS, MINIFS, OFFSET, and the spill functions SEQUENCE/SORT/UNIQUE/FILTER (#76, #120, #122); 0.11.2 added INDIRECT (GH-274), RAND/RANDBETWEEN via the seeded Rng capability (GH-115), and LET lexical bindings (GH-193) (dynamic text-to-reference resolution with deferred-bucket recalculation).
- ✅ Dependency Graph (WI-09d complete): Circular reference detection (Tarjan's SCC), topological sort (Kahn's algorithm), safe evaluation with cycle detection
- ✅ Cross-Sheet Formula References (TJC-351): Single cell refs (
=Sales!A1), range refs (=SUM(Sales!A1:A10)), arithmetic with cross-sheet refs, workbook-level cycle detection (DependencyGraph.fromWorkbook)
Performance (JMH Benchmarked - WI-15; figures captured on an earlier release, not re-validated for 0.12.x — treat as indicative):
- ✅ Streaming reads: 35% faster than POI for small files (0.887ms vs 1.357ms @ 1k rows)
- ✅ Streaming reads: Competitive with POI for large files (8.408ms vs 7.773ms @ 10k rows - within 8%)
- ✅ In-memory reads: 26% faster than POI for small files (1.225ms vs 1.650ms @ 1k rows)
- ✅ Inline hot paths (SAX parser: 3.8x speedup vs fs2-data-xml)
- ✅ Zero-overhead opaque types
- ✅ Macros compile away (no runtime parsing)
⚠️ Writes: POI 49% faster (future optimization work - Phase 3)
Streaming API:
- ✅ Excel[F[_]] algebra trait
- ✅ ExcelIO[IO] interpreter
- ✅
readStream/readSheetStream/readStreamByIndex– constant‑memory streaming read (fs2.io.readInputStream + fs2‑data‑xml) - ✅
writeStream/writeStreamsSeq– constant‑memory streaming write (fs2‑data‑xml) - ✅
writeWorkbookStream– lower-allocation SAX/StAX write for in-memory workbooks; preserves merges, comments, tables, row/column properties, and freeze panes - ✅
writeFast– SAX/StAX streaming write (opt-in viaExcelIO.writeFast()orWriterConfig(backend = XmlBackend.SaxStax)) - ✅ Benchmark: 100k rows in ~1.8s read (~10MB constant memory) / ~1.1s write (~10MB constant memory)
Output Configuration (P6.7 Complete):
- ✅ WriterConfig with compression and prettyPrint options
- ✅ Compression.Deflated default (5-10x smaller files)
- ✅ WriterConfig.debug for debugging (STORED + prettyPrint)
- ✅ Backward compatible API (writeWith for custom config)
- ✅ 4 compression tests verify behavior
Infrastructure:
- ✅ Mill build system
- ✅ Scalafmt 3.10.1 integration
- ✅ GitHub Actions CI pipeline
- ✅ Comprehensive documentation (README.md, CLAUDE.md)
6,952 test cases (verified via ./mill __.test, 2026-09-08, the 0.22.0 release): all passed; the style-performance comparison stays ignored, and four subprocess smokes (openpyxl, unwritable-directory) skip where the sandbox lacks the tool or runs as root.
| Module | Tests | Covers |
|---|---|---|
| xl-evaluator | 2447 | parser, evaluator, 116-function library, dependency graph, cross-sheet formulas, recalculation, structural editing, Excel comparison total order, array CSE semantics |
| xl-core | 1560 | addressing laws, Patch/StylePatch monoids, codecs, optics, RichText, interpolation, render (HTML/SVG), styles DSL, charts, drawings, conditional formatting |
| xl-ooxml | 1152 | round-trips (cells, styles, tables, comments, hyperlinks, charts, drawings, conditional formatting), compression, security (XXE, ZIP bomb), preservation |
| xl-cli | 1420 | command parsing, batch ops, view/eval/export, streaming mode, memory guard (GH-636) |
| xl-cats-effect | 175 | streaming I/O, O(1) memory verification, SAX/StAX write, spill-directory routing |
| xl-agent | 146 | benchmark engine, skill abstraction, failure-path diagnostics, release-asset resolution |
| xl (prelude) | 52 | external-consumer probes (xl/test/src/xlprelude/) |
| xl-testkit | 0 | placeholder (no sources yet) |
See reference/testing-guide.md for suite structure and testing patterns.
Formula System (WI-07, WI-08, WI-09a/b/c/d - Production Ready):
- ✅ Parsing (WI-07): Typed AST (TExpr GADT), FormulaParser, FormulaPrinter, round-trip verification, 57 tests
- ✅ Evaluation (WI-08): Pure functional evaluator, total error handling, short-circuit semantics, 58 tests
- ✅ Function Library (WI-09a-h + TJC-1055 complete): 108 built-in functions, extensible type class parser, evaluation API
- Aggregate (12): SUM, COUNT, COUNTA, COUNTBLANK, AVERAGE, MEDIAN, MIN, MAX, STDEV, STDEVP, VAR, VARP
- Statistical (5): LARGE, SMALL, RANK, PERCENTILE, QUARTILE
- Conditional (9): SUMIF, COUNTIF, SUMIFS, COUNTIFS, AVERAGEIF, AVERAGEIFS, MAXIFS, MINIFS, SUMPRODUCT
- Logical / Selection (13): IF, IFS, IFERROR, SWITCH, CHOOSE, AND, OR, NOT, ISNUMBER, ISTEXT, ISBLANK, ISERR, ISERROR
- Text (12): CONCATENATE, LEFT, RIGHT, MID, LEN, UPPER, LOWER, TRIM, FIND, SUBSTITUTE, TEXT, VALUE
- Date (12): TODAY, NOW, DATE, YEAR, MONTH, DAY, EOMONTH, EDATE, DATEDIF, NETWORKDAYS, WORKDAY, YEARFRAC
- Math (16): ABS, ROUND, ROUNDUP, ROUNDDOWN, INT, MOD, POWER, SQRT, LOG, LN, EXP, FLOOR, CEILING, TRUNC, SIGN, PI
- Financial (9): NPV, IRR, XNPV, XIRR, PMT, FV, PV, RATE, NPER
- Lookup / Reference (12): VLOOKUP, HLOOKUP, XLOOKUP, INDEX, MATCH, OFFSET, INDIRECT, ROW, COLUMN, ROWS, COLUMNS, ADDRESS
- Dynamic Arrays (5): TRANSPOSE, SEQUENCE, SORT, UNIQUE, FILTER
- Random (2): RAND, RANDBETWEEN
- FunctionSpec registry: macro-collected specs with extensible registry
- APIs: sheet.evaluateFormula(), sheet.evaluateCell(), sheet.evaluateAllFormulas()
- Clock trait for pure date/time functions (deterministic testing)
- ✅ Dependency Graph (WI-09d): Circular reference detection + topological sort, 52 tests
- Tarjan's SCC algorithm: O(V+E) cycle detection with early exit
- Kahn's algorithm: O(V+E) topological sort for correct evaluation order
- Precedent/dependent queries: O(1) lookups via adjacency lists
- Safe evaluation: sheet.evaluateWithDependencyCheck() (production-ready)
- Performance: Handles 10k formula cells in <10ms
⚠️ Merged cells are supported by the in-memory OOXML path andwriteWorkbookStream. Pure row-stream generation (writeStream/writeStreamsSeq) has no merge API.- ✅ Hyperlinks serialized as of 0.10.0 (
Cell.hyperlink→<hyperlinks>+ worksheet relationships; populated on read). - ✅ Column/row properties (width, height, hidden, outlineLevel, collapsed) are fully serialized via DirectSaxEmitter.
Minor Limitations:
- ✅ Theme colors resolved via
Color.toResolvedArgb(theme)/toResolvedHex(theme)(slot lookup + tint application throughThemePalette.resolve) ⚠️ StyleRegistry requires explicit initialization per sheet (design choice for purity)
Missing Parts (not critical for MVP):
- ❌ docProps/core.xml, docProps/app.xml (metadata)
⚠️ xl/theme/theme1.xml (theme palette) — preserved from source on round-trip, not generated for new workbooks- ❌ xl/calcChain.xml (formula calculation order)
- ✅ Worksheet relationships (
_rels/sheetN.xml.rels) — written when a sheet has comments, tables, or hyperlinks ⚠️ Print settings, page setup — odd + even/first header/footer, margins, print area, repeat rows (#259, #266), andfitToPagetri-state (#284); shipped across 0.11.0–0.12.1- ✅ Conditional formatting (0.12.1, #136): typed
Sheet.conditionalFormatrules (cellIs/expression/colorScale/dataBar/top10/text) +Dxfdifferential formats; library API (no CLI yet) — see LIMITATIONS §10 - ❌ Data validation (preserved through edits, but no authoring API yet)
- ✅ Named ranges (authoring shipped in 0.10.0:
DefinedNameserialization + CLIname add/rm)
Row-stream write path (✅ Working):
- ✅ True constant-memory row streaming with
writeStream/writeStreamsSeq - ✅ O(1) memory regardless of file size
⚠️ No SST support (inline strings only - larger files)⚠️ Minimal styles (default only - no rich formatting)⚠️ No row-stream API for workbook metadata such as merged ranges, comments, tables, and freeze panes
In-memory workbook SAX/StAX write path (✅ Working):
- ✅
writeWorkbookStreamwrites an already-materializedWorkbookthrough the SAX/StAX backend - ✅ Preserves full workbook metadata handled by the OOXML writer, including merges, comments, tables, row/column properties, and freeze panes
⚠️ Not a row-input streaming API; theWorkbookis already in memory
Read Path (✅ P6.6 Complete):
- ✅ True constant-memory streaming - uses
fs2.io.readInputStream - ✅ O(1) memory for worksheet data (unlimited rows supported)
- ✅ Streams worksheet XML incrementally (4KB chunks)
⚠️ SharedStrings Table (SST) materialized in memory (~10MB typical, scales with unique strings)- ✅ Large files (500k+ rows) process without OOM
- ✅ Memory tests verify O(1) behavior
Result:
- Both streaming read and write achieve constant memory for worksheet data ✅
- 500k rows: ~10-20MB memory (worksheet streaming + SST materialized)
- 1M+ rows supported without memory issues (unless >100k unique strings)
- Design tradeoff: SST materialization acceptable for most use cases (text typically <10MB)
Implemented:
- ✅ ZIP bomb detection
- ✅ XXE (XML External Entity) prevention
- ✅ Formula injection guards in in-memory and streaming writes
Remaining:
- ❌ XLSM macro preservation policy and tests (macros are never executed)
Implemented (continued):
- ✅ Configurable file size limits via CLI
--max-size <MB>(default 100MB;--max-size 0= unlimited)
Completed (P6, P7, P8, P31, WI-07/08/09, WI-10, WI-15, WI-17):
- ✅ P6: CellCodec primitives (9 types with auto-formatting)
- ✅ P7: String interpolation Phase 1 (runtime validation for all macros)
- ✅ P8: String interpolation Phase 2 (compile-time optimization)
- ✅ P31: Optics, RichText, HTML export, enhanced ergonomics
- ✅ Formula System (WI-07/08/09): Parser, evaluator, 108 functions, dependency graph, cycle detection
- ✅ Excel Tables (WI-10): Structured data with headers, AutoFilter, styling
- ✅ Benchmarks (WI-15): JMH performance suite (XL vs POI)
- ✅ SAX Write (WI-17): Fast SAX/StAX streaming write path
- ✅ Security Hardening (WI-30): ZIP bomb detection, XXE prevention, formula injection guards
Future (and recently shipped):
- ❌ P6b: Full case class codec derivation (Magnolia/Shapeless)
- ❌ P9: Advanced macros (path macro, style literal)
- ✅ P10: Drawings — embedded pictures (#221):
Sheet.addImage/pictures/removeDrawing, three anchor forms, 7-format classification, sha-deduped media, hybrid byte-preservation of non-picture drawings (shapes asDrawing.Preserved); shape authoring still future. In-memory only — see LIMITATIONS §13 - ✅ P11: Charts (0.12.0, #222): typed bar/line/pie via
Chart.bar/line/pie+Sheet.addChart(CLIchart add); out-of-fence/Excel-authored charts stay byte-preserved — see LIMITATIONS §12 - ❌ Pivot Tables (remaining part of P12)
For detailed roadmap and future plans, see plan/roadmap.md
Focus: Address PR review feedback
- Optimize style indexOf from O(n²) to O(1)
- Extract whitespace check utilities
- Add error path tests
- Full round-trip integration tests
Focus: Automatic case class mapping
- Derive RowCodec[A] for case classes
- Header-based column binding
- Type-safe bulk operations
Focus: Additional compile-time validation
pathmacro for file path validationstyleliteral for CellStyle DSLs- Enhanced diagnostics
Focus: Drawings, Charts, Tables, Security
- See plan/roadmap.md for detailed breakdown
xl/src/com/tjclp/xl/
└── scripting.scala ✅ One-import scripting prelude (com.tjclp.xl.scripting)
xl-core/src/com/tjclp/xl/
├── addressing/ ✅ Opaque types (Column, Row, ARef packing), CellRange, SheetName
├── cells/ ✅ Cell, CellValue, CellError, Comment
├── sheets/ ✅ Sheet, SheetView, PageSetup
├── workbooks/ ✅ Workbook, WorkbookMetadata, DefinedName
├── patch/ ✅ Patch Monoid (incl. MergeBorder)
├── styles/ ✅ CellStyle, Font, Fill, Border, Color, NumFmt, StylePatch, style DSL
├── codec/ ✅ CellCodec (9 primitive types), readTyped/readTypedOr/readTypedOpt
├── macros/ ✅ ref"", fx"", money"" … compile-time literals (lives in xl-core; no separate xl-macros module)
├── optics/ ✅ Lens, Optional, focus DSL
├── richtext/ ✅ TextRun, RichText, DSL extensions
├── formatted/ ✅ Formatted literals + FormattedParsers.detect
├── render/ ✅ HTML/SVG renderers (sheet.toHtml / toSvg)
└── dsl/ ✅ Ergonomic patch operators (:=, ++, outlined)
xl-ooxml/src/com/tjclp/xl/ooxml/
├── ContentTypes.scala ✅ [Content_Types].xml
├── Relationships.scala ✅ .rels files
├── Workbook.scala ✅ xl/workbook.xml
├── worksheet/ ✅ xl/worksheets/sheet#.xml (RichText, merges, hyperlinks, page setup)
├── SharedStrings.scala ✅ xl/sharedStrings.xml (SST with RichText)
├── Styles.scala ✅ xl/styles.xml
├── XlsxWriter.scala ✅ ZIP assembly + surgical modification
└── XlsxReader.scala ✅ ZIP parsing + security limits
xl-cats-effect/src/com/tjclp/xl/io/
├── Excel.scala ✅ Algebra trait
├── ExcelIO.scala ✅ Interpreter with true streaming
└── Sax/StAX + fs2 writers ✅ Event-based streaming read/write
xl-evaluator/✅ Complete (WI-07/08/09 - formula parsing, evaluation, 108 functions, dependency graph, structural editing, recalculation)xl-cli/✅ Complete (statelessxlCLI: 46 subcommands, 27 batch ops, rendering, streaming mode)xl-agent/✅ Complete (AI agent benchmark runner)xl-benchmarks/✅ Complete (WI-15 - JMH performance benchmarks)
xl-testkit/(law helpers, golden test framework) — still a placeholder
Drawings and charts shipped in 0.12.0 as
com.tjclp.xl.drawings/com.tjclp.xl.chartswithinxl-core, not as separate modules.
StreamingXmlWriter compilation- ✅ fs2-data-xml integration completeDateTime serialization- ✅ Excel serial number conversion implementedCell → CellStyle linkage- ✅ StyleRegistry provides sheet-level style managementComments- ✅ Full OOXML round-trip (xl/commentsN.xml + VML drawings), rich text support, 12+ testsMerged cells- ✅mergedRangesserialized via<mergeCells>(in-memory +writeWorkbookStreampaths)Column/row properties- ✅ width/height/hidden/outline serialized via DirectSaxEmitterHyperlinks- ✅ serialized with worksheet relationships (0.10.0)
- Theme resolution - Improve Theme color ARGB approximations (currently functional but not perfect)
⚠️ Captured on an earlier release (JDK 25, Apple Silicon); not re-validated for 0.12.x — treat as indicative. 0.12.1 made codecputpaths ~2.4x faster (#297), not reflected below.
XL vs Apache POI (Apple Silicon M-series, JDK 25):
| Rows | POI | XL | Result |
|---|---|---|---|
| 1,000 | 1.357 ± 0.076 ms | 0.887 ± 0.060 ms | ✨ XL 35% faster |
| 10,000 | 7.773 ± 0.590 ms | 8.408 ± 0.153 ms | Competitive (XL within 8%) |
| Rows | POI | XL | Result |
|---|---|---|---|
| 1,000 | 1.650 ± 0.055 ms | 1.225 ± 0.086 ms | ✨ XL 26% faster |
| 10,000 | 13.784 ± 0.377 ms | 14.115 ± 1.250 ms | Competitive (XL within 2%) |
| Rows | POI | XL | Result |
|---|---|---|---|
| 1,000 | 1.280 ± 0.041 ms | 1.906 ± 0.245 ms | POI 49% faster |
| 10,000 | 10.228 ± 0.417 ms | 15.248 ± 1.315 ms | POI 49% faster |
Key Findings:
- ✨ XL is fastest for small-medium files (< 5k rows): 35% faster streaming, 26% faster in-memory
- ✅ XL competitive on large files: Within 8% of POI on 10k row streaming reads
- 🔧 Write optimization: Future work (Phase 3) - POI currently 49% faster
- 💾 Constant memory: Streaming uses O(1) memory regardless of file size
- ⚡ SAX parser: 3.8x speedup vs previous fs2-data-xml implementation
Recommendation: Use ExcelIO.readStream() for production workloads (fastest for <5k rows, constant memory).
Memory characteristics:
- Write: ~10MB constant memory (O(1)) ✅
- Read: ~10MB constant memory (O(1)) ✅ (P6.6 fixed with fs2.io.readInputStream)
- Scalability: Can handle 1M+ rows without OOM ✅
Performance characteristics (validated with JMH):
- Streaming vs in-memory: Streaming 1.7x faster for large files (8.4ms streaming vs 14.1ms in-memory @ 10k rows)
- XL vs POI: XL is fastest for small files (35% faster @ 1k rows), competitive for large files (within 8% @ 10k rows)
| Operation | XL | Apache POI SXSSF | Improvement |
|---|---|---|---|
| Write 100k | 1.1s @ 10MB | ~5s @ 800MB | 4.5x faster, 80x less memory ✅ |
| Write 1M | ~11s @ 10MB | ~50s @ 800MB | 4.5x faster, constant memory ✅ |
| Read 100k | 1.8s @ 10MB | ~8s @ 1GB | 4.4x faster, 100x less memory ✅ |
| Read 500k | ~9s @ 10MB | OOM @ 1GB+ | Constant memory vs OOM ✅ |
Note: The SXSSF comparison figures above are approximate (POI columns marked ~ are estimates); the JMH tables earlier in this section are the measured numbers. True O(1) streaming is verified by memory tests.
Result for Writes: Exceeded goal of 3-5x throughput, achieved 80x memory improvement with constant memory.
# Quick start
./mill __.compile
./mill __.test
# Work on streaming
./mill xl-cats-effect.compile
./mill xl-cats-effect.test
# Format code
./mill __.reformat
# Create sample file (once fixed)
./mill xl-ooxml.test.runMain com.tjclp.xl.ooxml.Demo- Purity maintained - Core is 100% pure, zero side effects
- Laws verified - All Monoids tested with property-based tests
- Deterministic output - Same input = same bytes (stable diffs)
- Zero overhead - Opaque types, inline, compile-time macros
- Real files - Creates valid XLSX that Excel/LibreOffice opens
- Type safety - Opaque types prevent mixing units; codecs enforce type correctness
- Performance - ~35% faster than Apache POI on streaming reads (JMH validated), writes currently ~49% slower, 80x less memory