Skip to content

Univers42/formula-engine

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

86 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

formula-engine

A high-performance formula evaluator written in Rust, compiled to WebAssembly via wasm-pack. It powers the formula and rollup property types in notion-database-sys, but is designed as a standalone library you can embed in any TypeScript/JavaScript project.


Architecture

Formula string
      │
      ▼
  ┌────────┐     tokens     ┌──────────┐     AST      ┌────────────┐
  │ Lexer  │ ────────────▶  │  Parser  │ ──────────▶  │  Compiler  │
  └────────┘                └──────────┘              └────────────┘
                                                             │  Chunk (bytecode)
                                                             ▼
                                                      ┌────────────┐
                                          Context ──▶ │     VM     │ ──▶ Value
                                                      └────────────┘
Module File Responsibility
Lexer src/lexer.rs Tokenise formula strings; track line/column positions
Parser src/parser.rs Pratt (precedence climbing) parser → AST
Compiler src/compiler.rs Walk AST → bytecode Chunk with constant pool
VM src/vm.rs Register-based stack executor
Types src/types.rs Value enum, Expr AST nodes
Error src/error.rs FormulaError with source spans
Functions src/functions/ Built-in function registry (math, text, date, array, logic)
WASM entry src/lib.rs wasm-bindgen exports: compile / evaluate / validate
TS bridge bridge.ts TypeScript wrapper with init, cache and typed helpers

Building

Prerequisites

# Install wasm-pack (one-time)
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh

# Build the WASM package (outputs to pkg/)
cd src/lib/engine
wasm-pack build --target web --out-dir pkg

# Development build (faster, no optimisations)
wasm-pack build --target web --out-dir pkg --dev

The pkg/ directory contains:

  • formula_engine_bg.wasm — the compiled binary
  • formula_engine.js — JS glue generated by wasm-bindgen
  • formula_engine.d.ts — TypeScript declarations

Usage (TypeScript)

1 — Initialise once at app startup

import { initFormulaEngine } from './bridge';

await initFormulaEngine();

The module also auto-initialises on import (non-blocking), so await is optional for most use-cases.

2 — Quick one-shot evaluation

import { evalFormula } from './bridge';

const result = evalFormula('price * quantity', {
  price: 49.99,
  quantity: 3,
});
// → 149.97

3 — Compile once, evaluate many rows (fastest for tables)

import { compileFormula, evaluateHandle } from './bridge';

const compiled = compileFormula('upper(name) + " — " + category');
if (compiled?.ok && compiled.handle !== undefined) {
  for (const row of rows) {
    const label = evaluateHandle(compiled.handle, row);
  }
}

4 — Batch evaluate an entire table column

import { batchEvaluate } from './bridge';

const discountedPrices = batchEvaluate('round(price * (1 - discount), 2)', rows);

5 — Validate a formula before saving

import { validateFormula } from './bridge';

const { ok, errors, dependencies } = validateFormula('if(score > 100, "A", "B")');
// ok          → true
// errors      → []
// dependencies → ['score']

Built-in Functions

Math

Function Signature Description
abs abs(n) Absolute value
ceil ceil(n) Round up to integer
floor floor(n) Round down to integer
round round(n, decimals?) Round to N decimal places
sqrt sqrt(n) Square root
pow pow(base, exp) Exponentiation
log log(n, base?) Logarithm (base 10 default)
min min(a, b, …) Minimum of values or array
max max(a, b, …) Maximum of values or array
mod mod(n, divisor) Modulo
sign sign(n) -1, 0 or 1
clamp clamp(n, min, max) Clamp value to range
pi pi() π constant
e e() Euler's number

Text

Function Signature Description
upper upper(s) Uppercase
lower lower(s) Lowercase
len len(s) Character count (UTF-8 aware)
concat concat(a, b, …) Join strings
join join(sep, arr) Array → delimited string
trim trim(s) Strip leading/trailing whitespace
substr substr(s, start, len?) Substring
contains contains(s, sub) Case-sensitive substring check
startsWith startsWith(s, prefix) Prefix check
endsWith endsWith(s, suffix) Suffix check
replace replace(s, find, rep) First occurrence substitution
split split(s, sep) String → array

Date

Function Signature Description
now now() Current timestamp (ms)
today today() Midnight of current day
dateAdd dateAdd(d, n, unit) Add years/months/weeks/days/hours
dateDiff dateDiff(a, b, unit) Difference in given unit
dateFormat dateFormat(d, fmt) Format with YYYY, MM, DD, HH, mm tokens
year year(d) Extract year
month month(d) Extract month (1–12)
day day(d) Extract day of month
weekday weekday(d) Day of week (0=Sun … 6=Sat)
timestamp timestamp(d) Date → Unix ms

Array

Function Signature Description
count count(arr) Number of non-empty elements
sum sum(arr) Sum of numeric elements
avg avg(arr) Arithmetic mean
min min(arr) Minimum element
max max(arr) Maximum element
unique unique(arr) Deduplicated array
flatten flatten(arr) Recursively flatten nested arrays
first first(arr) First element or empty
last last(arr) Last element or empty

Logic

Function Signature Description
if if(cond, then, else) Ternary
ifs ifs(c1, v1, c2, v2, …) Multi-branch ternary
switch switch(val, case1, res1, …, default) Pattern switch
and and(a, b, …) Logical AND (callable form)
or or(a, b, …) Logical OR (callable form)
not not(a) Logical NOT (callable form)

Operators

Category Operators
Arithmetic + - * / % **
Comparison == != < > <= >=
Logical and or not
String + (concatenation)
Property access prop["field name"]

Formula Examples

The 10 examples below are ordered from beginner (1) to advanced (10).


1 — Basic arithmetic

price * quantity

Context: { price: 29.99, quantity: 4 } Result: 119.96

Simple multiplication. The engine coerces property values to numbers automatically.


2 — Rounding a calculated value

round(price * 1.2, 2)

Context: { price: 49.99 } Result: 59.99

Wraps an arithmetic expression in round() to control decimal places.


3 — String formatting

upper(first_name) + " " + upper(last_name)

Context: { first_name: "Jane", last_name: "doe" } Result: "JANE DOE"

Demonstrates string concatenation with + and the upper() text function.


4 — Ternary status badge

if(stock > 0, "In Stock", "Out of Stock")

Context: { stock: 12 } Result: "In Stock"

The most common use of if(). Condition is a comparison expression, branches are string literals.


5 — Date formatting

dateFormat(due_date, "YYYY-MM-DD")

Context: { due_date: 1743292800000 } Result: "2025-03-30"

Converts a Unix-ms timestamp stored in a date property into a readable string.


6 — Days until deadline with urgency label

if(
  dateDiff(today(), due_date, "day") < 0,
  "Overdue by " + abs(dateDiff(today(), due_date, "day")) + " days",
  if(
    dateDiff(today(), due_date, "day") == 0,
    "Due today",
    dateDiff(today(), due_date, "day") + " days left"
  )
)

Context: { due_date: <tomorrow's timestamp> } Result: "1 days left"

Nested if() with dateDiff and abs to render an urgency label from a raw date.


7 — Multi-tier discount label

ifs(
  discount >= 0.3,  "🔥 Big Deal — " + round(discount * 100, 0) + "% off",
  discount >= 0.15, "Sale — "        + round(discount * 100, 0) + "% off",
  discount >  0,    round(discount * 100, 0) + "% off",
  "Full price"
)

Context: { discount: 0.35 } Result: "🔥 Big Deal — 35% off"

Uses ifs() as a multi-branch classifier. The last argument without a paired condition is the default fallback.


8 — Normalised progress score

clamp(
  round((completed / if(total == 0, 1, total)) * 100, 1),
  0,
  100
)

Context: { completed: 7, total: 8 } Result: 87.5

Guards against division-by-zero with if(total == 0, 1, total), computes a percentage, rounds, then clamps to a 0–100 band.


9 — Full name slug for URL generation

lower(
  replace(
    replace(
      trim(first_name) + "-" + trim(last_name),
      " ", "-"
    ),
    "--", "-"
  )
)

Context: { first_name: " Alice ", last_name: "Van Der Berg" } Result: "alice-van-der-berg"

Chains trim, +, two replace calls, and lower to produce a URL-safe identifier.


10 — Compound SLA risk score

round(
  clamp(
    (
      if(priority == "critical", 40, if(priority == "high", 20, 5))
      + if(dateDiff(today(), due_date, "day") < 2, 35, if(dateDiff(today(), due_date, "day") < 7, 15, 0))
      + if(len(assignee) == 0, 20, 0)
      + if(prop["story_points"] > 8, 10, 0)
    ),
    0, 100
  ),
  0
)

Context: { priority: "critical", due_date: <2 days away>, assignee: "", story_points: 13 } Result: 95

Combines multi-field boolean conditions, date arithmetic, string checks, property access via prop["key"], weighted addition, and a final clamp-and-round. Models a real-world risk-scoring formula where several independent factors each contribute a penalty score.


WASM API Reference

The raw WASM exports (consumed by bridge.ts) are:

Export Signature Description
compile (formula: string) → JSON Compile and cache; returns { ok, handle, error, error_pos }
evaluate (handle: number, propsJson: string) → JSON Run cached handle; returns { ok, value, error }
batch_evaluate (handle: number, rowsJson: string) → JSON Run over array of rows; returns { ok, values, error }
eval_formula (formula: string, propsJson: string) → JSON Compile + evaluate in one call
validate (formula: string) → JSON Returns { ok, errors[], dependencies[] }
get_dependencies (handle: number) → JSON Property names the formula reads
free_formula (handle: number) → void Release handle from WASM cache
clear_formula_cache () → void Clear all cached handles
formula_handle_count () → number Current number of cached handles

All propsJson and rowsJson arguments expect the FormulaValue envelope format:

{ "field_name": { "type": "number", "value": 42 } }

The TypeScript bridge handles this serialisation automatically.


Error Format

Compile and validation errors include source positions for editor highlighting:

{
  "ok": false,
  "error": "unexpected token ')'",
  "error_pos": [12, 13]
}

error_pos is a [start_byte, end_byte] pair relative to the formula string.


License

MIT

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors