@marcbachmann/cel-js
Advanced tools
+80
-42
@@ -1,10 +0,19 @@ | ||
| export class ParseError extends Error { | ||
| class CelError extends Error { | ||
| #node | ||
| constructor(message, node, cause) { | ||
| super(message, {cause}) | ||
| this.name = 'ParseError' | ||
| #code | ||
| #range | ||
| #summary | ||
| constructor({name, code, message, node, cause, range}) { | ||
| super(message, cause ? {cause} : undefined) | ||
| this.name = name | ||
| this.#code = code | ||
| this.#summary = message | ||
| this.#node = node | ||
| this.#range = (range && normalizeRange(range)) || normalizeRange(node) | ||
| if (!node?.input) return | ||
| this.message = formatErrorWithHighlight(this.message, node) | ||
| this.message = formatErrorWithHighlight(this.#summary, node, this.#range) | ||
| } | ||
@@ -16,23 +25,12 @@ | ||
| withAst(node) { | ||
| if (this.#node || !node?.input) return this | ||
| this.#node = node | ||
| this.message = formatErrorWithHighlight(this.message, node) | ||
| return this | ||
| get code() { | ||
| return this.#code | ||
| } | ||
| } | ||
| export class EvaluationError extends Error { | ||
| #node | ||
| constructor(message, node, cause) { | ||
| super(message, {cause}) | ||
| this.name = 'EvaluationError' | ||
| this.#node = node | ||
| if (!node?.input) return | ||
| this.message = formatErrorWithHighlight(this.message, node) | ||
| get range() { | ||
| return this.#range | ||
| } | ||
| get node() { | ||
| return this.#node | ||
| get summary() { | ||
| return this.#summary | ||
| } | ||
@@ -43,3 +41,4 @@ | ||
| this.#node = node | ||
| this.message = formatErrorWithHighlight(this.message, node) | ||
| this.#range ??= normalizeRange(node) | ||
| this.message = formatErrorWithHighlight(this.#summary, node, this.#range) | ||
| return this | ||
@@ -49,28 +48,62 @@ } | ||
| export class TypeError extends Error { | ||
| #node | ||
| constructor(message, node, cause) { | ||
| super(message, {cause}) | ||
| this.name = 'TypeError' | ||
| this.#node = node | ||
| function normalizeArgs(name, defaultCode, message, node, cause) { | ||
| if (typeof message === 'string') return {name, code: defaultCode, message, node, cause} | ||
| if (!node?.input) return | ||
| this.message = formatErrorWithHighlight(this.message, node) | ||
| const opts = message | ||
| if (typeof opts !== 'object') throw new Error('First param to error must be a string or object') | ||
| return { | ||
| name, | ||
| code: opts.code || defaultCode, | ||
| message: opts.message, | ||
| node: opts.node, | ||
| cause: opts.cause, | ||
| range: opts.range | ||
| } | ||
| } | ||
| get node() { | ||
| return this.#node | ||
| export class ParseError extends CelError { | ||
| constructor(message, node, cause) { | ||
| super(normalizeArgs('ParseError', 'parse_error', message, node, cause)) | ||
| } | ||
| } | ||
| withAst(node) { | ||
| if (this.#node || !node?.input) return this | ||
| this.#node = node | ||
| this.message = formatErrorWithHighlight(this.message, node) | ||
| return this | ||
| export class EvaluationError extends CelError { | ||
| constructor(message, node, cause) { | ||
| super(normalizeArgs('EvaluationError', 'evaluation_error', message, node, cause)) | ||
| } | ||
| } | ||
| function formatErrorWithHighlight(message, node) { | ||
| if (node?.pos === undefined) return message | ||
| const pos = node.pos | ||
| export class TypeError extends CelError { | ||
| constructor(message, node, cause) { | ||
| super(normalizeArgs('TypeError', 'type_error', message, node, cause)) | ||
| } | ||
| } | ||
| export function parseError(code, message, node) { | ||
| if (typeof code === 'object') return new ParseError(code) | ||
| return new ParseError({code, message, node}) | ||
| } | ||
| export function evaluationError(code, message, node) { | ||
| if (typeof code === 'object') return new EvaluationError(code) | ||
| return new EvaluationError({code, message, node}) | ||
| } | ||
| export function typeError(code, message, node) { | ||
| if (typeof code === 'object') return new TypeError(code) | ||
| return new TypeError({code, message, node}) | ||
| } | ||
| function normalizeRange(node) { | ||
| const start = node?.pos ?? node?.start | ||
| if (typeof start !== 'number') return | ||
| const end = typeof node.end === 'number' ? node.end : start | ||
| return {start, end} | ||
| } | ||
| function formatErrorWithHighlight(message, node, range) { | ||
| const pos = node.pos ?? range?.start | ||
| if (typeof pos !== 'number') return message | ||
| const input = node.input | ||
@@ -91,3 +124,2 @@ | ||
| // Show a few lines of context | ||
| let contextStart = pos | ||
@@ -100,3 +132,9 @@ let contextEnd = pos | ||
| const highlight = `> ${`${lineNum}`.padStart(4, ' ')} | ${line}\n${' '.repeat(9 + columnNum)}^` | ||
| return `${message}\n\n${highlight}` | ||
| } | ||
| export function attachErrorAst(error, node) { | ||
| if (error instanceof CelError) return error.withAst(node) | ||
| return error | ||
| } |
+14
-2
@@ -1,4 +0,16 @@ | ||
| export * from './index.js' | ||
| import {Environment, check, evaluate, parse} from './index.js' | ||
| import {Duration, UnsignedInt} from './functions.js' | ||
| // Re-export Duration and UnsignedInt from functions.js | ||
| export {Environment, check, evaluate, parse} | ||
| export {Duration, UnsignedInt} from './functions.js' | ||
| declare const evaluator: { | ||
| parse: typeof parse | ||
| evaluate: typeof evaluate | ||
| check: typeof check | ||
| Environment: typeof Environment | ||
| Duration: typeof Duration | ||
| UnsignedInt: typeof UnsignedInt | ||
| } | ||
| export default evaluator |
+6
-6
| import {createRegistry, RootContext} from './registry.js' | ||
| import {EvaluationError} from './errors.js' | ||
| import {evaluationError} from './errors.js' | ||
| import {registerFunctions, Duration, UnsignedInt} from './functions.js' | ||
@@ -77,4 +77,4 @@ import {registerMacros} from './macros.js' | ||
| return this.#checkAST(this.#parser.parse(expression)) | ||
| } catch (e) { | ||
| return {valid: false, error: e} | ||
| } catch (error) { | ||
| return {valid: false, error} | ||
| } | ||
@@ -87,4 +87,4 @@ } | ||
| return {valid: true, type: this.#formatTypeForCheck(typeDecl)} | ||
| } catch (e) { | ||
| return {valid: false, error: e} | ||
| } catch (error) { | ||
| return {valid: false, error} | ||
| } | ||
@@ -124,3 +124,3 @@ } | ||
| super(opts) | ||
| this.Error = EvaluationError | ||
| this.createError = evaluationError | ||
| } | ||
@@ -127,0 +127,0 @@ |
+32
-2
@@ -23,2 +23,5 @@ import type {Registry} from './registry.js' | ||
| toString(): string | ||
| /** Validate and store an unsigned integer value. */ | ||
| verify(v: bigint): void | ||
| } | ||
@@ -44,7 +47,34 @@ | ||
| /** Convert to primitive bigint (seconds only) for operations. */ | ||
| valueOf(): bigint | ||
| /** Construct a duration from a millisecond value. */ | ||
| static fromMilliseconds(ms: number): Duration | ||
| /** Convert to primitive milliseconds for operations. */ | ||
| valueOf(): number | ||
| /** Add another duration. */ | ||
| addDuration(other: Duration): Duration | ||
| /** Subtract another duration. */ | ||
| subtractDuration(other: Duration): Duration | ||
| /** Add this duration to a timestamp. */ | ||
| extendTimestamp(timestamp: Date): Date | ||
| /** Subtract this duration from a timestamp. */ | ||
| subtractTimestamp(timestamp: Date): Date | ||
| /** Convert to string representation in format like "5s", "1h30m", etc. */ | ||
| toString(): string | ||
| /** Whole hours represented by this duration. */ | ||
| getHours(): bigint | ||
| /** Whole minutes represented by this duration. */ | ||
| getMinutes(): bigint | ||
| /** Whole seconds represented by this duration. */ | ||
| getSeconds(): bigint | ||
| /** Total milliseconds represented by this duration. */ | ||
| getMilliseconds(): bigint | ||
| } | ||
@@ -51,0 +81,0 @@ |
+87
-49
@@ -1,9 +0,6 @@ | ||
| import {EvaluationError} from './errors.js' | ||
| import {evaluationError} from './errors.js' | ||
| import {TYPES, Type} from './registry.js' | ||
| import {register as registerOptional} from './optional.js' | ||
| import {objKeys, arrayFrom} from './globals.js' | ||
| import {objKeys, arrayFrom, MIN_UINT, MAX_UINT, MIN_INT, MAX_INT} from './globals.js' | ||
| const MIN_UINT = 0n | ||
| const MAX_UINT = 18446744073709551615n | ||
| export class UnsignedInt { | ||
@@ -28,3 +25,5 @@ #value | ||
| verify(v) { | ||
| if (v < MIN_UINT || v > MAX_UINT) throw new EvaluationError('Unsigned integer overflow') | ||
| if (v < MIN_UINT || v > MAX_UINT) { | ||
| throw evaluationError('numeric_overflow', 'Unsigned integer overflow') | ||
| } | ||
| this.#value = v | ||
@@ -42,9 +41,12 @@ } | ||
| const billion = 1_000_000_000 | ||
| const billionBigInt = 1_000_000_000n | ||
| const UNIT_NANOSECONDS = { | ||
| h: 3600000000000n, | ||
| m: 60000000000n, | ||
| s: 1000000000n, | ||
| ms: 1000000n, | ||
| us: 1000n, | ||
| µs: 1000n, | ||
| h: 3_600_000_000_000n, | ||
| m: 60_000_000_000n, | ||
| s: billionBigInt, | ||
| ms: 1_000_000n, | ||
| us: 1_000n, | ||
| µs: 1_000n, | ||
| ns: 1n | ||
@@ -71,3 +73,3 @@ } | ||
| valueOf() { | ||
| return Number(this.#seconds) * 1000 + this.#nanos / 1000000 | ||
| return Number(this.#seconds) * 1000 + this.#nanos / 1_000_000 | ||
| } | ||
@@ -77,4 +79,4 @@ | ||
| const totalNanos = BigInt(Math.trunc(ms * 1_000_000)) | ||
| const seconds = totalNanos / 1_000_000_000n | ||
| const nanos = Number(totalNanos % 1_000_000_000n) | ||
| const seconds = totalNanos / billionBigInt | ||
| const nanos = Number(totalNanos % billionBigInt) | ||
| return new Duration(seconds, nanos) | ||
@@ -86,4 +88,4 @@ } | ||
| return new Duration( | ||
| this.#seconds + other.seconds + BigInt(Math.floor(nanos / 1_000_000_000)), | ||
| nanos % 1_000_000_000 | ||
| this.#seconds + other.seconds + BigInt(Math.floor(nanos / billion)), | ||
| nanos % billion | ||
| ) | ||
@@ -95,4 +97,4 @@ } | ||
| return new Duration( | ||
| this.#seconds - other.seconds + BigInt(Math.floor(nanos / 1_000_000_000)), | ||
| (nanos + 1_000_000_000) % 1_000_000_000 | ||
| this.#seconds - other.seconds + BigInt(Math.floor(nanos / billion)), | ||
| (nanos + billion) % billion | ||
| ) | ||
@@ -115,3 +117,3 @@ } | ||
| const nanos = this.#nanos | ||
| ? (this.#nanos / 1000000000) | ||
| ? (this.#nanos / billion) | ||
| .toLocaleString('en-US', {useGrouping: false, maximumFractionDigits: 9}) | ||
@@ -177,3 +179,6 @@ .slice(1) | ||
| default: | ||
| throw new EvaluationError(`bool() conversion error: invalid string value "${v}"`) | ||
| throw evaluationError( | ||
| 'bool_conversion_error', | ||
| `bool() conversion error: invalid string value "${v}"` | ||
| ) | ||
| } | ||
@@ -199,3 +204,6 @@ }) | ||
| if (!v || v !== v.trim()) | ||
| throw new EvaluationError('double() type error: cannot convert to double') | ||
| throw evaluationError( | ||
| 'double_conversion_error', | ||
| 'double() type error: cannot convert to double' | ||
| ) | ||
@@ -217,3 +225,6 @@ const s = v.toLowerCase() | ||
| if (!Number.isNaN(parsed)) return parsed | ||
| throw new EvaluationError('double() type error: cannot convert to double') | ||
| throw evaluationError( | ||
| 'double_conversion_error', | ||
| 'double() type error: cannot convert to double' | ||
| ) | ||
| } | ||
@@ -226,3 +237,3 @@ } | ||
| if (Number.isFinite(v)) return BigInt(Math.trunc(v)) | ||
| throw new EvaluationError('int() type error: integer overflow') | ||
| throw evaluationError('numeric_overflow', 'int() type error: integer overflow') | ||
| }) | ||
@@ -232,3 +243,3 @@ | ||
| if (v !== v.trim() || v.length > 20 || v.includes('0x')) { | ||
| throw new EvaluationError('int() type error: cannot convert to int') | ||
| throw evaluationError('int_conversion_error', 'int() type error: cannot convert to int') | ||
| } | ||
@@ -238,6 +249,6 @@ | ||
| const num = BigInt(v) | ||
| if (num <= 9223372036854775807n && num >= -9223372036854775808n) return num | ||
| if (num <= MAX_INT && num >= MIN_INT) return num | ||
| } catch (_e) {} | ||
| throw new EvaluationError('int() type error: cannot convert to int') | ||
| throw evaluationError('int_conversion_error', 'int() type error: cannot convert to int') | ||
| }) | ||
@@ -250,3 +261,3 @@ | ||
| } catch (e) { | ||
| throw new EvaluationError('uint() type error: cannot convert to uint') | ||
| throw evaluationError('uint_conversion_error', 'uint() type error: cannot convert to uint') | ||
| } | ||
@@ -258,3 +269,3 @@ }) | ||
| } catch (e) { | ||
| throw new EvaluationError('uint() type error: unsigned integer overflow') | ||
| throw evaluationError('numeric_overflow', 'uint() type error: unsigned integer overflow') | ||
| } | ||
@@ -265,3 +276,3 @@ }) | ||
| if (v !== v.trim() || v.length > 20 || v.includes('0x')) { | ||
| throw new EvaluationError('uint() type error: cannot convert to uint') | ||
| throw evaluationError('uint_conversion_error', 'uint() type error: cannot convert to uint') | ||
| } | ||
@@ -272,3 +283,3 @@ | ||
| } catch (e) { | ||
| throw new EvaluationError('uint() type error: cannot convert to uint') | ||
| throw evaluationError('uint_conversion_error', 'uint() type error: cannot convert to uint') | ||
| } | ||
@@ -303,3 +314,6 @@ }) | ||
| if (fromIndex < 0 || fromIndex >= string.length) { | ||
| throw new EvaluationError('string.indexOf(search, fromIndex): fromIndex out of range') | ||
| throw evaluationError( | ||
| 'index_out_of_range', | ||
| 'string.indexOf(search, fromIndex): fromIndex out of range' | ||
| ) | ||
| } | ||
@@ -319,3 +333,6 @@ | ||
| if (fromIndex < 0 || fromIndex >= string.length) { | ||
| throw new EvaluationError('string.lastIndexOf(search, fromIndex): fromIndex out of range') | ||
| throw evaluationError( | ||
| 'index_out_of_range', | ||
| 'string.lastIndexOf(search, fromIndex): fromIndex out of range' | ||
| ) | ||
| } | ||
@@ -329,3 +346,6 @@ | ||
| if (start < 0 || start > string.length) { | ||
| throw new EvaluationError('string.substring(start, end): start index out of range') | ||
| throw evaluationError( | ||
| 'index_out_of_range', | ||
| 'string.substring(start, end): start index out of range' | ||
| ) | ||
| } | ||
@@ -339,3 +359,6 @@ | ||
| if (start < 0 || start > string.length) { | ||
| throw new EvaluationError('string.substring(start, end): start index out of range') | ||
| throw evaluationError( | ||
| 'index_out_of_range', | ||
| 'string.substring(start, end): start index out of range' | ||
| ) | ||
| } | ||
@@ -345,3 +368,6 @@ | ||
| if (end < start || end > string.length) { | ||
| throw new EvaluationError('string.substring(start, end): end index out of range') | ||
| throw evaluationError( | ||
| 'index_out_of_range', | ||
| 'string.substring(start, end): end index out of range' | ||
| ) | ||
| } | ||
@@ -356,3 +382,3 @@ | ||
| } catch (_err) { | ||
| throw new EvaluationError(`Invalid regular expression: ${b}`) | ||
| throw evaluationError('invalid_regular_expression', `Invalid regular expression: ${b}`) | ||
| } | ||
@@ -375,3 +401,6 @@ }) | ||
| if (typeof v[i] !== 'string') { | ||
| throw new EvaluationError('string.join(): list must contain only strings') | ||
| throw evaluationError( | ||
| 'invalid_list_element_type', | ||
| 'string.join(): list must contain only strings' | ||
| ) | ||
| } | ||
@@ -385,3 +414,6 @@ } | ||
| if (typeof v[i] !== 'string') { | ||
| throw new EvaluationError('string.join(separator): list must contain only strings') | ||
| throw evaluationError( | ||
| 'invalid_list_element_type', | ||
| 'string.join(separator): list must contain only strings' | ||
| ) | ||
| } | ||
@@ -423,3 +455,5 @@ } | ||
| functionOverload('bytes.at(int): int', (b, index) => { | ||
| if (index < 0 || index >= b.length) throw new EvaluationError('Bytes index out of range') | ||
| if (index < 0 || index >= b.length) { | ||
| throw evaluationError('index_out_of_range', 'Bytes index out of range') | ||
| } | ||
| return BigInt(b[index]) | ||
@@ -451,3 +485,3 @@ }) | ||
| if (v.length < 20 || v.length > 30) { | ||
| throw new EvaluationError('timestamp() requires a string in ISO 8601 format') | ||
| throw evaluationError('invalid_timestamp', 'timestamp() requires a string in ISO 8601 format') | ||
| } | ||
@@ -457,3 +491,3 @@ | ||
| if (d <= 253402300799999 && d >= -62135596800000) return d | ||
| throw new EvaluationError('timestamp() requires a string in ISO 8601 format') | ||
| throw evaluationError('invalid_timestamp', 'timestamp() requires a string in ISO 8601 format') | ||
| }) | ||
@@ -464,3 +498,6 @@ | ||
| if (i <= 253402300799999 && i >= -62135596800000) return new Date(i) | ||
| throw new EvaluationError('timestamp() requires a valid integer unix timestamp') | ||
| throw evaluationError( | ||
| 'invalid_timestamp', | ||
| 'timestamp() requires a valid integer unix timestamp' | ||
| ) | ||
| }) | ||
@@ -499,3 +536,3 @@ | ||
| function parseDuration(/** @type {string} */ string) { | ||
| if (!string) throw new EvaluationError(`Invalid duration string: ''`) | ||
| if (!string) throw evaluationError('invalid_duration', `Invalid duration string: ''`) | ||
@@ -508,5 +545,6 @@ const isNegative = string[0] === '-' | ||
| const match = parseDurationPattern.exec(string) | ||
| if (!match) throw new EvaluationError(`Invalid duration string: ${string}`) | ||
| if (!match) throw evaluationError('invalid_duration', `Invalid duration string: ${string}`) | ||
| if (match.index !== 0) throw new EvaluationError(`Invalid duration string: ${string}`) | ||
| if (match.index !== 0) | ||
| throw evaluationError('invalid_duration', `Invalid duration string: ${string}`) | ||
| string = string.slice(match[0].length) | ||
@@ -518,3 +556,3 @@ | ||
| const fracNanos = fracPart | ||
| ? (BigInt(fracPart.slice(0, 13).padEnd(13, '0')) * unitNanos) / 10000000000000n | ||
| ? (BigInt(fracPart.slice(0, 13).padEnd(13, '0')) * unitNanos) / 10_000_000_000_000n | ||
| : 0n | ||
@@ -526,4 +564,4 @@ | ||
| const seconds = nanoseconds >= 1000000000n ? nanoseconds / 1000000000n : 0n | ||
| const nanos = Number(nanoseconds % 1000000000n) | ||
| const seconds = nanoseconds >= billionBigInt ? nanoseconds / billionBigInt : 0n | ||
| const nanos = Number(nanoseconds % billionBigInt) | ||
@@ -530,0 +568,0 @@ if (isNegative) return new Duration(-seconds, -nanos) |
+6
-0
@@ -8,2 +8,8 @@ export const hasOwn = Object.hasOwn | ||
| export const MIN_UINT = 0n | ||
| export const MAX_UINT = 18446744073709551615n | ||
| export const MAX_INT = 9223372036854775807n | ||
| export const MIN_INT = -9223372036854775808n | ||
| export function isAsync(fn, fallback) { | ||
@@ -10,0 +16,0 @@ if (fn?.[Symbol.toStringTag] === 'AsyncFunction') return true |
+181
-38
| import type {UnsignedInt} from './functions.js' | ||
| import type { | ||
| DefinitionsResult, | ||
| RegisterConstantDeclaration, | ||
| RegisterFunctionDeclaration, | ||
| RegisterFunctionMetadata, | ||
| RegisterFunctionOptions, | ||
| RegisterTypeDeclaration, | ||
| RegisterTypeDefinition, | ||
| RegisterVariableDeclaration, | ||
| RegisterVariableMetadata, | ||
| RegisterVariableOptions, | ||
| RegisteredFunctionHandler, | ||
| RegisteredVariableType | ||
| } from './registry.js' | ||
@@ -63,7 +77,42 @@ /** | ||
| interface ASTNodeBase<T extends ASTOperator> { | ||
| /** The position in the source string where this node starts */ | ||
| export interface SourceRange { | ||
| readonly start: number | ||
| readonly end: number | ||
| } | ||
| export interface ErrorOptions { | ||
| readonly code?: string | ||
| readonly message?: string | ||
| readonly node?: ErrorLocation | ASTNode | ||
| readonly cause?: unknown | ||
| readonly range?: SourceRange | ||
| } | ||
| export interface SourceLocation { | ||
| /** Compatibility position used by existing consumers and error reporting. */ | ||
| readonly pos: number | ||
| /** The full source range start for this node or error anchor. */ | ||
| readonly start: number | ||
| /** The full source range end for this node or error anchor. */ | ||
| readonly end: number | ||
| /** The original CEL input string */ | ||
| readonly input?: string | ||
| /** The full source range for this node or error anchor. */ | ||
| readonly range?: SourceRange | ||
| } | ||
| export interface ErrorLocation extends SourceLocation { | ||
| /** Present when the error is attached to a parsed AST node. */ | ||
| readonly op?: ASTOperator | ||
| /** Present when the error is attached to a parsed AST node. */ | ||
| readonly args?: unknown | ||
| /** Present when the error is attached to a parsed AST node. */ | ||
| toOldStructure?(): LegacyAstTuple | ||
| } | ||
| interface ASTNodeBase<T extends ASTOperator> extends SourceLocation { | ||
| /** The original CEL input string for this parsed AST node. */ | ||
| readonly input: string | ||
| /** The full source range for this AST node. */ | ||
| readonly range: SourceRange | ||
| /** Operator for this node */ | ||
@@ -85,7 +134,33 @@ readonly op: T | ||
| */ | ||
| export interface Context { | ||
| [key: string]: any | ||
| } | ||
| export type Context = Record<string, any> | ||
| export type {RootContext, OverlayContext} from './registry.d' | ||
| export type { | ||
| DefinitionFunction, | ||
| DefinitionFunctionParam, | ||
| DefinitionsResult, | ||
| DefinitionVariable, | ||
| ObjectSchema, | ||
| OverlayContext, | ||
| RegisterConstantDeclaration, | ||
| RegisterFunctionDeclaration, | ||
| RegisterFunctionMetadata, | ||
| RegisterFunctionOptions, | ||
| RegisterFunctionWithName, | ||
| RegisterFunctionWithSignature, | ||
| RegisterTypeDeclaration, | ||
| RegisterTypeDefinition, | ||
| RegisteredFunctionHandler, | ||
| RegisteredFunctionParam, | ||
| RegisteredFunctionTypedParam, | ||
| RegisteredType, | ||
| RegisteredTypeFieldDeclaration, | ||
| RegisteredVariableType, | ||
| RegisterVariableDeclaration, | ||
| RegisterVariableMetadata, | ||
| RegisterVariableOptions, | ||
| RegisterVariableSchemaOptions, | ||
| RegisterVariableTypeOptions, | ||
| RootContext, | ||
| TypeDeclaration | ||
| } from './registry.js' | ||
@@ -100,4 +175,4 @@ /** | ||
| type?: string | ||
| /** The type error that occurred (only present if valid is false) */ | ||
| error?: TypeError | ||
| /** The parse or type error that occurred (only present if valid is false) */ | ||
| error?: ParseError | TypeError | ||
| } | ||
@@ -117,4 +192,10 @@ | ||
| export class ParseError extends Error { | ||
| constructor(message: string) | ||
| constructor(message: string, node?: ErrorLocation, cause?: unknown) | ||
| constructor(options: ErrorOptions) | ||
| readonly name: 'ParseError' | ||
| readonly code: string | ||
| readonly summary: string | ||
| readonly node?: ErrorLocation | ||
| readonly range?: SourceRange | ||
| withAst(node: ASTNode | ErrorLocation): this | ||
| } | ||
@@ -126,4 +207,10 @@ | ||
| export class EvaluationError extends Error { | ||
| constructor(message: string) | ||
| constructor(message: string, node?: ASTNode, cause?: unknown) | ||
| constructor(options: ErrorOptions) | ||
| readonly name: 'EvaluationError' | ||
| readonly code: string | ||
| readonly summary: string | ||
| readonly node?: ASTNode | ||
| readonly range?: SourceRange | ||
| withAst(node: ASTNode | SourceLocation): this | ||
| } | ||
@@ -136,4 +223,10 @@ | ||
| export class TypeError extends Error { | ||
| constructor(message: string) | ||
| constructor(message: string, node?: ASTNode, cause?: unknown) | ||
| constructor(options: ErrorOptions) | ||
| readonly name: 'TypeError' | ||
| readonly code: string | ||
| readonly summary: string | ||
| readonly node?: ASTNode | ||
| readonly range?: SourceRange | ||
| withAst(node: ASTNode | SourceLocation): this | ||
| } | ||
@@ -145,3 +238,3 @@ | ||
| */ | ||
| export class Optional { | ||
| export class Optional<T = unknown> { | ||
| /** | ||
@@ -152,3 +245,3 @@ * Create a new Optional with a value. | ||
| */ | ||
| static of(value: any): Optional | ||
| static of<T>(value: T): Optional<T> | ||
@@ -159,3 +252,3 @@ /** | ||
| */ | ||
| static none(): Optional | ||
| static none(): Optional<never> | ||
@@ -170,3 +263,3 @@ /** Check if a value is present. */ | ||
| */ | ||
| value(): any | ||
| value(): T | ||
@@ -178,3 +271,3 @@ /** | ||
| */ | ||
| or(optional: Optional): Optional | ||
| or<U>(optional: Optional<U>): Optional<T | U> | ||
@@ -186,3 +279,3 @@ /** | ||
| */ | ||
| orValue(defaultValue: any): any | ||
| orValue<U>(defaultValue: U): T | U | ||
| } | ||
@@ -228,2 +321,10 @@ | ||
| /** | ||
| * Type check a CEL expression string directly. | ||
| * | ||
| * @param expression - The CEL expression string to check | ||
| * @returns Validation result with inferred type or error details | ||
| */ | ||
| export function check(expression: string): TypeCheckResult | ||
| /** | ||
| * Serialize an AST back to a CEL expression string. | ||
@@ -282,2 +383,6 @@ * | ||
| export type ResolvedEnvironmentOptions = Omit<Required<EnvironmentOptions>, 'limits'> & { | ||
| limits: Limits | ||
| } | ||
| /** | ||
@@ -297,2 +402,5 @@ * Environment for CEL expression evaluation with type checking and custom functions. | ||
| export class Environment { | ||
| /** The fully resolved options for this environment instance. */ | ||
| readonly opts: ResolvedEnvironmentOptions | ||
| /** | ||
@@ -317,3 +425,3 @@ * Create a new Environment with optional configuration. | ||
| * @param typename - The name of the type (e.g., 'Vector', 'Point') | ||
| * @param constructor - The constructor function or class for the type | ||
| * @param definition - The type constructor or registration object | ||
| * @returns This environment for chaining | ||
@@ -325,5 +433,8 @@ * | ||
| * env.registerType('Vector', Vector) | ||
| * env.registerType('Vector', {ctor: Vector, fields: {x: 'double', y: 'double'}}) | ||
| * env.registerType({name: 'Vector', schema: {x: 'double', y: 'double'}}) | ||
| * ``` | ||
| */ | ||
| registerType(typename: string, constructor: any): this | ||
| registerType(typename: string, definition: RegisterTypeDefinition): this | ||
| registerType(definition: RegisterTypeDeclaration): this | ||
@@ -333,4 +444,3 @@ /** | ||
| * | ||
| * @param name - The variable name | ||
| * @param type - The CEL type name ('string', 'int', 'double', 'bool', 'list', 'map', etc.) | ||
| * Supports `name + type`, `name + {type|schema}`, and a single declaration object. | ||
| * @returns This environment for chaining | ||
@@ -342,6 +452,13 @@ * @throws Error if variable is already registered | ||
| * env.registerVariable('username', 'string') | ||
| * .registerVariable('count', 'int') | ||
| * env.registerVariable('user', {type: 'map', description: 'The current user'}) | ||
| * env.registerVariable({name: 'profile', schema: {email: 'string'}}) | ||
| * ``` | ||
| */ | ||
| registerVariable(name: string, type: string): this | ||
| registerVariable( | ||
| name: string, | ||
| type: RegisteredVariableType, | ||
| opts?: RegisterVariableMetadata | ||
| ): this | ||
| registerVariable(name: string, options: RegisterVariableOptions): this | ||
| registerVariable(definition: RegisterVariableDeclaration): this | ||
@@ -351,5 +468,3 @@ /** | ||
| * | ||
| * @param name - The constant identifier exposed to CEL expressions | ||
| * @param type - The CEL type name of the constant (e.g., 'int', 'string') | ||
| * @param value - The concrete value supplied during registration | ||
| * Supports `name + type + value` and a single declaration object. | ||
| * @returns This environment for chaining further registrations | ||
@@ -360,6 +475,8 @@ * | ||
| * const env = new Environment().registerConstant('timezone', 'string', 'UTC') | ||
| * env.registerConstant({name: 'minAge', type: 'int', value: 18n, description: 'Minimum age'}) | ||
| * env.evaluate('timezone == "UTC"') // true | ||
| * ``` | ||
| */ | ||
| registerConstant(name: string, type: string, value: any): this | ||
| registerConstant(name: string, type: RegisteredVariableType, value: any): this | ||
| registerConstant(definition: RegisterConstantDeclaration): this | ||
@@ -369,4 +486,6 @@ /** | ||
| * | ||
| * Supports signature-based registration as well as a single declaration object. | ||
| * @param signature - Function signature in format 'name(type1, type2): returnType' or 'Type.method(args): returnType' | ||
| * @param handlerOrOptions - Either the function implementation or an options object with handler and optional typeCheck | ||
| * @param handler - The function implementation | ||
| * @param opts - Optional metadata such as descriptions, param docs, and async hints | ||
| * @returns This environment for chaining | ||
@@ -379,10 +498,29 @@ * | ||
| * | ||
| * // Signature string with descriptions | ||
| * env.registerFunction('greet(string): string', handler, {description: 'Greets someone'}) | ||
| * | ||
| * // Instance method | ||
| * env.registerFunction('string.reverse(): string', (str) => str.split('').reverse().join('')) | ||
| * | ||
| * // Macro function with type checker | ||
| * env.registerFunction('list.custom(ast, ast): bool', { | ||
| * handler: (receiver, ast) => { ... }, | ||
| * typeCheck: (checker, receiverType, args) => 'bool' | ||
| * // Single object with signature and named params | ||
| * env.registerFunction({ | ||
| * signature: 'formatDate(int, string): string', | ||
| * handler, | ||
| * description: 'Formats a timestamp', | ||
| * params: [ | ||
| * {name: 'timestamp', description: 'Unix timestamp in seconds'}, | ||
| * {name: 'format', description: 'Date format string'} | ||
| * ] | ||
| * }) | ||
| * | ||
| * // Macro function | ||
| * env.registerFunction('macro(ast): dyn', ({args}) => ({ | ||
| * firstArgument: args[0], | ||
| * typeCheck(checker, macro, ctx) { | ||
| * return checker.check(macro.firstArgument, ctx) | ||
| * }, | ||
| * evaluate(evaluator, macro, ctx) { | ||
| * return evaluator.eval(macro.firstArgument, ctx) | ||
| * } | ||
| * })) | ||
| * ``` | ||
@@ -392,9 +530,7 @@ */ | ||
| signature: string, | ||
| handlerOrOptions: | ||
| | ((...args: any[]) => any) | ||
| | { | ||
| handler: (...args: any[]) => any | ||
| typeCheck?: (checker: any, receiverType: string, args: any[]) => string | ||
| } | ||
| handler: RegisteredFunctionHandler, | ||
| opts?: RegisterFunctionMetadata | ||
| ): this | ||
| registerFunction(signature: string, options: RegisterFunctionOptions): this | ||
| registerFunction(definition: RegisterFunctionDeclaration): this | ||
@@ -416,2 +552,8 @@ /** | ||
| /** | ||
| * Return user-facing definitions for all registered variables and functions, | ||
| * including the built-ins inherited from the global environment. | ||
| */ | ||
| getDefinitions(): DefinitionsResult | ||
| /** | ||
| * Check if a variable is registered in this environment. | ||
@@ -486,2 +628,3 @@ * | ||
| evaluate: typeof evaluate | ||
| check: typeof check | ||
| serialize: typeof serialize | ||
@@ -488,0 +631,0 @@ Environment: typeof Environment |
+5
-5
@@ -1,2 +0,2 @@ | ||
| import {EvaluationError, ParseError} from './errors.js' | ||
| import {evaluationError, parseError} from './errors.js' | ||
| import {OPERATORS as OPS} from './operators.js' | ||
@@ -7,3 +7,3 @@ const identity = (x) => x | ||
| if (node.op === 'id') return node.args | ||
| throw new ParseError(message, node) | ||
| throw parseError('invalid_macro_argument', message, node) | ||
| } | ||
@@ -99,3 +99,3 @@ | ||
| if (!(!inOptionalContext && i && node.op === '.')) break | ||
| throw new EvaluationError(`No such key: ${node.args[1]}`, node) | ||
| throw evaluationError('no_such_key', `No such key: ${node.args[1]}`, node) | ||
| } | ||
@@ -107,7 +107,7 @@ return obj !== undefined | ||
| let node = macro.args[0] | ||
| if (node.op !== '.') throw new checker.Error(invalidHasArgument, node) | ||
| if (node.op !== '.') throw checker.createError('invalid_macro_argument', invalidHasArgument, node) | ||
| if (!macro.macroHasProps) { | ||
| const props = [] | ||
| while (node.op === '.' || node.op === '.?') node = props.push(node) && node.args[0] | ||
| if (node.op !== 'id') throw new checker.Error(invalidHasArgument, node) | ||
| if (node.op !== 'id') throw checker.createError('invalid_macro_argument', invalidHasArgument, node) | ||
| checker.check(node, ctx) | ||
@@ -114,0 +114,0 @@ props.push(node) |
+82
-35
| import {celTypes} from './registry.js' | ||
| import {objKeys, isArray, hasOwn} from './globals.js' | ||
| import {attachErrorAst} from './errors.js' | ||
| const dynType = celTypes.dyn | ||
@@ -65,3 +66,3 @@ | ||
| function unsupportedType(self, type) { | ||
| throw new self.Error(`Unsupported type: ${type}`) | ||
| throw self.createError('unsupported_type', `Unsupported type: ${type}`) | ||
| } | ||
@@ -119,13 +120,15 @@ | ||
| function checkElementHomogenous(chk, ctx, expected, el, m) { | ||
| const HOMOGENEOUS_PREFIX = { | ||
| heterogeneous_list_element: 'List elements must have the same type,', | ||
| heterogeneous_map_key: 'Map key uses wrong type,', | ||
| heterogeneous_map_value: 'Map value uses wrong type,' | ||
| } | ||
| function checkElementHomogenous(chk, ctx, expected, el, code) { | ||
| const type = chk.check(el, ctx) | ||
| if (type === expected || expected.isEmpty()) return type | ||
| if (type.isEmpty()) return expected | ||
| let prefix | ||
| if (m === 0) prefix = 'List elements must have the same type,' | ||
| else if (m === 1) prefix = 'Map key uses wrong type,' | ||
| else if (m === 2) prefix = 'Map value uses wrong type,' | ||
| throw new chk.Error( | ||
| `${prefix} expected type '${chk.formatType(expected)}' but found '${chk.formatType(type)}'`, | ||
| throw chk.createError( | ||
| code, | ||
| `${HOMOGENEOUS_PREFIX[code]} expected type '${chk.formatType(expected)}' but found '${chk.formatType(type)}'`, | ||
| el | ||
@@ -141,3 +144,7 @@ ) | ||
| const type = ev.debugRuntimeType(value) | ||
| return new ev.Error(`${node.meta.label || 'Ternary condition must be bool'}, got '${type}'`, node) | ||
| return ev.createError( | ||
| 'invalid_condition_type', | ||
| `${node.meta.label || 'Ternary condition must be bool'}, got '${type}'`, | ||
| node | ||
| ) | ||
| } | ||
@@ -153,3 +160,7 @@ | ||
| const type = ev.debugRuntimeType(value) | ||
| return new ev.Error(`Logical operator requires bool operands, got '${type}'`, node) | ||
| return ev.createError( | ||
| 'invalid_logical_operand', | ||
| `Logical operator requires bool operands, got '${type}'`, | ||
| node | ||
| ) | ||
| } | ||
@@ -184,3 +195,4 @@ | ||
| if (!leftType.isDynOrBool()) { | ||
| throw new chk.Error( | ||
| throw chk.createError( | ||
| 'invalid_logical_operand', | ||
| `Logical operator requires bool operands, got '${chk.formatType(leftType)}'`, | ||
@@ -191,3 +203,4 @@ ast | ||
| if (!rightType.isDynOrBool()) { | ||
| throw new chk.Error( | ||
| throw chk.createError( | ||
| 'invalid_logical_operand', | ||
| `Logical operator requires bool operands, got '${chk.formatType(rightType)}'`, | ||
@@ -212,3 +225,9 @@ ast | ||
| const overload = ast.candidates.findUnaryOverload(right) | ||
| if (!overload) throw new chk.Error(`no such overload: ${op[0]}${chk.formatType(right)}`, ast) | ||
| if (!overload) { | ||
| throw chk.createError( | ||
| 'no_such_overload', | ||
| `no such overload: ${op[0]}${chk.formatType(right)}`, | ||
| ast | ||
| ) | ||
| } | ||
@@ -223,3 +242,3 @@ ast.handle = maybeAsync(ast.args, false, overload.handler) | ||
| if (overload) return overload.handler(left) | ||
| throw new ev.Error(`no such overload: ${ast.op[0]}${leftType}`, ast) | ||
| throw ev.createError('no_such_overload', `no such overload: ${ast.op[0]}${leftType}`, ast) | ||
| } | ||
@@ -248,3 +267,4 @@ | ||
| throw new chk.Error( | ||
| throw chk.createError( | ||
| 'no_such_overload', | ||
| `no such overload: ${chk.formatType(left)} ${op} ${chk.formatType(right)}`, | ||
@@ -268,5 +288,23 @@ ast | ||
| if (overload) return overload.handler(left, right, ast, ev) | ||
| throw new ev.Error(`no such overload: ${leftType} ${ast.op} ${rightType}`, ast) | ||
| throw ev.createError( | ||
| 'no_such_overload', | ||
| `no such overload: ${leftType} ${ast.op} ${rightType}`, | ||
| ast | ||
| ) | ||
| } | ||
| function callFunctionHandler(handler, ev, args, ast) { | ||
| try { | ||
| const result = handler.apply(ev, args) | ||
| if (result instanceof Promise) { | ||
| return result.catch((error) => { | ||
| throw attachErrorAst(error, ast) | ||
| }) | ||
| } | ||
| return result | ||
| } catch (error) { | ||
| throw attachErrorAst(error, ast) | ||
| } | ||
| } | ||
| function callFn(args, ast, ev) { | ||
@@ -279,4 +317,5 @@ const argAst = ast.args[1] | ||
| const decl = ast.candidates.findFunction(types) | ||
| if (decl) return decl.handler.apply(ev, args) | ||
| throw new ev.Error( | ||
| if (decl) return callFunctionHandler(decl.handler, ev, args, ast) | ||
| throw ev.createError( | ||
| 'no_matching_overload', | ||
| `found no matching overload for '${ast.args[0]}(${types | ||
@@ -297,5 +336,6 @@ .map((t) => t.unwrappedType) | ||
| const decl = ast.candidates.findFunction(types, receiverType) | ||
| if (decl) return decl.handler.apply(ev, args) | ||
| if (decl) return callFunctionHandler(decl.handler, ev, args, ast) | ||
| throw new ev.Error( | ||
| throw ev.createError( | ||
| 'no_matching_overload', | ||
| `found no matching overload for '${receiverType.type}.${ast.args[0]}(${types | ||
@@ -332,3 +372,4 @@ .map((t) => t.unwrappedType) | ||
| if (iterType.kind === 'map') return iterType.keyType | ||
| throw new chk.Error( | ||
| throw chk.createError( | ||
| 'invalid_comprehension_range', | ||
| `Expression of type '${chk.formatType( | ||
@@ -345,3 +386,4 @@ iterType | ||
| if (coll && typeof coll === 'object') return objKeys(coll) | ||
| throw new ev.Error( | ||
| throw ev.createError( | ||
| 'invalid_comprehension_range', | ||
| `Expression of type '${ev.debugType( | ||
@@ -463,3 +505,3 @@ coll | ||
| } | ||
| throw new ev.Error(`No such key: ${right}`, ast) | ||
| throw ev.createError('no_such_key', `No such key: ${right}`, ast) | ||
| } | ||
@@ -482,3 +524,3 @@ | ||
| const variable = ctx.getVariable(ast.args) | ||
| if (!variable) throw new chk.Error(`Unknown variable: ${ast.args}`, ast) | ||
| if (!variable) throw chk.createError('unknown_variable', `Unknown variable: ${ast.args}`, ast) | ||
| if (variable.constant) { | ||
@@ -528,3 +570,4 @@ const alternate = ast.clone(OPERATORS.value, variable.value) | ||
| if (!decl) { | ||
| throw new chk.Error( | ||
| throw chk.createError( | ||
| 'no_matching_overload', | ||
| `found no matching overload for '${functionName}(${chk.formatTypeList(argTypes)})'`, | ||
@@ -537,3 +580,3 @@ ast | ||
| ? callFn | ||
| : (decl.handler.__handle ??= (l, _ast, e) => decl.handler.apply(e, l)) | ||
| : (decl.handler.__handle ??= (l, _ast, e) => callFunctionHandler(decl.handler, e, l, _ast)) | ||
@@ -565,3 +608,4 @@ ast.handle = maybeAsync(args, false, handle) | ||
| if (!decl) { | ||
| throw new chk.Error( | ||
| throw chk.createError( | ||
| 'no_matching_overload', | ||
| `found no matching overload for '${receiverType.type}.${methodName}(${chk.formatTypeList( | ||
@@ -575,4 +619,4 @@ argTypes | ||
| if (!receiverType.hasPlaceholderType && !argTypes.some((t) => t.hasDynType)) { | ||
| const handler = decl.handler | ||
| const handle = (handler.__handle ??= (a, ev) => handler.apply(ev, a)) | ||
| const fn = decl.handler | ||
| const handle = (fn.__handle ??= (a, ev, _ast) => callFunctionHandler(fn, ev, a, _ast)) | ||
| ast.handle = maybeAsync(ast.receiverWithArgs, false, handle) | ||
@@ -596,3 +640,4 @@ } | ||
| for (let i = 1; i < arrLen; i++) valueType = check(chk, ctx, valueType, arr[i], 0) | ||
| for (let i = 1; i < arrLen; i++) | ||
| valueType = check(chk, ctx, valueType, arr[i], 'heterogeneous_list_element') | ||
| return chk.registry.getListType(valueType) | ||
@@ -615,4 +660,4 @@ }, | ||
| const e = arr[i] | ||
| keyType = check(chk, ctx, keyType, e[0], 1) | ||
| valueType = check(chk, ctx, valueType, e[1], 2) | ||
| keyType = check(chk, ctx, keyType, e[0], 'heterogeneous_map_key') | ||
| valueType = check(chk, ctx, valueType, e[1], 'heterogeneous_map_value') | ||
| } | ||
@@ -701,3 +746,4 @@ return chk.registry.getMapType(keyType, valueType) | ||
| if (!condType.isDynOrBool()) { | ||
| throw new chk.Error( | ||
| throw chk.createError( | ||
| 'invalid_condition_type', | ||
| `${condast.meta.label || 'Ternary condition must be bool'}, got '${chk.formatType(condType)}'`, | ||
@@ -715,3 +761,4 @@ condast | ||
| throw new chk.Error( | ||
| throw chk.createError( | ||
| 'incompatible_ternary_branches', | ||
| `Ternary branches must have the same type, got '${chk.formatType( | ||
@@ -718,0 +765,0 @@ leftType |
+14
-7
@@ -1,2 +0,2 @@ | ||
| import {EvaluationError} from './errors.js' | ||
| import {evaluationError} from './errors.js' | ||
@@ -24,3 +24,5 @@ export class Optional { | ||
| value() { | ||
| if (this.#value === undefined) throw new EvaluationError('Optional value is not present') | ||
| if (this.#value === undefined) { | ||
| throw evaluationError('optional_value_missing', 'Optional value is not present') | ||
| } | ||
| return this.#value | ||
@@ -32,3 +34,6 @@ } | ||
| if (optional instanceof Optional) return optional | ||
| throw new EvaluationError('Optional.or must be called with an Optional argument') | ||
| throw evaluationError( | ||
| 'invalid_optional_argument', | ||
| 'Optional.or must be called with an Optional argument' | ||
| ) | ||
| } | ||
@@ -75,3 +80,3 @@ | ||
| if (value instanceof Optional) return value | ||
| throw new EvaluationError(`${description} must be optional`, ast) | ||
| throw evaluationError('optional_expected', `${description} must be optional`, ast) | ||
| } | ||
@@ -95,3 +100,3 @@ | ||
| if (type.kind === 'dyn') return checker.getType('optional') | ||
| throw new checker.Error(`${description} must be optional, got '${type}'`, node) | ||
| throw checker.createError('optional_expected', `${description} must be optional, got '${type}'`, node) | ||
| } | ||
@@ -127,3 +132,4 @@ | ||
| if (unified) return unified | ||
| throw new check.Error( | ||
| throw check.createError( | ||
| 'incompatible_argument_type', | ||
| `${macro.functionDesc} argument must be compatible type, got '${l}' and '${r}'`, | ||
@@ -159,3 +165,4 @@ macro.arg | ||
| if (unified) return unified | ||
| throw new check.Error( | ||
| throw check.createError( | ||
| 'incompatible_argument_type', | ||
| `${macro.functionDesc} argument must be compatible type, got '${l}' and '${r}'`, | ||
@@ -162,0 +169,0 @@ macro.arg |
+16
-10
| import {UnsignedInt, Duration} from './functions.js' | ||
| import {EvaluationError} from './errors.js' | ||
| import {isArray, hasOwn, objKeys} from './globals.js' | ||
| import {evaluationError} from './errors.js' | ||
| import {isArray, hasOwn, objKeys, MIN_UINT, MIN_INT, MAX_INT} from './globals.js' | ||
@@ -9,9 +9,15 @@ export function registerOverloads(registry) { | ||
| const MAX_INT = 9223372036854775807n | ||
| const MIN_INT = -9223372036854775808n | ||
| function verifyInteger(v, ast) { | ||
| if (v <= MAX_INT && v >= MIN_INT) return v | ||
| throw new EvaluationError(`integer overflow: ${v}`, ast) | ||
| throw evaluationError('numeric_overflow', `integer overflow: ${v}`, ast) | ||
| } | ||
| function throwDivisionByZero(ast) { | ||
| throw evaluationError('division_by_zero', 'division by zero', ast) | ||
| } | ||
| function throwModuloByZero(ast) { | ||
| throw evaluationError('modulo_by_zero', 'modulo by zero', ast) | ||
| } | ||
| unaryOverload('!', 'bool', (a) => !a) | ||
@@ -25,7 +31,7 @@ unaryOverload('-', 'int', (a) => -a) | ||
| binaryOverload('int', '/', 'int', (a, b, ast) => { | ||
| if (b === 0n) throw new EvaluationError('division by zero', ast) | ||
| if (b === MIN_UINT) return throwDivisionByZero(ast) | ||
| return a / b | ||
| }) | ||
| binaryOverload('int', '%', 'int', (a, b, ast) => { | ||
| if (b === 0n) throw new EvaluationError('modulo by zero', ast) | ||
| if (b === MIN_UINT) return throwModuloByZero(ast) | ||
| return a % b | ||
@@ -160,7 +166,7 @@ }) | ||
| binaryOverload('uint', '/', 'uint', (a, b, ast) => { | ||
| if (b.valueOf() === 0n) throw new EvaluationError('division by zero', ast) | ||
| if (b.valueOf() === MIN_UINT) return throwDivisionByZero(ast) | ||
| return new UnsignedInt(a.valueOf() / b.valueOf()) | ||
| }) | ||
| binaryOverload('uint', '%', 'uint', (a, b, ast) => { | ||
| if (b.valueOf() === 0n) throw new EvaluationError('modulo by zero', ast) | ||
| if (b.valueOf() === MIN_UINT) return throwModuloByZero(ast) | ||
| return new UnsignedInt(a.valueOf() % b.valueOf()) | ||
@@ -216,3 +222,3 @@ }) | ||
| } | ||
| throw new EvaluationError(`Cannot compare values of type ${typeof a}`, ast) | ||
| throw evaluationError('invalid_comparison_type', `Cannot compare values of type ${typeof a}`, ast) | ||
| } |
+206
-91
| import {UnsignedInt} from './functions.js' | ||
| import {ParseError} from './errors.js' | ||
| import {parseError} from './errors.js' | ||
| import {OPERATORS as OPS} from './operators.js' | ||
@@ -62,2 +62,12 @@ import {RESERVED, isAsync} from './globals.js' | ||
| const ESCAPE_ERRORS = { | ||
| bytes_unicode_escape: (e) => `\\${e} not allowed in bytes literals`, | ||
| invalid_unicode_escape: (e) => `Invalid Unicode escape: \\${e}`, | ||
| invalid_unicode_surrogate: (e) => `Invalid Unicode surrogate: \\${e}`, | ||
| invalid_hex_escape: (e) => `Invalid hex escape: \\${e}`, | ||
| invalid_octal_escape: () => 'Octal escape must be 3 digits', | ||
| octal_escape_out_of_range: (e) => `Octal escape out of range: \\${e}`, | ||
| invalid_escape_sequence: (e) => `Invalid escape sequence: \\${e}` | ||
| } | ||
| const STRING_ESCAPES = { | ||
@@ -80,10 +90,15 @@ '\\': '\\', | ||
| #meta | ||
| constructor(input, pos, op, args) { | ||
| this.#meta = {input, pos, evaluate: op.evaluate, check: op.check} | ||
| #input | ||
| constructor(input, pos, start, end, op, args) { | ||
| this.#meta = {check: op.check, evaluate: op.evaluate} | ||
| this.#input = input | ||
| this.op = op.name | ||
| this.args = args | ||
| this.pos = pos | ||
| this.start = start | ||
| this.end = end | ||
| } | ||
| clone(op, args) { | ||
| return new ASTNode(this.#meta.input, this.#meta.pos, op, args) | ||
| return new ASTNode(this.#input, this.pos, this.start, this.end, op, args) | ||
| } | ||
@@ -95,2 +110,6 @@ | ||
| get input() { | ||
| return this.#input | ||
| } | ||
| #computeIsAsync() { | ||
@@ -168,10 +187,6 @@ const ast = this.#meta.alternate ?? this | ||
| get input() { | ||
| return this.#meta.input | ||
| get range() { | ||
| return {start: this.start, end: this.end} | ||
| } | ||
| get pos() { | ||
| return this.#meta.pos | ||
| } | ||
| toOldStructure() { | ||
@@ -295,3 +310,8 @@ const args = Array.isArray(this.args) ? this.args : [this.args] | ||
| throw new ParseError(`Unexpected character: ${ch}`, {pos, input}) | ||
| throw parseError('unexpected_character', `Unexpected character: ${ch}`, { | ||
| pos, | ||
| start: pos, | ||
| end: pos + 1, | ||
| input | ||
| }) | ||
| } | ||
@@ -309,3 +329,8 @@ } | ||
| if (Number.isFinite(value)) return this.token(start, TOKEN.NUMBER, value) | ||
| throw new ParseError(`Invalid number: ${value}`, {pos: start, input: this.input}) | ||
| throw parseError('invalid_number', `Invalid number: ${value}`, { | ||
| pos: start, | ||
| start, | ||
| end, | ||
| input: this.input | ||
| }) | ||
| } | ||
@@ -326,6 +351,7 @@ | ||
| throw new ParseError(isHex ? `Invalid hex integer: ${string}` : `Invalid integer: ${string}`, { | ||
| pos: start, | ||
| input: this.input | ||
| }) | ||
| throw parseError( | ||
| isHex ? 'invalid_hex_integer' : 'invalid_integer', | ||
| isHex ? `Invalid hex integer: ${string}` : `Invalid integer: ${string}`, | ||
| {pos: start, start, end: this.pos, input: this.input} | ||
| ) | ||
| } | ||
@@ -345,3 +371,9 @@ | ||
| pos = this._readDigits(input, length, pos) | ||
| if (start === pos) throw new ParseError('Invalid exponent', {pos, input}) | ||
| if (start === pos) | ||
| throw parseError('invalid_exponent', 'Invalid exponent', { | ||
| pos, | ||
| start: pos, | ||
| end: Math.min(pos + 1, input.length), | ||
| input | ||
| }) | ||
| } | ||
@@ -379,7 +411,7 @@ return pos | ||
| _closeQuotedString(rawValue, prefix, pos) { | ||
| _closeQuotedString(rawStart, rawValue, prefix, pos) { | ||
| switch (prefix) { | ||
| case 'b': | ||
| case 'B': { | ||
| const processed = this.processEscapes(rawValue, true) | ||
| const processed = this.processEscapes(rawStart, rawValue, true) | ||
| const bytes = new Uint8Array(processed.length) | ||
@@ -394,3 +426,3 @@ for (let i = 0; i < processed.length; i++) bytes[i] = processed.charCodeAt(i) & 0xff | ||
| default: { | ||
| const value = this.processEscapes(rawValue, false) | ||
| const value = this.processEscapes(rawStart, rawValue, false) | ||
| return this.token(pos, TOKEN.STRING, value) | ||
@@ -409,8 +441,14 @@ } | ||
| case delimiter: | ||
| const rawValue = input.slice(start + 1, pos) | ||
| const rawStart = start + 1 | ||
| const rawValue = input.slice(rawStart, pos) | ||
| this.pos = ++pos | ||
| return this._closeQuotedString(rawValue, prefix, start) | ||
| return this._closeQuotedString(rawStart, rawValue, prefix, start) | ||
| case '\n': | ||
| case '\r': | ||
| throw new ParseError('Newlines not allowed in single-quoted strings', {pos: start, input}) | ||
| throw parseError('newline_in_string', 'Newlines not allowed in single-quoted strings', { | ||
| pos, | ||
| start: pos, | ||
| end: pos + 1, | ||
| input | ||
| }) | ||
| case '\\': | ||
@@ -421,3 +459,8 @@ pos++ | ||
| } | ||
| throw new ParseError('Unterminated string', {pos: start, input}) | ||
| throw parseError('unterminated_string', 'Unterminated string', { | ||
| pos: start, | ||
| start, | ||
| end: input.length, | ||
| input | ||
| }) | ||
| } | ||
@@ -434,5 +477,6 @@ | ||
| if (input[pos + 1] === delimiter && input[pos + 2] === delimiter) { | ||
| const rawValue = input.slice(start + 3, pos) | ||
| const rawStart = start + 3 | ||
| const rawValue = input.slice(rawStart, pos) | ||
| this.pos = pos + 3 | ||
| return this._closeQuotedString(rawValue, prefix, start) | ||
| return this._closeQuotedString(rawStart, rawValue, prefix, start) | ||
| } | ||
@@ -445,12 +489,28 @@ break | ||
| } | ||
| throw new ParseError('Unterminated triple-quoted string', {pos: start, input}) | ||
| throw parseError('unterminated_triple_quoted_string', 'Unterminated triple-quoted string', { | ||
| pos: start, | ||
| start, | ||
| end: input.length, | ||
| input | ||
| }) | ||
| } | ||
| processEscapes(str, isBytes) { | ||
| #escapeErr(code, offset, len, i, chars, extra) { | ||
| const start = offset + i | ||
| return parseError(code, ESCAPE_ERRORS[code](extra), { | ||
| input: this.input, | ||
| pos: start, | ||
| start, | ||
| end: Math.min(start + chars, offset + len) | ||
| }) | ||
| } | ||
| processEscapes(offset, str, isBytes) { | ||
| if (!str.includes('\\')) return str | ||
| const len = str.length | ||
| let result = '' | ||
| let i = 0 | ||
| while (i < str.length) { | ||
| if (str[i] !== '\\' || i + 1 >= str.length) { | ||
| while (i < len) { | ||
| if (str[i] !== '\\' || i + 1 >= len) { | ||
| result += str[i++] | ||
@@ -464,29 +524,29 @@ continue | ||
| i += 2 | ||
| } else if (next === 'u') { | ||
| if (isBytes) throw new ParseError('\\u not allowed in bytes literals') | ||
| const hex = str.substring(i + 2, (i += 6)) | ||
| if (!/^[0-9a-fA-F]{4}$/.test(hex)) throw new ParseError(`Invalid Unicode escape: \\u${hex}`) | ||
| } else if (next === 'u' || next === 'U') { | ||
| if (isBytes) throw this.#escapeErr('bytes_unicode_escape', offset, len, i, 2, next) | ||
| const hexLen = next === 'u' ? 4 : 8 | ||
| const hex = str.substring(i + 2, i + 2 + hexLen) | ||
| const c = Number.parseInt(hex, 16) | ||
| if (c >= 0xd800 && c <= 0xdfff) throw new ParseError(`Invalid Unicode surrogate: \\u${hex}`) | ||
| result += String.fromCharCode(c) | ||
| } else if (next === 'U') { | ||
| if (isBytes) throw new ParseError('\\U not allowed in bytes literals') | ||
| const hex = str.substring(i + 2, (i += 10)) | ||
| if (!/^[0-9a-fA-F]{8}$/.test(hex)) throw new ParseError(`Invalid Unicode escape: \\U${hex}`) | ||
| const c = Number.parseInt(hex, 16) | ||
| if (c > 0x10ffff) throw new ParseError(`Invalid Unicode escape: \\U${hex}`) | ||
| if (c >= 0xd800 && c <= 0xdfff) throw new ParseError(`Invalid Unicode surrogate: \\U${hex}`) | ||
| if (hex.length !== hexLen || !/^[0-9a-fA-F]+$/.test(hex) || c > 0x10ffff) | ||
| throw this.#escapeErr('invalid_unicode_escape', offset, len, i, 2 + hexLen, next + hex) | ||
| if (c >= 0xd800 && c <= 0xdfff) | ||
| throw this.#escapeErr('invalid_unicode_surrogate', offset, len, i, 2 + hexLen, next + hex) | ||
| result += String.fromCodePoint(c) | ||
| i += 2 + hexLen | ||
| } else if (next === 'x' || next === 'X') { | ||
| const h = str.substring(i + 2, (i += 4)) | ||
| if (!/^[0-9a-fA-F]{2}$/.test(h)) throw new ParseError(`Invalid hex escape: \\${next}${h}`) | ||
| const h = str.substring(i + 2, i + 4) | ||
| if (!/^[0-9a-fA-F]{2}$/.test(h)) | ||
| throw this.#escapeErr('invalid_hex_escape', offset, len, i, 4, next + h) | ||
| result += String.fromCharCode(Number.parseInt(h, 16)) | ||
| i += 4 | ||
| } else if (next >= '0' && next <= '7') { | ||
| const o = str.substring(i + 1, (i += 4)) | ||
| if (!/^[0-7]{3}$/.test(o)) throw new ParseError('Octal escape must be 3 digits') | ||
| const o = str.substring(i + 1, i + 4) | ||
| if (!/^[0-7]{3}$/.test(o)) throw this.#escapeErr('invalid_octal_escape', offset, len, i, 4) | ||
| const value = Number.parseInt(o, 8) | ||
| if (value > 0xff) throw new ParseError(`Octal escape out of range: \\${o}`) | ||
| if (value > 0xff) throw this.#escapeErr('octal_escape_out_of_range', offset, len, i, 4, o) | ||
| result += String.fromCharCode(value) | ||
| i += 4 | ||
| } else { | ||
| throw new ParseError(`Invalid escape sequence: \\${next}`) | ||
| throw this.#escapeErr('invalid_escape_sequence', offset, len, i, 2, next) | ||
| } | ||
@@ -534,4 +594,6 @@ } | ||
| #limitExceeded(limitKey, pos = this.pos) { | ||
| throw new ParseError(`Exceeded ${limitKey} (${this.limits[limitKey]})`, { | ||
| throw parseError('limit_exceeded', `Exceeded ${limitKey} (${this.limits[limitKey]})`, { | ||
| pos, | ||
| start: pos, | ||
| end: pos, | ||
| input: this.input | ||
@@ -541,4 +603,4 @@ }) | ||
| #node(pos, op, args) { | ||
| const node = new ASTNode(this.input, pos, op, args) | ||
| #node(start, end, op, args, pos = start) { | ||
| const node = new ASTNode(this.input, pos, start, end, op, args) | ||
| if (!this.astNodesRemaining--) this.#limitExceeded('maxAstNodes', pos) | ||
@@ -548,2 +610,22 @@ return node | ||
| #infixNode(op, left, right) { | ||
| return this.#node(left.start, right.end, op, [left, right]) | ||
| } | ||
| #ternaryNode(expression, consequent, alternate) { | ||
| return this.#node(expression.start, alternate.end, OPS.ternary, [ | ||
| expression, | ||
| consequent, | ||
| alternate | ||
| ]) | ||
| } | ||
| #unaryNode(pos, op, arg) { | ||
| return this.#node(pos, arg.end, op, arg) | ||
| } | ||
| #accessNode(op, left, right, end, pos = left.start) { | ||
| return this.#node(left.start, end, op, [left, right], pos) | ||
| } | ||
| #advanceToken(returnValue = this.pos) { | ||
@@ -564,5 +646,6 @@ const l = this.lexer.nextToken() | ||
| if (this.type === expectedType) return this.#advanceToken() | ||
| throw new ParseError( | ||
| throw parseError( | ||
| 'expected_token', | ||
| `Expected ${TOKEN_BY_NUMBER[expectedType]}, got ${TOKEN_BY_NUMBER[this.type]}`, | ||
| {pos: this.pos, input: this.input} | ||
| {pos: this.pos, start: this.pos, end: this.lexer.pos, input: this.input} | ||
| ) | ||
@@ -577,3 +660,5 @@ } | ||
| parse(input) { | ||
| if (typeof input !== 'string') throw new ParseError('Expression must be a string') | ||
| if (typeof input !== 'string') { | ||
| throw parseError('expression_must_be_string', 'Expression must be a string') | ||
| } | ||
| this.input = this.lexer.reset(input) | ||
@@ -587,12 +672,20 @@ this.#advanceToken() | ||
| throw new ParseError(`Unexpected character: '${this.input[this.lexer.pos - 1]}'`, { | ||
| pos: this.pos, | ||
| input: this.input | ||
| }) | ||
| throw parseError( | ||
| 'unexpected_character', | ||
| `Unexpected character: '${this.input[this.lexer.pos - 1]}'`, | ||
| { | ||
| pos: this.pos, | ||
| start: this.pos, | ||
| end: this.lexer.pos, | ||
| input: this.input | ||
| } | ||
| ) | ||
| } | ||
| #expandMacro(pos, op, args) { | ||
| const [methodName, receiver, fnArgs] = op === OPS.rcall ? args : [args[0], null, args[1]] | ||
| #expandMacro(start, end, op, args) { | ||
| const methodName = args[0] | ||
| const receiver = op === OPS.rcall ? args[1] : null | ||
| const fnArgs = op === OPS.rcall ? args[2] : args[1] | ||
| const decl = this.registry.findMacro(methodName, !!receiver, fnArgs.length) | ||
| const ast = this.#node(pos, op, args) | ||
| const ast = this.#node(start, end, op, args) | ||
| if (!decl) return ast | ||
@@ -610,3 +703,3 @@ const macro = decl.handler({ast, args: fnArgs, receiver, methodName, parser: this}) | ||
| const questionPos = this.#advanceToken() | ||
| this.#advanceToken() | ||
| const consequent = this.parseExpression() | ||
@@ -616,3 +709,3 @@ this.consume(TOKEN.COLON) | ||
| this.maxDepthRemaining++ | ||
| return this.#node(questionPos, OPS.ternary, [expr, consequent, alternate]) | ||
| return this.#ternaryNode(expr, consequent, alternate) | ||
| } | ||
@@ -623,4 +716,6 @@ | ||
| let expr = this.parseLogicalAnd() | ||
| while (this.match(TOKEN.OR)) | ||
| expr = this.#node(this.#advanceToken(), OPS['||'], [expr, this.parseLogicalAnd()]) | ||
| while (this.match(TOKEN.OR)) { | ||
| this.#advanceToken() | ||
| expr = this.#infixNode(OPS['||'], expr, this.parseLogicalAnd()) | ||
| } | ||
| return expr | ||
@@ -632,4 +727,6 @@ } | ||
| let expr = this.parseEquality() | ||
| while (this.match(TOKEN.AND)) | ||
| expr = this.#node(this.#advanceToken(), OPS['&&'], [expr, this.parseEquality()]) | ||
| while (this.match(TOKEN.AND)) { | ||
| this.#advanceToken() | ||
| expr = this.#infixNode(OPS['&&'], expr, this.parseEquality()) | ||
| } | ||
| return expr | ||
@@ -643,3 +740,4 @@ } | ||
| const op = OP_FOR_TOKEN[this.type] | ||
| expr = this.#node(this.#advanceToken(), op, [expr, this.parseRelational()]) | ||
| this.#advanceToken() | ||
| expr = this.#infixNode(op, expr, this.parseRelational()) | ||
| } | ||
@@ -660,3 +758,4 @@ return expr | ||
| const op = OP_FOR_TOKEN[this.type] | ||
| expr = this.#node(this.#advanceToken(), op, [expr, this.parseAdditive()]) | ||
| this.#advanceToken() | ||
| expr = this.#infixNode(op, expr, this.parseAdditive()) | ||
| } | ||
@@ -671,3 +770,4 @@ return expr | ||
| const op = OP_FOR_TOKEN[this.type] | ||
| expr = this.#node(this.#advanceToken(), op, [expr, this.parseMultiplicative()]) | ||
| this.#advanceToken() | ||
| expr = this.#infixNode(op, expr, this.parseMultiplicative()) | ||
| } | ||
@@ -682,3 +782,4 @@ return expr | ||
| const op = OP_FOR_TOKEN[this.type] | ||
| expr = this.#node(this.#advanceToken(), op, [expr, this.parseUnary()]) | ||
| this.#advanceToken() | ||
| expr = this.#infixNode(op, expr, this.parseUnary()) | ||
| } | ||
@@ -690,6 +791,8 @@ return expr | ||
| parseUnary() { | ||
| if (this.type === TOKEN.NOT) | ||
| return this.#node(this.#advanceToken(), OPS.unaryNot, this.parseUnary()) | ||
| if (this.type === TOKEN.MINUS) | ||
| return this.#node(this.#advanceToken(), OPS.unaryMinus, this.parseUnary()) | ||
| if (this.type === TOKEN.NOT) { | ||
| return this.#unaryNode(this.#advanceToken(), OPS.unaryNot, this.parseUnary()) | ||
| } | ||
| if (this.type === TOKEN.MINUS) { | ||
| return this.#unaryNode(this.#advanceToken(), OPS.unaryMinus, this.parseUnary()) | ||
| } | ||
| return this.parsePostfix() | ||
@@ -713,9 +816,12 @@ } | ||
| const propertyValue = this.value | ||
| const propertyPos = this.consume(TOKEN.IDENTIFIER) | ||
| const start = this.pos | ||
| const end = this.lexer.pos | ||
| this.consume(TOKEN.IDENTIFIER) | ||
| if (op === OPS.fieldAccess && this.match(TOKEN.LPAREN) && this.#advanceToken()) { | ||
| const args = this.parseArgumentList() | ||
| const closeEnd = this.lexer.pos | ||
| this.consume(TOKEN.RPAREN) | ||
| expr = this.#expandMacro(propertyPos, OPS.rcall, [propertyValue, expr, args]) | ||
| expr = this.#expandMacro(expr.start, closeEnd, OPS.rcall, [propertyValue, expr, args]) | ||
| } else { | ||
| expr = this.#node(propertyPos, op, [expr, propertyValue]) | ||
| expr = this.#accessNode(op, expr, propertyValue, end, start) | ||
| } | ||
@@ -735,4 +841,5 @@ continue | ||
| const index = this.parseExpression() | ||
| const closeEnd = this.lexer.pos | ||
| this.consume(TOKEN.RBRACKET) | ||
| expr = this.#node(bracket, op, [expr, index]) | ||
| expr = this.#accessNode(op, expr, index, closeEnd) | ||
| continue | ||
@@ -765,4 +872,6 @@ } | ||
| throw new ParseError(`Unexpected token: ${TOKEN_BY_NUMBER[this.type]}`, { | ||
| throw parseError('unexpected_token', `Unexpected token: ${TOKEN_BY_NUMBER[this.type]}`, { | ||
| pos: this.pos, | ||
| start: this.pos, | ||
| end: this.lexer.pos, | ||
| input: this.input | ||
@@ -773,3 +882,3 @@ }) | ||
| #consumeLiteral() { | ||
| return this.#advanceToken(this.#node(this.pos, OPS.value, this.value)) | ||
| return this.#advanceToken(this.#node(this.pos, this.lexer.pos, OPS.value, this.value)) | ||
| } | ||
@@ -779,6 +888,9 @@ | ||
| const value = this.value | ||
| const pos = this.consume(TOKEN.IDENTIFIER) | ||
| const end = this.lexer.pos | ||
| const start = this.consume(TOKEN.IDENTIFIER) | ||
| if (RESERVED.has(value)) { | ||
| throw new ParseError(`Reserved identifier: ${value}`, { | ||
| pos: pos, | ||
| throw parseError('reserved_identifier', `Reserved identifier: ${value}`, { | ||
| pos: start, | ||
| start, | ||
| end, | ||
| input: this.input | ||
@@ -788,7 +900,8 @@ }) | ||
| if (!this.match(TOKEN.LPAREN)) return this.#node(pos, OPS.id, value) | ||
| if (!this.match(TOKEN.LPAREN)) return this.#node(start, end, OPS.id, value) | ||
| this.#advanceToken() | ||
| const args = this.parseArgumentList() | ||
| const closeEnd = this.lexer.pos | ||
| this.consume(TOKEN.RPAREN) | ||
| return this.#expandMacro(pos, OPS.call, [value, args]) | ||
| return this.#expandMacro(start, closeEnd, OPS.call, [value, args]) | ||
| } | ||
@@ -804,3 +917,3 @@ | ||
| parseList() { | ||
| const token = this.consume(TOKEN.LBRACKET) | ||
| const start = this.consume(TOKEN.LBRACKET) | ||
| const elements = [] | ||
@@ -820,8 +933,9 @@ let remainingElements = this.limits.maxListElements | ||
| const closeEnd = this.lexer.pos | ||
| this.consume(TOKEN.RBRACKET) | ||
| return this.#node(token, OPS.list, elements) | ||
| return this.#node(start, closeEnd, OPS.list, elements) | ||
| } | ||
| parseMap() { | ||
| const token = this.consume(TOKEN.LBRACE) | ||
| const start = this.consume(TOKEN.LBRACE) | ||
| const props = [] | ||
@@ -841,4 +955,5 @@ let remainingEntries = this.limits.maxMapEntries | ||
| const closeEnd = this.lexer.pos | ||
| this.consume(TOKEN.RBRACE) | ||
| return this.#node(token, OPS.map, props) | ||
| return this.#node(start, closeEnd, OPS.map, props) | ||
| } | ||
@@ -845,0 +960,0 @@ |
+196
-32
@@ -27,3 +27,3 @@ /** | ||
| constructor(options: { | ||
| kind: 'primitive' | 'list' | 'map' | 'message' | 'enum' | ||
| kind: 'primitive' | 'list' | 'map' | 'message' | 'enum' | 'dyn' | 'optional' | 'param' | ||
| type: string | ||
@@ -36,4 +36,4 @@ name: string | ||
| /** The kind of type (primitive, list, map, message, enum). */ | ||
| kind: 'primitive' | 'list' | 'map' | 'message' | 'enum' | ||
| /** The kind of type (primitive, list, map, message, enum, dyn, optional, param). */ | ||
| kind: 'primitive' | 'list' | 'map' | 'message' | 'enum' | 'dyn' | 'optional' | 'param' | ||
@@ -55,5 +55,26 @@ /** The type name. */ | ||
| /** The underlying non-dyn type. */ | ||
| unwrappedType: TypeDeclaration | ||
| /** A wrapped dyn variant of this type. */ | ||
| wrappedType: TypeDeclaration | ||
| /** True when this declaration contains dyn anywhere in its structure. */ | ||
| hasDynType: boolean | ||
| /** True when this declaration contains placeholder type parameters. */ | ||
| hasPlaceholderType: boolean | ||
| /** Check if this type is 'dyn' or 'bool'. */ | ||
| isDynOrBool(): boolean | ||
| /** Check if this declaration represents an empty aggregate placeholder. */ | ||
| isEmpty(): boolean | ||
| /** Attempt to unify this declaration with another declaration. */ | ||
| unify(registry: Registry, other: TypeDeclaration): TypeDeclaration | null | ||
| /** Replace placeholder types using the provided bindings. */ | ||
| templated(registry: Registry, bind?: Map<string, TypeDeclaration>): TypeDeclaration | ||
| /** Convert to string representation. */ | ||
@@ -92,2 +113,3 @@ toString(): string | ||
| type: TypeDeclaration | ||
| optional: TypeDeclaration | ||
| list: TypeDeclaration | ||
@@ -109,2 +131,142 @@ 'list<dyn>': TypeDeclaration | ||
| export type RegisteredVariableType = string | TypeDeclaration | ||
| export interface RegisterVariableMetadata { | ||
| description?: string | ||
| } | ||
| export interface RegisterVariableTypeOptions extends RegisterVariableMetadata { | ||
| type: RegisteredVariableType | ||
| } | ||
| export interface RegisterVariableSchemaOptions extends RegisterVariableMetadata { | ||
| schema: ObjectSchema | ||
| } | ||
| export type RegisterVariableOptions = RegisterVariableTypeOptions | RegisterVariableSchemaOptions | ||
| export type RegisterVariableDeclaration = {name: string} & RegisterVariableOptions | ||
| export type RegisterConstantDeclaration = {name: string; value: any} & RegisterVariableOptions | ||
| export type RegisteredFunctionHandler = (...args: any[]) => any | ||
| export interface RegisteredFunctionParam { | ||
| name?: string | ||
| type?: string | ||
| description?: string | ||
| } | ||
| export interface RegisteredFunctionTypedParam extends RegisteredFunctionParam { | ||
| type: string | ||
| } | ||
| export interface RegisterFunctionMetadata { | ||
| description?: string | ||
| params?: RegisteredFunctionParam[] | ||
| async?: boolean | ||
| } | ||
| export interface RegisterFunctionOptions extends RegisterFunctionMetadata { | ||
| handler: RegisteredFunctionHandler | ||
| } | ||
| export interface RegisterFunctionWithSignature extends RegisterFunctionOptions { | ||
| signature: string | ||
| } | ||
| export interface RegisterFunctionWithName extends Omit<RegisterFunctionOptions, 'params'> { | ||
| name: string | ||
| receiverType?: string | ||
| returnType: string | ||
| params: RegisteredFunctionTypedParam[] | ||
| } | ||
| export type RegisterFunctionDeclaration = RegisterFunctionWithSignature | RegisterFunctionWithName | ||
| export type RegisteredTypeFieldDeclaration = | ||
| | string | ||
| | { | ||
| id?: number | ||
| keyType?: string | ||
| map?: boolean | ||
| repeated?: boolean | ||
| type?: string | ||
| } | ||
| export interface RegisterTypeWithCtor { | ||
| ctor: Function | ||
| fields?: Record<string, RegisteredTypeFieldDeclaration> | ||
| convert?: (value: any) => any | ||
| } | ||
| export interface RegisterTypeWithFields { | ||
| fields: Record<string, RegisteredTypeFieldDeclaration> | ||
| convert?: (value: any) => any | ||
| } | ||
| export interface RegisterTypeWithSchema { | ||
| schema: ObjectSchema | ||
| ctor?: Function | ||
| convert?: (value: any) => any | ||
| } | ||
| export type NamedTypeIdentity = {name: string; fullName?: string} | {fullName: string; name?: string} | ||
| export type RegisterTypeDeclaration = | ||
| | ({name?: string; fullName?: string} & RegisterTypeWithCtor) | ||
| | (NamedTypeIdentity & RegisterTypeWithFields) | ||
| | (NamedTypeIdentity & RegisterTypeWithSchema) | ||
| export type RegisterTypeDefinition = | ||
| | Function | ||
| | RegisterTypeWithCtor | ||
| | RegisterTypeWithFields | ||
| | RegisterTypeWithSchema | ||
| export interface RegisteredType { | ||
| name: string | ||
| typeType: Type | ||
| type: TypeDeclaration | ||
| ctor: Function | ||
| convert?: (value: any) => any | ||
| fields?: Record<string, TypeDeclaration> | ||
| } | ||
| export class VariableDeclaration { | ||
| constructor(name: string, type: TypeDeclaration, description?: string | null, value?: any) | ||
| readonly name: string | ||
| readonly type: TypeDeclaration | ||
| readonly description: string | null | ||
| readonly constant: boolean | ||
| readonly value: any | ||
| } | ||
| export interface DefinitionVariable { | ||
| name: string | ||
| description: string | null | ||
| type: string | ||
| } | ||
| export interface DefinitionFunctionParam { | ||
| name: string | ||
| type: string | ||
| description: string | null | ||
| } | ||
| export interface DefinitionFunction { | ||
| signature: string | ||
| name: string | ||
| description: string | null | ||
| receiverType: string | null | ||
| returnType: string | ||
| params: DefinitionFunctionParam[] | ||
| } | ||
| export interface DefinitionsResult { | ||
| variables: DefinitionVariable[] | ||
| functions: DefinitionFunction[] | ||
| } | ||
| /** | ||
@@ -123,13 +285,13 @@ * Registry for managing function overloads, operator overloads, and type mappings. | ||
| * @param signature - Function signature in format 'name(type1, type2): returnType' or 'Type.method(args): returnType' | ||
| * @param handlerOrOptions - Either the function implementation or an options object with handler and optional typeCheck | ||
| * @param handler - The function implementation | ||
| * @param opts - Optional metadata such as descriptions, param docs, and async hints | ||
| * Supports signature-based registration as well as a single declaration object. | ||
| */ | ||
| registerFunctionOverload( | ||
| signature: string, | ||
| handlerOrOptions: | ||
| | ((...args: any[]) => any) | ||
| | { | ||
| handler: (...args: any[]) => any | ||
| typeCheck?: (checker: any, receiverType: string, args: any[]) => string | ||
| } | ||
| handler: RegisteredFunctionHandler, | ||
| opts?: RegisterFunctionMetadata | ||
| ): void | ||
| registerFunctionOverload(signature: string, options: RegisterFunctionOptions): void | ||
| registerFunctionOverload(definition: RegisterFunctionDeclaration): void | ||
@@ -148,11 +310,9 @@ /** | ||
| * @param typename - The name of the type | ||
| * @param definition - Either a constructor function or an object with ctor/fields/convert | ||
| * @param definition - A constructor function or registration object with ctor, fields, schema, and/or convert | ||
| */ | ||
| registerType( | ||
| typename: string, | ||
| definition: | ||
| | Function | ||
| | {ctor: Function; fields?: Record<string, string>; convert?: (value: any) => any} | ||
| | {fields: Record<string, string>; convert?: (value: any) => any} | ||
| ): void | ||
| definition: RegisterTypeDefinition | ||
| ): RegisteredType | ||
| registerType(definition: RegisterTypeDeclaration): RegisteredType | ||
@@ -170,15 +330,16 @@ /** | ||
| * via `registerType` with runtime conversion support. | ||
| * @param name - The variable name | ||
| * @param type - The variable type name, declaration, or options with schema | ||
| * Supports `name + type`, `name + {type|schema}`, and a single declaration object. | ||
| */ | ||
| registerVariable( | ||
| name: string, | ||
| type: | ||
| | string | ||
| | TypeDeclaration | ||
| | {type: string; description?: string} | ||
| | {schema: ObjectSchema; description?: string} | ||
| ): this | ||
| registerVariable(name: string, type: RegisteredVariableType, opts?: RegisterVariableMetadata): this | ||
| registerVariable(name: string, options: RegisterVariableOptions): this | ||
| registerVariable(definition: RegisterVariableDeclaration): this | ||
| /** | ||
| * Register a constant value that is always available without requiring evaluation context. | ||
| * Supports `name + type + value` and a single declaration object. | ||
| */ | ||
| registerConstant(name: string, type: RegisteredVariableType, value: any): this | ||
| registerConstant(definition: RegisterConstantDeclaration): this | ||
| /** | ||
| * Register a unary operator overload. | ||
@@ -214,10 +375,13 @@ * @param op - The operator symbol ('-' or '!') | ||
| /** Read back user-facing variable/function definitions. */ | ||
| getDefinitions(): DefinitionsResult | ||
| /** Registered object types keyed by CEL typename. */ | ||
| readonly objectTypes: Map<string, any> | ||
| readonly objectTypes: Map<string, RegisteredType> | ||
| /** Map of constructors to their registered type metadata. */ | ||
| readonly objectTypesByConstructor: Map<Function | undefined, any> | ||
| readonly objectTypesByConstructor: Map<Function | undefined, RegisteredType> | ||
| /** Registered variables and their type declarations. */ | ||
| readonly variables: Map<string, TypeDeclaration> | ||
| readonly variables: Map<string, VariableDeclaration> | ||
@@ -232,8 +396,8 @@ /** Whether optional types/functions are enabled for this registry. */ | ||
| export interface RegistryOptions { | ||
| objectTypes?: Map<string, any> | ||
| objectTypesByConstructor?: Map<Function | undefined, any> | ||
| objectTypes?: Map<string, RegisteredType> | ||
| objectTypesByConstructor?: Map<Function | undefined, RegisteredType> | ||
| functionDeclarations?: Map<string, any> | ||
| operatorDeclarations?: Map<string, any> | ||
| typeDeclarations?: Map<string, TypeDeclaration> | ||
| variables?: Map<string, TypeDeclaration> | ||
| variables?: Map<string, VariableDeclaration> | ||
| unlistedVariablesAreDyn?: boolean | ||
@@ -240,0 +404,0 @@ enableOptionalTypes?: boolean |
+23
-10
@@ -1,2 +0,2 @@ | ||
| import {EvaluationError} from './errors.js' | ||
| import {EvaluationError, evaluationError} from './errors.js' | ||
| import {UnsignedInt} from './functions.js' | ||
@@ -188,3 +188,4 @@ import {Optional, OPTIONAL_NONE, toggleOptionalTypes} from './optional.js' | ||
| if (type.matchesValueType(value, ev)) return value | ||
| throw new EvaluationError( | ||
| throw evaluationError( | ||
| 'field_type_mismatch', | ||
| `Field '${key}' is not of type '${type}', got '${ev.debugType(value)}'`, | ||
@@ -201,3 +202,4 @@ ast | ||
| throw new EvaluationError( | ||
| throw evaluationError( | ||
| 'field_type_mismatch', | ||
| `Field '${key}' is not of type '${this.valueType}', got '${ev.debugType(value)}'`, | ||
@@ -229,3 +231,4 @@ ast | ||
| if (!obj) return | ||
| throw new EvaluationError( | ||
| throw evaluationError( | ||
| 'index_out_of_bounds', | ||
| `No such key: index out of bounds, index ${key} ${ | ||
@@ -240,3 +243,4 @@ key < 0 ? '< 0' : `>= size ${obj.length || obj.size}` | ||
| throw new EvaluationError( | ||
| throw evaluationError( | ||
| 'list_item_type_mismatch', | ||
| `List item with index '${key}' is not of type '${this.valueType}', got '${ev.debugType(value)}'`, | ||
@@ -252,3 +256,3 @@ ast | ||
| if (v !== undefined) return v | ||
| throw new EvaluationError(`No such key: ${key}`, ast) | ||
| throw evaluationError('no_such_key', `No such key: ${key}`, ast) | ||
| } | ||
@@ -1360,3 +1364,5 @@ | ||
| if (context === undefined || context === null) return | ||
| if (typeof context !== 'object') throw new EvaluationError('Context must be an object') | ||
| if (typeof context !== 'object') { | ||
| throw evaluationError('invalid_context', 'Context must be an object') | ||
| } | ||
| if (context instanceof Map) this.#contextMap = context | ||
@@ -1382,3 +1388,3 @@ else this.#contextObj = context | ||
| const v = this.getValue(ast.args) | ||
| if (v === undefined) throw new ev.Error(`Unknown variable: ${ast.args}`, ast) | ||
| if (v === undefined) throw ev.createError('unknown_variable', `Unknown variable: ${ast.args}`, ast) | ||
| if (ast.checkedType.matchesValueType(v, ev)) return v | ||
@@ -1394,3 +1400,7 @@ | ||
| throw new ev.Error(`Variable '${ast.args}' is not of type '${type}', got '${valueType}'`, ast) | ||
| throw ev.createError( | ||
| 'variable_type_mismatch', | ||
| `Variable '${ast.args}' is not of type '${type}', got '${valueType}'`, | ||
| ast | ||
| ) | ||
| } | ||
@@ -1436,3 +1446,6 @@ | ||
| throw new ev.Error(`Variable '${this.iterVar}' is not of type '${type}', got '${valueType}'`) | ||
| throw ev.createError( | ||
| 'variable_type_mismatch', | ||
| `Variable '${this.iterVar}' is not of type '${type}', got '${valueType}'` | ||
| ) | ||
| } | ||
@@ -1439,0 +1452,0 @@ |
@@ -1,1 +0,8 @@ | ||
| export {serialize, ASTNode} from './index' | ||
| import type {ASTNode} from './index.js' | ||
| export function serialize(ast: ASTNode): string | ||
| declare const serializeDefault: typeof serialize | ||
| export type {ASTNode} from './index.js' | ||
| export default serializeDefault |
+12
-7
@@ -1,2 +0,2 @@ | ||
| import {TypeError, EvaluationError} from './errors.js' | ||
| import {attachErrorAst, evaluationError, typeError} from './errors.js' | ||
| import {Base} from './operators.js' | ||
@@ -16,3 +16,3 @@ const toDynTypeBinding = new Map().set('A', 'dyn').set('T', 'dyn').set('K', 'dyn').set('V', 'dyn') | ||
| super(opts) | ||
| this.Error = isEvaluating ? EvaluationError : TypeError | ||
| this.createError = isEvaluating ? evaluationError : typeError | ||
| } | ||
@@ -27,3 +27,7 @@ | ||
| check(ast, ctx) { | ||
| return (ast.checkedType ??= ast.check(this, ast, ctx)) | ||
| try { | ||
| return (ast.checkedType ??= ast.check(this, ast, ctx)) | ||
| } catch (error) { | ||
| throw attachErrorAst(error, ast) | ||
| } | ||
| } | ||
@@ -40,3 +44,3 @@ | ||
| if (indexTypeName === 'int' || indexTypeName === 'dyn') return leftType.valueType | ||
| throw new this.Error(`List index must be int, got '${indexTypeName}'`, ast) | ||
| throw this.createError('invalid_index_type', `List index must be int, got '${indexTypeName}'`, ast) | ||
| } | ||
@@ -49,3 +53,4 @@ | ||
| if (!(indexTypeName === 'string' || indexTypeName === 'dyn')) { | ||
| throw new this.Error( | ||
| throw this.createError( | ||
| 'invalid_index_type', | ||
| `Cannot index type '${leftType.name}' with type '${indexTypeName}'`, | ||
@@ -66,3 +71,3 @@ ast | ||
| if (allowMissingField) return this.dynType | ||
| throw new this.Error(`No such key: ${keyName}`, ast) | ||
| throw this.createError('no_such_key', `No such key: ${keyName}`, ast) | ||
| } | ||
@@ -74,3 +79,3 @@ } | ||
| // No other types support indexing/property access | ||
| throw new this.Error(`Cannot index type '${this.formatType(leftType)}'`, ast) | ||
| throw this.createError('cannot_index_type', `Cannot index type '${this.formatType(leftType)}'`, ast) | ||
| } | ||
@@ -77,0 +82,0 @@ |
+6
-6
| { | ||
| "name": "@marcbachmann/cel-js", | ||
| "version": "7.5.3", | ||
| "version": "7.6.0", | ||
| "description": "A lightweight Common Expression Language (CEL) implementation in JavaScript with zero dependencies", | ||
@@ -72,12 +72,12 @@ "keywords": [ | ||
| "service": "drone", | ||
| "commit": "7497170f8567f37a50d9428243fcc7140c02c2a2", | ||
| "build": "256", | ||
| "buildUrl": "https://drone.livingdocs.io/marcbachmann/cel-js/256", | ||
| "commit": "d600a93110f1bfc63833e39a84de3086b382879b", | ||
| "build": "261", | ||
| "buildUrl": "https://drone.livingdocs.io/marcbachmann/cel-js/261", | ||
| "branch": "main", | ||
| "jobUrl": "https://drone.livingdocs.io/marcbachmann/cel-js/256", | ||
| "jobUrl": "https://drone.livingdocs.io/marcbachmann/cel-js/261", | ||
| "isPr": false, | ||
| "slug": "marcbachmann/cel-js", | ||
| "root": "/drone/src", | ||
| "date": "2026-03-10T23:23:14.229Z" | ||
| "date": "2026-03-26T01:26:14.342Z" | ||
| } | ||
| } |
+7
-5
@@ -607,3 +607,3 @@ # @marcbachmann/cel-js [](https://www.npmjs.com/package/@marcbachmann/cel-js) [](https://opensource.org/licenses/MIT) | ||
| ```javascript | ||
| import {evaluate, ParseError, EvaluationError, TypeError} from '@marcbachmann/cel-js' | ||
| import {Environment, evaluate, ParseError, EvaluationError, TypeError} from '@marcbachmann/cel-js' | ||
@@ -614,5 +614,6 @@ try { | ||
| if (error instanceof ParseError) { | ||
| console.error('Syntax error:', error.message) | ||
| console.error('Syntax error:', error.code, error.range, error.summary) | ||
| console.error(error.message) // Includes source highlighting for humans | ||
| } else if (error instanceof EvaluationError) { | ||
| console.error('Runtime error:', error.message) | ||
| console.error('Runtime error:', error.code, error.range, error.summary) | ||
| } | ||
@@ -624,4 +625,5 @@ } | ||
| const result = env.check('x + "string"') | ||
| if (!result.valid && result.error instanceof TypeError) { | ||
| console.error('Type error:', result.error.message) | ||
| if (!result.valid) { | ||
| const error = result.error | ||
| console.error('Type error:', error.code, error.range) | ||
| } | ||
@@ -628,0 +630,0 @@ ``` |
227365
8.77%5595
10.84%829
0.24%