Public barrel for @causl/formula. Re-exports the formula grammar / AST types, parser entry point, domain model (Cell / Sheet / Workbook), engine adapter that wires formulas into the reactive graph, tagged result type for evaluation, and the static cycle-detection helpers used at registration time. The causl core deliberately does not ship a spreadsheet engine — it supports spreadsheet patterns on top of the core, and this package is that layer. The exported surface is intentionally narrow: only symbols re-exported from this module are part of the package's stability contract.

@causl/formula

Spreadsheet-style formula primitives layered on @causl/causl-wasm-ts. The core engine knows nothing about =A1+B1; this package translates AST → derived node.

pnpm add @causl/formula @causl/causl-wasm-ts
import { createCausl } from '@causl/causl-wasm-ts'
import {
cellId,
createFormulaAdapter,
parseFormula,
valueOr,
} from '@causl/formula'

const graph = createCausl()
const a1 = graph.input(cellId('wb', 'Sheet1', { col: 0, row: 0 }), 5)
const b1 = graph.input(cellId('wb', 'Sheet1', { col: 1, row: 0 }), 10)

const adapter = createFormulaAdapter(graph, {
workbook: 'wb',
sheet: 'Sheet1',
resolve: (ref) => {
if (ref.col === 0 && ref.row === 0) return a1
if (ref.col === 1 && ref.row === 0) return b1
return undefined
},
})

const c1 = adapter.registerFormula(
{ col: 2, row: 0 },
parseFormula('=A1+B1'),
)
console.log(graph.read(c1)) // { kind: 'value', value: 15 }
console.log(valueOr(graph.read(c1), 0)) // 15

Make impossible states impossible: rather than four optional fields where "has a value AND an error" is representable, evaluation resolves to a discriminated union. Divide-by-zero, non-numeric coercion, unresolved refs, unknown functions, AVG/MIN/MAX of empty sets, and upstream propagation all become {kind: 'error', error: {kind, message, ref?}} rather than throwing or silently zeroing. The tag is the gate; a compile-time check stands between a caller and the value.

const broken = adapter.registerFormula({col: 3, row: 0}, parseFormula('=A1/0'))
const r = graph.read(broken)
// { kind: 'error', error: { kind: 'div-by-zero', message: 'Division by zero' } }

SUM, AVG / AVERAGE, MIN, MAX. Operate on cell ranges (A1:A10) or scalar lists (SUM(A1, B2, 5)).

import {
emptyFormulaGraph,
addFormula,
detectCycle,
parseFormula,
} from '@causl/formula'

const g = emptyFormulaGraph()
addFormula(g, { col: 0, row: 0 }, parseFormula('=B1'))
addFormula(g, { col: 1, row: 0 }, parseFormula('=A1'))
detectCycle(g) // ['A1', 'B1', 'A1']

The engine also catches cycles at first-commit time (CycleError). This module is the pre-flight detector that lets the host application reject formulas before they hit the engine.

A 100-cell spreadsheet diamond demo lives under demo/ and exercises the same wiring this README describes against @causl/causl-wasm-ts and @causl/devtools. From the repo root:

pnpm --filter @causl/formula run demo

The demo builds the three packages and serves demo/index.html on a local port. It is the runnable companion to the SPEC §11 "the engine is its own observer" claim: every grid cell, the commit-log panel, and the "why did this update?" line subscribe through the same public engine surface tests use.

0.1.0 was a surface-breaking pre-1.0 minor (issue #1081, follow-up to the IR carve in #1075 / original #697); both PRs landed pre-0.9.0 and the surface below is the current one. Two changes:

  1. parseFormula consumes IR types directly. parseFormula and the cycle helpers now import Ast, BinOp, and CellRef from the internal IR module instead of ./grammar.js. The @causl/formula package barrel re-exports the same types, so the public type identity is unchanged for adopters that import from the package root. Adopters who imported types from the deep path @causl/formula/grammar will see a moved-source identity.

  2. FormulaHost interface + public evaluate(ast, host). The evaluator no longer takes a (resolve, get) closure pair. It now consumes a FormulaHost:

    interface FormulaHost {
    readNumber(cellId: string): number | FormulaError
    }
    evaluate(ast: Ast, host: FormulaHost): FormulaResult

    cellId is the A1 reference string. The host owns coercion and upstream propagation: a cell whose backing value is itself an errored FormulaResult must be returned as a propagated FormulaError, so the evaluator can forward host errors as-is. A Rust evaluator port that satisfies the same contract is the long- term plan; the wire-IR mirror in tools/engine-rs-core/src/formula_ir.rs (feature-gated future, landed via #1078 / #1080) is the seam, but the actual evaluator port is deferred to post-0.9.0 epic #1133 with the GO/NO-GO criteria documented in the epic body.

    createFormulaAdapter is unchanged: it now constructs a FormulaHost internally from the existing resolve + the engine's read hook. Direct adopters of the previous internal evaluate(ast, resolve, get) form (none in-tree) must migrate to the new entry.

Classes

FormulaParseError

Interfaces

Cell
CellRef
FormulaAdapter
FormulaAdapterOptions
FormulaGraph
FormulaHost
Sheet
Workbook

Type Aliases

Ast
BinOp
CellId
CellNode
CellValue
FormulaError
FormulaErrorKind
FormulaResult
SheetId
WorkbookId

Variables

FORMULA_IR_VERSION
VERSION

Functions

a1ToCellRef
addFormula
cellId
cellRefToA1
createFormulaAdapter
detectCycle
emptyCell
emptyFormulaGraph
emptySheet
emptyWorkbook
evaluate
expandRange
formulaCell
formulaError
formulaErrorResult
formulaOk
formulaRootCause
literalCell
parseFormula
refKey
staticReferences
valueOr