From 4c2d6772f1354f8b7e6dd2292386fbcc0cf7839a Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Thu, 23 Apr 2026 23:26:49 +0100 Subject: [PATCH] feat(base-formula): add AST and value types --- packages/base-formula/src/ast.ts | 33 ++++++++++ .../base-formula/src/functions/registry.ts | 2 + packages/base-formula/src/index.client.ts | 3 +- packages/base-formula/src/index.server.ts | 3 +- packages/base-formula/src/types.ts | 61 +++++++++++++++++++ 5 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 packages/base-formula/src/ast.ts create mode 100644 packages/base-formula/src/functions/registry.ts create mode 100644 packages/base-formula/src/types.ts diff --git a/packages/base-formula/src/ast.ts b/packages/base-formula/src/ast.ts new file mode 100644 index 000000000..cff54118b --- /dev/null +++ b/packages/base-formula/src/ast.ts @@ -0,0 +1,33 @@ +export type OpCode = + | "+" | "-" | "*" | "/" | "%" + | "==" | "!=" | ">" | "<" | ">=" | "<=" + | "neg" | "not"; + +export type FormulaAST = + | { t: "num"; v: number } + | { t: "str"; v: string } + | { t: "bool"; v: boolean } + | { t: "null" } + | { t: "prop"; id: string } + | { t: "op"; op: OpCode; args: FormulaAST[] } + | { t: "if"; cond: FormulaAST; then: FormulaAST; else: FormulaAST } + | { t: "and"; args: FormulaAST[] } + | { t: "or"; args: FormulaAST[] } + | { t: "call"; fn: string; args: FormulaAST[] }; + +/* + * Raw AST: what the parser produces before resolving property names to IDs. + * Only the `propName` variant differs from FormulaAST — every other node is + * reused directly. We deliberately keep this type-level to avoid duplicating + * the tree shape. + */ +export type RawFormulaAST = + | Exclude + | { t: "propName"; name: string } + | { t: "op"; op: OpCode; args: RawFormulaAST[] } + | { t: "if"; cond: RawFormulaAST; then: RawFormulaAST; else: RawFormulaAST } + | { t: "and"; args: RawFormulaAST[] } + | { t: "or"; args: RawFormulaAST[] } + | { t: "call"; fn: string; args: RawFormulaAST[] }; + +export const AST_VERSION = 1 as const; diff --git a/packages/base-formula/src/functions/registry.ts b/packages/base-formula/src/functions/registry.ts new file mode 100644 index 000000000..6ed1e27d3 --- /dev/null +++ b/packages/base-formula/src/functions/registry.ts @@ -0,0 +1,2 @@ +// Minimal stub: FormulaFn type will be expanded in Task 10. +export type FormulaFn = unknown; diff --git a/packages/base-formula/src/index.client.ts b/packages/base-formula/src/index.client.ts index e18e0a97a..bebab0e16 100644 --- a/packages/base-formula/src/index.client.ts +++ b/packages/base-formula/src/index.client.ts @@ -1,3 +1,4 @@ // Client-side public surface: parse, typecheck, cycle-detect, pretty-print. // Does NOT export eval or the function registry. -export {}; +export * from "./ast"; +export * from "./types"; diff --git a/packages/base-formula/src/index.server.ts b/packages/base-formula/src/index.server.ts index 9970f7142..77ea21d3f 100644 --- a/packages/base-formula/src/index.server.ts +++ b/packages/base-formula/src/index.server.ts @@ -1,2 +1,3 @@ // Server-side public surface: everything in client + evaluator + registry. -export {}; +export * from "./ast"; +export * from "./types"; diff --git a/packages/base-formula/src/types.ts b/packages/base-formula/src/types.ts new file mode 100644 index 000000000..aa6721669 --- /dev/null +++ b/packages/base-formula/src/types.ts @@ -0,0 +1,61 @@ +import type { FormulaAST } from "./ast"; + +export type FormulaResultType = + | "number" + | "string" + | "boolean" + | "date" + | "null"; + +export type FormulaTypeOptions = { + source: string; + ast: FormulaAST; + resultType: FormulaResultType; + dependencies: string[]; + astVersion: 1; + formatOptions?: Record; +}; + +/* + * The runtime value produced by evaluating a node. Strings and numbers are + * their JS equivalents; dates are ISO 8601 UTC strings (matches how the date + * property type already stores cells); booleans are booleans; missing or + * filtered-out values are null. Errors are distinguishable from all valid + * values because they are objects with a `__err` key. + */ +export type Value = number | string | boolean | null | ErrorCell; + +export type ErrorCell = { + __err: ErrorCode; + msg: string; + v: 1; +}; + +export type ErrorCode = + | "MISSING_PROP" + | "TYPE_MISMATCH" + | "DIV_BY_ZERO" + | "DATE_INVALID" + | "DEPTH_EXCEEDED" + | "DEPENDENCY_ERROR"; + +/* + * EvalContext carries everything the evaluator needs that isn't in the AST: + * the function registry (server-only), the property map for resolving `prop` + * nodes to their formula ASTs when nested, and the current recursion depth. + */ +export type EvalContext = { + registry: ReadonlyMap; + properties: ReadonlyMap; + depth: number; + maxDepth: number; + memo: Map; // keyed by propId for the current row-eval +}; + +export type PropertyLookup = { + id: string; + type: string; + typeOptions: unknown; +}; + +export const DEFAULT_MAX_DEPTH = 64;