# Expr

Parse, evaluate, and type-check Paradoc expressions

Canonical URL: https://docs.paradoc.dev/sdk/expr/

`@paradoc/expr` is the engine behind Paradoc's [expression language](/concepts/logic): 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](/concepts/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

```bash
npm install @paradoc/expr
```

The same surface is re-exported through the SDK under an `expr` namespace, so you can reach it without a separate dependency:

```ts
// 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.

```ts
import { parse } from '@paradoc/expr'

const { ast, errors } = parse('fields.age >= 18')
// ast is the typed expression tree; errors is empty
```

## Evaluating

`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.

```ts
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)
if (sum.success) valueToString(sum.value) // "30"
```

If you already have an AST, `evaluate(ast, ctx)` runs it directly.

`evaluateExpression` is the safe source API. Low-level `evaluate`, `parseOrThrow`, and direct `Decimal` operations may throw. Internal numbers remain decimals; converting them to JavaScript numbers is explicitly lossy. Division retains up to 20 fractional places. The engine bounds expression nesting, regex work, and decimal scale.

`coalesce` stops at the first non-null argument. Dates use canonical `YYYY-MM-DD` values, datetimes representing instants require `Z` or an explicit offset, and calendar month/year addition clamps to the last valid day. Diagnostic offsets count UTF-16 code units.

## 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.

```ts
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: [{
//   name: 'fields.unknownField',
//   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.

## Aggregates

`sum`, `count`, `min`, `max`, `avg`, `any`, and `all` read their first argument as a path into a list rather than as a value; the [Logic](/concepts/logic#aggregating-lists) page covers the language rules. Two things are the host's to supply.

**Row visibility.** Pass `rowVisible` to `createContext` to keep hidden rows out. It receives the list's path and the row's position at every list level, and every row counts when it is absent. `@paradoc/core` supplies it from the artifact's `visible` conditions.

```ts
const ctx = createContext(
  { fields: { items: [{ qty: 2 }, { qty: 5 }] } },
  { rowVisible: (listPath, indices) => !(listPath === 'fields.items' && indices[0] === 1) },
)
evaluateExpression('sum(fields.items.qty)', ctx) // 2: the second row is hidden
```

**Row types.** A `TypeEnv` types a list as `T.array(...)` and registers each item member under the list's own path, typed per row. The checker then knows that `fields.items.qty` is a number read from every row: accepted inside an aggregate, reported with the `invalid-aggregate` code anywhere else.

```ts
const env = createTypeEnv({
  'fields.items': T.array(T.object),
  'fields.items.qty': T.number,
})
check('sum(fields.items.qty)', env).type    // { kind: 'number' }
check('fields.items.qty > 1', env).diagnostics[0].code // 'invalid-aggregate'
```

A money sum over rows in more than one currency fails with the `currency-mismatch` evaluation code.

## 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`.

```ts
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.

## Related

* [Logic](/concepts/logic) - The expression language: operators, functions, addressing
* [Sessions](/sdk/sessions) - The session engine whose gates this powers
* [Artifacts](/sdk/artifacts) - Where `visible` and `required` expressions live
