@paradoc/expr 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,113 @@
1
+ <p align="center">
2
+ <a href="https://paradoc.dev?utm_source=github&utm_medium=expr" target="_blank" rel="noopener noreferrer">
3
+ <picture>
4
+ <source media="(prefers-color-scheme: dark)" srcset="https://assets.paradoc.dev/logo-400x400.png" type="image/png">
5
+ <img src="https://assets.paradoc.dev/logo-400x400.png" height="64" alt="Paradoc logo">
6
+ </picture>
7
+ </a>
8
+ <br />
9
+ </p>
10
+
11
+ <h1 align="center">@paradoc/expr</h1>
12
+
13
+ <div align="center">
14
+
15
+ [![Paradoc documentation](https://img.shields.io/badge/Documentation-Paradoc-red.svg)](https://docs.paradoc.dev?utm_source=github&utm_medium=expr)
16
+ [![Follow on Twitter](https://img.shields.io/twitter/follow/paradochq?style=social)](https://twitter.com/intent/follow?screen_name=paradochq)
17
+
18
+ </div>
19
+
20
+ [Paradoc](https://paradoc.dev?utm_source=github&utm_medium=expr) is **documents as code**. It lets developers and AI agents define, validate, and render business documents using typed, composable schemas. This eliminates template drift, broken mappings, and brittle glue code, while giving AI systems a reliable document layer they can safely read, reason over, and generate against in production workflows.
21
+
22
+ ## Package overview
23
+
24
+ The purpose-built expression language for Paradoc artifacts. One typed AST is shared by an evaluator and an artifact-aware checker, so authoring-time type checking and runtime evaluation can never disagree. Its scope is logic inside a single artifact: field and section visibility, required, computed defs, validation rules, and payment amounts.
25
+
26
+ - 🧮 **Exact decimal arithmetic** - money math with no floating-point drift
27
+ - 📅 **Temporal types** - date, datetime, time, and duration, with a host-injected `today()` / `now()` clock
28
+ - 🔎 **Authoring-time checker** - catches unknown references, type mismatches, and non-boolean gates before a form ever runs
29
+ - 🧩 **One registry, two consumers** - the evaluator and the checker share a single function registry, so they never drift apart
30
+ - 🛡️ **Null-safe** - missing values read as `null` instead of throwing, and `null` is first-class
31
+
32
+ ## Installation
33
+
34
+ ```bash
35
+ npm install @paradoc/expr
36
+ ```
37
+
38
+ The same surface is re-exported through `@paradoc/sdk` under an `expr` namespace, so SDK users reach it without a separate dependency:
39
+
40
+ ```typescript
41
+ // Direct import.
42
+ import { check, parse, evaluateExpression, Decimal } from "@paradoc/expr";
43
+
44
+ // Or via the SDK namespace.
45
+ import { expr } from "@paradoc/sdk";
46
+ expr.check(/* ... */);
47
+ ```
48
+
49
+ ## Usage
50
+
51
+ Evaluate a source string against a context. It returns a result and never throws on a bad expression; values are tagged (`{ kind, value }`):
52
+
53
+ ```typescript
54
+ import { evaluateExpression, createContext } from "@paradoc/expr";
55
+
56
+ const ctx = createContext({ fields: { age: 25 } });
57
+
58
+ const result = evaluateExpression("fields.age >= 18", ctx);
59
+ // { success: true, value: { kind: "boolean", value: true } }
60
+ ```
61
+
62
+ Type-check an expression at authoring time. This is what an editor lints with as an author edits a `visible` or computed expression:
63
+
64
+ ```typescript
65
+ import { check, createTypeEnv, T } from "@paradoc/expr";
66
+
67
+ const env = createTypeEnv({
68
+ "fields.age": T.number,
69
+ "fields.country": T.string,
70
+ });
71
+
72
+ check("fields.age >= 18", env);
73
+ // { type: { kind: "boolean" }, diagnostics: [] }
74
+
75
+ // An unknown reference is caught before the form ever runs.
76
+ check("fields.age + fields.unknownField", env);
77
+ // diagnostics: [{ severity: "error", code: "unknown-identifier",
78
+ // message: "Unknown reference: fields.unknownField", span: { ... } }]
79
+ ```
80
+
81
+ Money math is exact, never floating point:
82
+
83
+ ```typescript
84
+ import { Decimal } from "@paradoc/expr";
85
+
86
+ Decimal.fromString("0.1").add(Decimal.fromString("0.2")).toString();
87
+ // "0.3" (not 0.30000000000000004)
88
+
89
+ Decimal.fromString("19.95").mul(Decimal.fromInt(3)).toString();
90
+ // "59.85"
91
+ ```
92
+
93
+ The expression language itself (operators, functions, temporal types) is documented at [docs.paradoc.dev](https://docs.paradoc.dev/concepts/logic). `@paradoc/expr` is a boolean and value expression language, not a flow, routing, or form-selection language.
94
+
95
+ ## Changelog
96
+
97
+ View the [Changelog](https://github.com/paradoc-dev/paradoc/blob/main/packages/expr/CHANGELOG.md) for updates.
98
+
99
+ ## Related packages
100
+
101
+ - [`@paradoc/core`](../core) - Runtime and builders; consumes this engine for artifact logic
102
+ - [`@paradoc/sdk`](../sdk) - Complete framework; re-exports this as the `expr` namespace
103
+ - [`@paradoc/types`](../types) - TypeScript utilities and types
104
+
105
+ ## Contributing
106
+
107
+ We're open to all community contributions! If you'd like to contribute in any way, please read our [contribution guidelines](https://github.com/paradoc-dev/paradoc/blob/main/CONTRIBUTING.md) and [code of conduct](https://github.com/paradoc-dev/paradoc/blob/main/CODE_OF_CONDUCT.md).
108
+
109
+ ## License
110
+
111
+ This project is licensed under the MIT license.
112
+
113
+ See [LICENSE](https://github.com/paradoc-dev/paradoc/blob/main/LICENSE) for more information.
@@ -0,0 +1,513 @@
1
+ /**
2
+ * Core shared types for @paradoc/expr: source spans, the expression type
3
+ * system, and diagnostics. Consumed by the parser, evaluator, and checker.
4
+ */
5
+ /** A 0-based byte offset plus 1-based line/column into the source string. */
6
+ interface Position {
7
+ readonly offset: number;
8
+ readonly line: number;
9
+ readonly column: number;
10
+ }
11
+ /** A source span `[start, end)` over the original expression text. */
12
+ interface Span {
13
+ readonly start: Position;
14
+ readonly end: Position;
15
+ }
16
+ /** Primitive (non-parameterized) type kinds in the language. */
17
+ type PrimitiveTypeKind = 'string' | 'number' | 'boolean' | 'date' | 'datetime' | 'time' | 'duration' | 'money' | 'object' | 'null' | 'unknown';
18
+ /** A type in the language. Arrays carry their element type (`array<T>`). */
19
+ type ExprType = {
20
+ readonly kind: PrimitiveTypeKind;
21
+ } | {
22
+ readonly kind: 'array';
23
+ readonly element: ExprType;
24
+ };
25
+ /** Terse constructors, e.g. `T.array(T.string)`. */
26
+ declare const T: {
27
+ readonly string: ExprType;
28
+ readonly number: ExprType;
29
+ readonly boolean: ExprType;
30
+ readonly date: ExprType;
31
+ readonly datetime: ExprType;
32
+ readonly time: ExprType;
33
+ readonly duration: ExprType;
34
+ readonly money: ExprType;
35
+ readonly object: ExprType;
36
+ readonly null: ExprType;
37
+ readonly unknown: ExprType;
38
+ readonly array: (element: ExprType) => ExprType;
39
+ };
40
+ /** Human-readable rendering, e.g. `array<string>`. */
41
+ declare function formatType(t: ExprType): string;
42
+ /** Structural type equality. */
43
+ declare function typesEqual(a: ExprType, b: ExprType): boolean;
44
+ /** Diagnostic severity. Errors block authoring; warnings inform. */
45
+ type Severity = 'error' | 'warning';
46
+ /** Stable diagnostic codes. Extended as the checker grows. */
47
+ type DiagnosticCode = 'syntax' | 'unknown-identifier' | 'unknown-function' | 'type-mismatch' | 'arity' | 'non-boolean-gate' | 'circular-defs' | 'forbidden-operator' | 'non-deterministic' | 'division-by-zero';
48
+ interface Diagnostic {
49
+ readonly severity: Severity;
50
+ readonly code: DiagnosticCode;
51
+ readonly message: string;
52
+ readonly span: Span;
53
+ }
54
+
55
+ /**
56
+ * The typed AST produced by the parser and consumed by both the evaluator and
57
+ * the checker. Every node carries a source `span` so diagnostics can point at
58
+ * exact positions.
59
+ *
60
+ * Numeric literals keep their raw lexeme (`value: string`) so the evaluator can
61
+ * parse them into exact decimals without an intermediate float that would lose
62
+ * precision.
63
+ */
64
+
65
+ interface NodeBase {
66
+ readonly span: Span;
67
+ }
68
+ interface NumberLiteral extends NodeBase {
69
+ readonly kind: 'NumberLiteral';
70
+ /** Raw lexeme, parsed to exact decimal by the evaluator. */
71
+ readonly value: string;
72
+ }
73
+ interface StringLiteral extends NodeBase {
74
+ readonly kind: 'StringLiteral';
75
+ readonly value: string;
76
+ }
77
+ interface BooleanLiteral extends NodeBase {
78
+ readonly kind: 'BooleanLiteral';
79
+ readonly value: boolean;
80
+ }
81
+ interface NullLiteral extends NodeBase {
82
+ readonly kind: 'NullLiteral';
83
+ }
84
+ interface ArrayLiteral extends NodeBase {
85
+ readonly kind: 'ArrayLiteral';
86
+ readonly elements: readonly Expr[];
87
+ }
88
+ /** A bare reference: a defs key, or a context root such as `fields`. */
89
+ interface Identifier extends NodeBase {
90
+ readonly kind: 'Identifier';
91
+ readonly name: string;
92
+ }
93
+ /** Dotted member access `object.property`. Null-safe at evaluation. */
94
+ interface Member extends NodeBase {
95
+ readonly kind: 'Member';
96
+ readonly object: Expr;
97
+ readonly property: string;
98
+ }
99
+ /** Indexed access `object[index]`. Reserved; not an author surface in v1. */
100
+ interface Index extends NodeBase {
101
+ readonly kind: 'Index';
102
+ readonly object: Expr;
103
+ readonly index: Expr;
104
+ }
105
+ type UnaryOp = 'not' | 'neg';
106
+ interface Unary extends NodeBase {
107
+ readonly kind: 'Unary';
108
+ readonly op: UnaryOp;
109
+ readonly operand: Expr;
110
+ }
111
+ type ArithmeticOp = '+' | '-' | '*' | '/' | '%';
112
+ type ComparisonOp = '==' | '!=' | '<' | '<=' | '>' | '>=';
113
+ /** `+` is polymorphic: numeric add or string concat, resolved by operand type. */
114
+ type BinaryOp = ArithmeticOp | ComparisonOp;
115
+ interface Binary extends NodeBase {
116
+ readonly kind: 'Binary';
117
+ readonly op: BinaryOp;
118
+ readonly left: Expr;
119
+ readonly right: Expr;
120
+ }
121
+ type LogicalOp = 'and' | 'or';
122
+ interface Logical extends NodeBase {
123
+ readonly kind: 'Logical';
124
+ readonly op: LogicalOp;
125
+ readonly left: Expr;
126
+ readonly right: Expr;
127
+ }
128
+ /** `element in collection` / `element not in collection`. */
129
+ interface Membership extends NodeBase {
130
+ readonly kind: 'Membership';
131
+ readonly negated: boolean;
132
+ readonly element: Expr;
133
+ readonly collection: Expr;
134
+ }
135
+ /** Ternary `test ? consequent : alternate`. */
136
+ interface Conditional extends NodeBase {
137
+ readonly kind: 'Conditional';
138
+ readonly test: Expr;
139
+ readonly consequent: Expr;
140
+ readonly alternate: Expr;
141
+ }
142
+ /** A function call by name. Functions are not first-class values. */
143
+ interface Call extends NodeBase {
144
+ readonly kind: 'Call';
145
+ readonly callee: string;
146
+ readonly args: readonly Expr[];
147
+ }
148
+ type Expr = NumberLiteral | StringLiteral | BooleanLiteral | NullLiteral | ArrayLiteral | Identifier | Member | Index | Unary | Binary | Logical | Membership | Conditional | Call;
149
+ type ExprKind = Expr['kind'];
150
+
151
+ /**
152
+ * Pure-data grammar configuration consumed by the parser. Declares the keyword
153
+ * set and the operator precedence / associativity table. No logic lives here;
154
+ * the parser (config-declares, packages-execute) reads this to drive parsing.
155
+ */
156
+ /** Reserved words that are never parsed as identifiers. */
157
+ declare const KEYWORDS: readonly ["and", "or", "not", "in", "true", "false", "null"];
158
+ type Keyword = (typeof KEYWORDS)[number];
159
+ type Associativity = 'left' | 'right';
160
+ interface OperatorInfo {
161
+ readonly token: string;
162
+ readonly precedence: number;
163
+ readonly associativity: Associativity;
164
+ }
165
+ /**
166
+ * Binary, logical, and membership operators by binding power (higher binds
167
+ * tighter). Precedence ladder, loosest to tightest:
168
+ * or < and < equality < comparison < membership < additive < multiplicative.
169
+ * Unary operators bind tighter than any binary; the ternary `? :` is the
170
+ * loosest construct and is handled directly by the parser.
171
+ *
172
+ * `not in` is lexed as the `in` operator with a preceding `not`; the parser
173
+ * folds it into a negated Membership node.
174
+ */
175
+ declare const BINARY_OPERATORS: readonly OperatorInfo[];
176
+ /** Prefix unary operators. `!` is an alias for `not`. */
177
+ declare const UNARY_OPERATORS: readonly ["not", "!", "-"];
178
+ /**
179
+ * Operators the language deliberately rejects, mapped to the message the
180
+ * checker surfaces. `=` is assignment (a `==` typo); `||` / `&&` are the
181
+ * dropped overloaded forms.
182
+ */
183
+ declare const FORBIDDEN_OPERATORS: Readonly<Record<string, string>>;
184
+ /** Fast membership lookup for the lexer/parser. */
185
+ declare const KEYWORD_SET: ReadonlySet<string>;
186
+ declare const BINARY_OPERATOR_TOKENS: ReadonlySet<string>;
187
+
188
+ /**
189
+ * The function registry: the single source of truth for the language's
190
+ * callable functions. Both the evaluator and the checker read it, so the
191
+ * design-time and runtime function vocabularies cannot drift (the class of
192
+ * "type-checks green, throws at runtime" bug is structurally impossible).
193
+ *
194
+ * This module declares only SIGNATURES (names, parameter types, return types,
195
+ * flags). The evaluator binds runtime implementations to these names; a
196
+ * conformance test asserts the two sets are identical.
197
+ */
198
+
199
+ /**
200
+ * How a function's return type is determined. Most are `fixed`; `commonOfArgs`
201
+ * (coalesce) and `elementOf` (reserved for collection ops) depend on argument
202
+ * types and are resolved by the checker.
203
+ */
204
+ type ReturnSpec = {
205
+ readonly kind: 'fixed';
206
+ readonly type: ExprType;
207
+ } | {
208
+ readonly kind: 'commonOfArgs';
209
+ } | {
210
+ readonly kind: 'elementOf';
211
+ readonly arg: number;
212
+ };
213
+ interface ParamSpec {
214
+ readonly name: string;
215
+ /** Expected type; `unknown` accepts any argument. */
216
+ readonly type: ExprType;
217
+ readonly optional?: boolean;
218
+ }
219
+ type FnCategory = 'string' | 'number' | 'date' | 'collection' | 'domain' | 'logical';
220
+ interface FnSignature {
221
+ readonly name: string;
222
+ readonly category: FnCategory;
223
+ readonly params: readonly ParamSpec[];
224
+ /** The final parameter may repeat (e.g. `min`, `max`, `coalesce`). */
225
+ readonly variadic?: boolean;
226
+ readonly returns: ReturnSpec;
227
+ /**
228
+ * Requires host-provided context at evaluation: `today`/`now` need the
229
+ * as-of timestamp; the party/witness functions need the party context.
230
+ */
231
+ readonly hostInjected?: boolean;
232
+ /** All registered functions must be deterministic; non-determinism is rejected. */
233
+ readonly deterministic: boolean;
234
+ }
235
+ /**
236
+ * The default registry. Collection aggregates (`any`/`all`/`none`/`sum`/`join`)
237
+ * are intentionally absent until a repeating-group field type exists; see
238
+ * _docs/plans/paradoc-expr/language-spec.md.
239
+ */
240
+ declare const DEFAULT_SIGNATURES: readonly FnSignature[];
241
+ /** A resolved registry: name -> signature, with helpers. */
242
+ interface Registry {
243
+ readonly signatures: ReadonlyMap<string, FnSignature>;
244
+ has(name: string): boolean;
245
+ get(name: string): FnSignature | undefined;
246
+ names(): readonly string[];
247
+ }
248
+ /**
249
+ * Build a registry from the default signatures plus any host-provided
250
+ * extensions (e.g. additional domain functions). Later entries override
251
+ * earlier ones by name.
252
+ */
253
+ declare function buildRegistry(extra?: readonly FnSignature[]): Registry;
254
+
255
+ /**
256
+ * Recursive-descent / precedence-climbing parser. Consumes the token stream and
257
+ * the pure-data grammar config to produce the typed AST. Reports a single,
258
+ * positioned syntax error (good messages over multi-error recovery for v1),
259
+ * including helpful diagnostics for the deliberately rejected operators.
260
+ */
261
+
262
+ interface ParseResult {
263
+ readonly ast: Expr | null;
264
+ readonly errors: readonly Diagnostic[];
265
+ }
266
+ /** Parse an expression string into an AST, collecting a syntax error if any. */
267
+ declare function parse(source: string): ParseResult;
268
+ /** Parse, throwing on the first error. Convenience for callers that expect valid input. */
269
+ declare function parseOrThrow(source: string): Expr;
270
+
271
+ /**
272
+ * Tokenizer. Turns an expression string into a flat token stream with a source
273
+ * span on every token. Forbidden operators (`=`, `||`, `&&`) are lexed as
274
+ * operator tokens so the parser can report them with a helpful message rather
275
+ * than a bare "unexpected character".
276
+ */
277
+
278
+ type TokenType = 'number' | 'string' | 'identifier' | 'keyword' | 'operator' | 'lparen' | 'rparen' | 'lbracket' | 'rbracket' | 'comma' | 'dot' | 'question' | 'colon' | 'eof';
279
+ interface Token {
280
+ readonly type: TokenType;
281
+ readonly value: string;
282
+ readonly span: Span;
283
+ }
284
+ declare class LexError extends Error {
285
+ readonly position: Position;
286
+ constructor(message: string, position: Position);
287
+ }
288
+ declare function tokenize(source: string): Token[];
289
+
290
+ /**
291
+ * Static reference extraction. Walks an AST and returns the set of value
292
+ * references it depends on, as dotted paths rooted at a bare identifier
293
+ * (`fields.age`, `fields.unitPrice.amount`, `isAdult`).
294
+ *
295
+ * This is the load-bearing guarantee behind the dependency-aware fill state in
296
+ * @paradoc/core: gate expressions must expose their field/defs references
297
+ * completely and cheaply. `fullyStatic` is false when a reference path passes
298
+ * through a dynamic segment (an index expression), which the DAG cannot resolve
299
+ * statically; such expressions must be rejected or handled explicitly upstream.
300
+ */
301
+
302
+ interface References {
303
+ /** Dotted paths rooted at an identifier, sorted and de-duplicated. */
304
+ readonly paths: readonly string[];
305
+ /** False if any reference path passes through a dynamic (index) segment. */
306
+ readonly fullyStatic: boolean;
307
+ }
308
+ declare function extractReferences(ast: Expr): References;
309
+
310
+ /**
311
+ * Exact-decimal arithmetic, in-house, backed by `bigint`. No floating point is
312
+ * ever used, so `19.95 * 3` is exactly `59.85` and money/percentage math is
313
+ * exact. A value is `n / 10^scale` with `scale >= 0`.
314
+ *
315
+ * Rounding defaults to half-up (commercial rounding). Division is computed to a
316
+ * bounded number of decimal places and then trimmed of trailing zeros.
317
+ */
318
+ type RoundingMode = 'half-up' | 'down' | 'floor' | 'ceil';
319
+ declare class DivisionByZeroError extends Error {
320
+ constructor();
321
+ }
322
+ declare class Decimal {
323
+ readonly n: bigint;
324
+ readonly scale: number;
325
+ private constructor();
326
+ static fromString(s: string): Decimal;
327
+ static fromInt(i: bigint | number): Decimal;
328
+ static readonly ZERO: Decimal;
329
+ /** Bring two decimals to a common scale, returning aligned magnitudes. */
330
+ private static align;
331
+ add(b: Decimal): Decimal;
332
+ sub(b: Decimal): Decimal;
333
+ mul(b: Decimal): Decimal;
334
+ div(b: Decimal, rm?: RoundingMode): Decimal;
335
+ mod(b: Decimal): Decimal;
336
+ neg(): Decimal;
337
+ abs(): Decimal;
338
+ /** -1, 0, or 1. */
339
+ cmp(b: Decimal): number;
340
+ eq(b: Decimal): boolean;
341
+ lt(b: Decimal): boolean;
342
+ lte(b: Decimal): boolean;
343
+ gt(b: Decimal): boolean;
344
+ gte(b: Decimal): boolean;
345
+ isZero(): boolean;
346
+ /** Round to `digits` decimal places (default 0). Negative/fractional
347
+ * digits are clamped to a valid non-negative integer scale. */
348
+ round(digits?: number, rm?: RoundingMode): Decimal;
349
+ floor(): Decimal;
350
+ ceil(): Decimal;
351
+ private toScale;
352
+ /** Remove trailing fractional zeros, keeping the value identical. */
353
+ private trim;
354
+ toString(): string;
355
+ /** Lossy conversion to a JS number, for interop only. */
356
+ toNumber(): number;
357
+ toJSON(): string;
358
+ }
359
+
360
+ /**
361
+ * Runtime values. The evaluator works over this tagged union. Money is just an
362
+ * `object` with `amount` (number) and `currency` (string); temporal values are
363
+ * ISO strings. Neither needs a dedicated runtime kind: the type system tracks
364
+ * those distinctions, and the date functions parse the strings as needed.
365
+ */
366
+
367
+ type Value = {
368
+ readonly kind: 'number';
369
+ readonly value: Decimal;
370
+ } | {
371
+ readonly kind: 'string';
372
+ readonly value: string;
373
+ } | {
374
+ readonly kind: 'boolean';
375
+ readonly value: boolean;
376
+ } | {
377
+ readonly kind: 'null';
378
+ } | {
379
+ readonly kind: 'array';
380
+ readonly value: readonly Value[];
381
+ } | {
382
+ readonly kind: 'object';
383
+ readonly value: ReadonlyMap<string, Value>;
384
+ };
385
+ declare const NULL: Value;
386
+ declare const Values: {
387
+ readonly number: (d: Decimal) => Value;
388
+ readonly num: (s: string) => Value;
389
+ readonly string: (s: string) => Value;
390
+ readonly boolean: (b: boolean) => Value;
391
+ readonly null: {
392
+ readonly kind: "null";
393
+ };
394
+ readonly array: (v: readonly Value[]) => Value;
395
+ readonly object: (entries: Iterable<readonly [string, Value]>) => Value;
396
+ };
397
+ /**
398
+ * Coerce a plain host JS value into a `Value`. Numbers become exact decimals;
399
+ * objects and arrays convert deeply. Money/temporal are not auto-detected (a
400
+ * money field arrives as `{ amount, currency }`, a date as an ISO string).
401
+ */
402
+ declare function toValue(js: unknown): Value;
403
+ /** Boolean coercion for gate contexts and ternary/logical tests. */
404
+ declare function truthy(v: Value): boolean;
405
+ /** String rendering, used by polymorphic `+` concatenation and display. */
406
+ declare function valueToString(v: Value): string;
407
+ /** Equality for `==` / `!=`. No cross-type coercion: `'1' != 1`. */
408
+ declare function valueEquals(a: Value, b: Value): boolean;
409
+
410
+ /** Runtime evaluation errors. The top-level wrapper maps these to results. */
411
+ type EvalErrorCode = 'division-by-zero' | 'type-error' | 'unknown-identifier' | 'unknown-function' | 'arity' | 'missing-clock';
412
+ declare class EvaluationError extends Error {
413
+ readonly code: EvalErrorCode;
414
+ constructor(code: EvalErrorCode, message: string);
415
+ }
416
+
417
+ /**
418
+ * The evaluation context: how the evaluator resolves bare references, reads the
419
+ * host-supplied "as of" clock for `today()`/`now()`, and dispatches
420
+ * host-injected domain functions (the party/witness predicates).
421
+ */
422
+
423
+ /** The as-of timestamps for `today()`/`now()`, stored with the submission so a
424
+ * re-evaluation is reproducible. Never read from the wall clock. */
425
+ interface AsOf {
426
+ /** ISO date, `YYYY-MM-DD`. */
427
+ readonly date: string;
428
+ /** ISO datetime. */
429
+ readonly datetime: string;
430
+ }
431
+ type HostFunction = (args: readonly Value[]) => Value;
432
+ interface EvaluationContext {
433
+ /** Resolve a top-level identifier (e.g. `fields`, a defs key) or undefined. */
434
+ lookup(name: string): Value | undefined;
435
+ readonly asOf?: AsOf;
436
+ /** Host-injected functions (party/witness predicates), by name. */
437
+ readonly hostFunctions?: Readonly<Record<string, HostFunction>>;
438
+ }
439
+ interface ContextOptions {
440
+ readonly asOf?: AsOf;
441
+ readonly hostFunctions?: Readonly<Record<string, HostFunction>>;
442
+ }
443
+ /** Build a context from a plain host data object (e.g. `{ fields, ...defs }`). */
444
+ declare function createContext(data: Record<string, unknown>, opts?: ContextOptions): EvaluationContext;
445
+
446
+ /**
447
+ * Built-in function implementations, keyed by the same names the registry
448
+ * declares. The evaluator dispatches here; the party/witness domain functions
449
+ * are NOT here (they are host-injected via the context). A conformance test
450
+ * asserts these keys match the registry's non-domain functions exactly.
451
+ */
452
+
453
+ type Impl = (args: readonly Value[], ctx: EvaluationContext) => Value;
454
+ declare const BUILTIN_IMPLS: Readonly<Record<string, Impl>>;
455
+
456
+ /**
457
+ * The tree-walking evaluator. Null-safe member access, polymorphic `+`,
458
+ * exact-decimal arithmetic, short-circuiting logic, membership, ternary, and
459
+ * function dispatch (host-injected functions first, then builtins).
460
+ */
461
+
462
+ declare function evaluate(node: Expr, ctx: EvaluationContext): Value;
463
+ type EvalResult = {
464
+ readonly success: true;
465
+ readonly value: Value;
466
+ } | {
467
+ readonly success: false;
468
+ readonly error: string;
469
+ readonly code?: EvalErrorCode;
470
+ readonly diagnostics?: readonly Diagnostic[];
471
+ };
472
+ /** Parse and evaluate a source expression, capturing errors as a result. */
473
+ declare function evaluateExpression(source: string, ctx: EvaluationContext): EvalResult;
474
+ /**
475
+ * Evaluate an expression in a boolean gate context, returning `defaultValue`
476
+ * when it is undefined or fails (the default-on-failure gate semantics). A
477
+ * boolean literal short-circuits without parsing.
478
+ */
479
+ declare function evaluateBoolean(condExpr: boolean | string | undefined, ctx: EvaluationContext, defaultValue: boolean): boolean;
480
+
481
+ /**
482
+ * The artifact-aware checker. Infers the type of an expression against a
483
+ * host-supplied type environment (which maps reference paths and defs keys to
484
+ * types) and the function registry, emitting positioned diagnostics. This is
485
+ * what an authoring editor lints with.
486
+ *
487
+ * @paradoc/expr stays decoupled from the artifact schema: the host (e.g.
488
+ * @paradoc/core) builds the TypeEnv from real field definitions.
489
+ */
490
+
491
+ interface TypeEnv {
492
+ /** Type of a reference path (`fields.age`, `isAdult`), or undefined if unknown. */
493
+ resolve(path: string): ExprType | undefined;
494
+ readonly registry: Registry;
495
+ }
496
+ interface CheckResult {
497
+ readonly type: ExprType;
498
+ readonly diagnostics: readonly Diagnostic[];
499
+ }
500
+ /** Build a simple TypeEnv from a path->type map (host adapters build richer ones). */
501
+ declare function createTypeEnv(paths: Record<string, ExprType>, registry?: Registry): TypeEnv;
502
+ /** Infer the type of a parsed expression, collecting diagnostics. */
503
+ declare function checkAst(ast: Expr, env: TypeEnv): CheckResult;
504
+ /** Parse and check a source expression. Syntax errors short-circuit type checks. */
505
+ declare function check(source: string, env: TypeEnv): CheckResult;
506
+ /**
507
+ * Check an expression used in a boolean gate (visible/required/include/rule):
508
+ * everything `check` does, plus a non-boolean-gate error when the result is not
509
+ * boolean (boolean literals are allowed as a degenerate gate).
510
+ */
511
+ declare function checkBooleanGate(source: string, env: TypeEnv): CheckResult;
512
+
513
+ export { type ArithmeticOp, type ArrayLiteral, type AsOf, type Associativity, BINARY_OPERATORS, BINARY_OPERATOR_TOKENS, BUILTIN_IMPLS, type Binary, type BinaryOp, type BooleanLiteral, type Call, type CheckResult, type ComparisonOp, type Conditional, type ContextOptions, DEFAULT_SIGNATURES, Decimal, type Diagnostic, type DiagnosticCode, DivisionByZeroError, type EvalErrorCode, type EvalResult, type EvaluationContext, EvaluationError, type Expr, type ExprKind, type ExprType, FORBIDDEN_OPERATORS, type FnCategory, type FnSignature, type HostFunction, type Identifier, type Impl, type Index, KEYWORDS, KEYWORD_SET, type Keyword, LexError, type Logical, type LogicalOp, type Member, type Membership, NULL, type NullLiteral, type NumberLiteral, type OperatorInfo, type ParamSpec, type ParseResult, type Position, type PrimitiveTypeKind, type References, type Registry, type ReturnSpec, type RoundingMode, type Severity, type Span, type StringLiteral, T, type Token, type TokenType, type TypeEnv, UNARY_OPERATORS, type Unary, type UnaryOp, type Value, Values, buildRegistry, check, checkAst, checkBooleanGate, createContext, createTypeEnv, evaluate, evaluateBoolean, evaluateExpression, extractReferences, formatType, parse, parseOrThrow, toValue, tokenize, truthy, typesEqual, valueEquals, valueToString };