ExprLast updated on
Last updated on
Parse, evaluate, and type-check Paradoc expressions
@paradoc/expr is the engine behind Paradoc's expression language: the small language that powers visible, required, and computed values. This page documents the package API. For the language itself, its operators, functions, and fields.<id> addressing, see Logic.
The package gives you three things: a parse that turns source into a typed AST, an evaluate that runs an expression against data, and a check that type-checks an expression against an artifact's field types at authoring time. It also ships Decimal for exact decimal arithmetic, so money and percentage math never drift.
Installation
npm install @paradoc/exprThe same surface is re-exported through the SDK under an expr namespace, so you can reach it without a separate dependency:
// Direct import.
import { check, parse, evaluateExpression, Decimal } from '@paradoc/expr'
// Or via the SDK namespace.
import { expr } from '@paradoc/sdk'
expr.check(/* ... */)Parsing
parse returns { ast, errors }. The AST is null when the source has syntax errors; the errors carry positioned diagnostics.
import { parse } from '@paradoc/expr'
const { ast, errors } = parse('fields.age >= 18')
// ast is the typed expression tree; errors is emptyEvaluating
evaluateExpression parses and runs a source string against a context built with createContext. It returns a result, never throws on a bad expression. Values are tagged ({ kind, value }); use valueToString or the Values helpers to read them.
import { evaluateExpression, createContext, valueToString } from '@paradoc/expr'
const ctx = createContext({ fields: { age: 25 } })
const result = evaluateExpression('fields.age >= 18', ctx)
// { success: true, value: { kind: 'boolean', value: true } }
const sum = evaluateExpression('fields.age + 5', ctx)
valueToString(sum.value) // "30"If you already have an AST, evaluate(ast, ctx) runs it directly.
Checking
check is the authoring-time type checker. Its value is validate-as-you-type: as an author edits a visible or computed expression, check confirms every reference resolves and every operator and function is well-typed, surfacing positioned diagnostics before the form ever runs. This is what an editor lints with.
check(source, env) returns { type, diagnostics }. The env is a TypeEnv that maps reference paths to types. Build one from an artifact's field types so the checker knows what fields.age is, then catch references that don't exist.
import { check, createTypeEnv, T } from '@paradoc/expr'
// The host (e.g. @paradoc/core) builds this from real field definitions.
const env = createTypeEnv({
'fields.age': T.number,
'fields.country': T.string,
})
// A well-typed expression: no diagnostics, inferred as boolean.
check('fields.age >= 18', env)
// { type: { kind: 'boolean' }, diagnostics: [] }
// An unknown reference is caught at authoring time.
check('fields.age + fields.unknownField', env)
// diagnostics: [{
// severity: 'error',
// code: 'unknown-identifier',
// message: 'Unknown reference: fields.unknownField',
// span: { ... },
// }]For expressions used as gates (visible, required, include, rules), checkBooleanGate adds a non-boolean-gate error when the result isn't boolean. T is the type constructor (T.number, T.string, T.array(T.string), etc.) and createTypeEnv builds a simple path-to-type environment; hosts build richer ones.
Exact decimals
Decimal is exact-decimal arithmetic backed by bigint. No floating point is used, so 19.95 * 3 is exactly 59.85 and money math doesn't accumulate rounding error. The constructor is private; build values with Decimal.fromString or Decimal.fromInt.
import { Decimal } from '@paradoc/expr'
Decimal.fromString('0.1').add(Decimal.fromString('0.2')).toString()
// "0.3" (not 0.30000000000000004)
Decimal.fromString('19.95').mul(Decimal.fromInt(3)).toString()
// "59.85"Decimal carries the usual operators (add, sub, mul, div, mod, cmp, round, and friends) and is the runtime representation of every number in the language.