From cc85020c81517109fd8ec74df8ecad3e2006cc1e Mon Sep 17 00:00:00 2001 From: samueltlg Date: Thu, 9 Apr 2026 23:57:28 +0100 Subject: [PATCH 1/2] feat: 'assertIsFunction' / 'getOp' utilities In disuse: but set for employment particularly in test-files --- .../boxed-expression/type-guards.ts | 11 ++++++++ src/compute-engine/boxed-expression/utils.ts | 28 ++++++++++++++++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/compute-engine/boxed-expression/type-guards.ts b/src/compute-engine/boxed-expression/type-guards.ts index d3e49456..b0001dfb 100644 --- a/src/compute-engine/boxed-expression/type-guards.ts +++ b/src/compute-engine/boxed-expression/type-guards.ts @@ -54,6 +54,17 @@ export function isFunction( ); } +export function assertIsFunction( + expr: Expression | null | undefined, + operator?: string +): asserts expr is Expression & FunctionInterface { + if (!isFunction(expr, operator)) { + throw new Error( + `Expected function${operator ? ` with operator ${operator}` : ''}` + ); + } +} + export function isString( expr: Expression | null | undefined ): expr is Expression & StringInterface { diff --git a/src/compute-engine/boxed-expression/utils.ts b/src/compute-engine/boxed-expression/utils.ts index 99de367b..0ceb7f0d 100644 --- a/src/compute-engine/boxed-expression/utils.ts +++ b/src/compute-engine/boxed-expression/utils.ts @@ -17,7 +17,13 @@ import { NumericValue } from '../numeric-value/types'; import { _BoxedOperatorDefinition } from './boxed-operator-definition'; import { _BoxedValueDefinition } from './boxed-value-definition'; import { _BoxedExpression } from './abstract-boxed-expression'; -import { isNumber, isFunction, isSymbol, numericValue } from './type-guards'; +import { + isNumber, + isFunction, + isSymbol, + numericValue, + assertIsFunction, +} from './type-guards'; /** * Check if an expression contains symbolic transcendental functions of constants @@ -452,3 +458,23 @@ export function placeholderDef( value: new _BoxedValueDefinition(ce, name, { type: 'function' }), }; } +/** + * Get nth (1-based) operand of *expr*; or `null` if this does not exist (or expr is not a + * function). + * Further, if assert is true (**default**: false), will throw if expr. is not a function. + * + * + * + */ +export function getOp( + expr: Expression, + index: number, + assert: boolean = false +): Expression | null { + if (assert) assertIsFunction(expr); + else if (!isFunction(expr)) return null; + + return expr.ops[index - 1] || null; +} From e2e4e0edb1f55f699be71fe364fc62dbfcadd9c1 Mon Sep 17 00:00:00 2001 From: samueltlg Date: Sat, 11 Apr 2026 02:10:23 +0100 Subject: [PATCH 2/2] Feat: 'Expression.transform()' 'transform()' is a wrapper around 'replace()' - always acting recursively - and offers an ergonomic means to match sub-expression targets and apply common fundamental operations/transformations in an expression 'tree', without the need for custom 'Rule' logic / workarounds. Change includes tests. - The typing of 'TransformOptions' has necessitated that 'SimplifyOptions' be shifted from its previous home of 'types-definitions(.ts)', to 'types-kernel-evaluation.ts'. It has also been made generic over Expr/SemiExpr/CE, in a similar way as for its sibling types (such as 'RuleReplaceFunction'). --- .../abstract-boxed-expression.ts | 6 + .../boxed-expression/transform.ts | 179 ++++++++++++++++++ src/compute-engine/global-types.ts | 1 - src/compute-engine/types-definitions.ts | 46 ----- src/compute-engine/types-evaluation.ts | 17 ++ src/compute-engine/types-expression.ts | 87 ++++++++- src/compute-engine/types-kernel-evaluation.ts | 173 ++++++++++++++++- .../types-kernel-serialization.ts | 2 + 8 files changed, 454 insertions(+), 57 deletions(-) create mode 100644 src/compute-engine/boxed-expression/transform.ts diff --git a/src/compute-engine/boxed-expression/abstract-boxed-expression.ts b/src/compute-engine/boxed-expression/abstract-boxed-expression.ts index a7ea5ba4..04fb75fa 100644 --- a/src/compute-engine/boxed-expression/abstract-boxed-expression.ts +++ b/src/compute-engine/boxed-expression/abstract-boxed-expression.ts @@ -25,6 +25,7 @@ import type { JsonSerializationOptions, PatternMatchOptions, SimplifyOptions, + TransformOptions, IComputeEngine as ComputeEngine, Scope, Tensor, @@ -38,6 +39,7 @@ import { toAsciiMath } from './ascii-math'; // Dynamic import for serializeJson to avoid circular dependency import { cmp, eq, same } from './compare'; import { CancellationError } from '../../common/interruptible'; +import { transform } from './transform'; import { isSymbol, isString, isNumber, isFunction } from './type-guards'; import { extractIntervalBounds } from './inequality-bounds'; @@ -822,6 +824,10 @@ export abstract class _BoxedExpression implements Expression { return null; } + transform(options: TransformOptions): Expression | null { + return transform(this, options); + } + has(_v: string | string[]): boolean { return false; } diff --git a/src/compute-engine/boxed-expression/transform.ts b/src/compute-engine/boxed-expression/transform.ts new file mode 100644 index 00000000..74f5d8c7 --- /dev/null +++ b/src/compute-engine/boxed-expression/transform.ts @@ -0,0 +1,179 @@ +import type { + EvaluateOptions, + Expression, + ExpressionInput, + FormOption, + ReplaceOptions, + Rule, + RuleConditionFunction, + RuleFunction, + RuleReplaceFunction, + TransformOptions, +} from '../global-types'; +import type { LatexString } from '../types'; + +export function transform( + expr: Expression, + options: TransformOptions +): Expression | null { + const { engine: ce } = expr; + const { type, match } = options; + let { targets } = options; + + // In absence of any matching spec., set the target as this *expr* + if (match === undefined && targets === undefined) targets = expr; + if (match !== undefined && targets !== undefined) + throw new Error('Cannot specify both `match` and `targets`'); + + // if (options.type === 'replace') options.replace + + /* + * All transformations take place through the match->replace mechanism of 'replace()', using a single rule + * Bundle the components to construct the rule. + */ + let replace: LatexString | Expression | RuleReplaceFunction | RuleFunction; + + // First, generate the 'replace' component; dependent upon transformation + // ------------------------------ + + switch (type) { + case 'replace': + if (options.replace === undefined) + throw new Error( + `Expected 'replace' option for transformation 'replace'` + ); + // @todo: ensure wrapped in a 'RuleFunction' for consistency? + replace = options.replace; + break; + + case 'structural': + replace = ((expr) => + expr.isStructural ? undefined : expr.structural) satisfies RuleFunction; + break; + + case 'canonical': + // 'canonical' must have a degree: i.e. either 'true' or 'CanonicalForm | CanonicalForm[]' + if (!options.canonical) + throw new Error( + `Expected 'canonical' option for transformation 'canonical'` + ); + replace = ((expr) => + expr.isCanonical + ? undefined + : ce.expr(expr, { + form: + options.canonical === true + ? 'canonical' + : options.canonical /* CanonicalForm */, + })) satisfies RuleFunction; + break; + + case 'evaluate': + case 'N': + const evalOptions: Partial = { + ...(options.evalOptions ?? {}), + numericApproximation: type === 'N' ? true : false, + }; + replace = ((expr) => { + const result = expr.canonical.evaluate(evalOptions); + if (result.isSame(expr)) return undefined; + return result; + }) satisfies RuleFunction; + break; + case 'simplify': + replace = ((expr) => { + const result = expr.simplify(options.simplifyOptions); + if (result.isSame(expr)) return undefined; + return result; + }) satisfies RuleFunction; + break; + default: + throw new TypeError(`Unknown transform type: '${type}'`); + } + + /* + * Build the rule (and 'match' component based on strategy). + * + */ + let rule: Rule; + // The only case where recursivity is _not_ to apply. + const directOnly = + targets && + (targets === expr || + (Array.isArray(targets) && targets.length === 1 && targets[0] === expr)); + const replaceOptions: Partial = { + recursive: directOnly ? false : true, + direction: options.direction, + // @note: do not supply 'form' here, since this will undesirably apply to the entire input. + // Instead, apply this in the replacement `RuleFunction` + }; + + // For select transformations, a 'form' definition may be supplied. + // (Notably, all others - with exception of 'structural' - by definition produce canonical + // (output)) + if (type === 'replace') replaceOptions.form = options.form; + + // Standard pattern-matching route + // ----------------------------------- + if (match !== undefined) { + let pattern: LatexString | ExpressionInput; + let condition: LatexString | RuleConditionFunction | undefined; + + // Pattern bundled with match-options/condition + if (typeof match === 'object' && 'pattern' in match) { + if (match.useVariations) + replaceOptions.useVariations = match.useVariations; + if (match.matchPermutations) + replaceOptions.matchPermutations = match.matchPermutations; + // @fix?: 'matchMissingTerms' is not currently utilized in a 'replace()' context (i.e., is not + // forwarded for internal matching). + // if (match.matchMissingTerms) + // replaceOptions.matchMissingTerms = match.matchMissingTerms; + + pattern = match.pattern; + condition = match.condition; + } else { + // Str + pattern = match; + } + + rule = { + match: pattern, + replace, + condition, + }; + } else { + // Targeted transformation ('targets') + // ----------------------------------- + // (In contrast to 'match', permit allow exact/referential expression-based, and predicate-based + // matching) + const replacementForm: FormOption = replaceOptions.form ?? 'canonical'; // For all + // transformations where 'form' not specifiable, the output is always [to be] made canonical + + // Proceed by way of a 'RuleFunction' to emulate exact matching (with standard match patterns + // neither permiting matching via expr.-identity nor predicate.) + rule = (expr) => { + if (!directOnly) { + // Instead of matching via 'rule.match', 'targets' replicates this through either + // referential-identity and/or predicate-based matching. + if (typeof targets === 'function') { + if (!targets(expr)) return undefined; + } else { + const targetExprs = Array.isArray(targets) ? targets : [targets]; + if (!targetExprs.some((target) => target === expr)) return undefined; + } + } + + // With exception of a 'replace' transformation - which may specify its replacement + // 'replace' will take the form of a RuleFunction (according to the transformation) + return replace instanceof Function + ? (replace as RuleFunction)(expr) + : ce.expr(replace, { + form: replacementForm, + }); + }; + } + + // Transformations ultimately apply via single-Rule application with `replace()` + return expr.replace(rule, replaceOptions); +} diff --git a/src/compute-engine/global-types.ts b/src/compute-engine/global-types.ts index 74ee21b4..d9d1149c 100644 --- a/src/compute-engine/global-types.ts +++ b/src/compute-engine/global-types.ts @@ -18,7 +18,6 @@ export type { OEISOptions, OperatorDefinition, BaseDefinition, - SimplifyOptions, SymbolDefinition, SymbolDefinitions, LibraryDefinition, diff --git a/src/compute-engine/types-definitions.ts b/src/compute-engine/types-definitions.ts index 046739e7..403f2b11 100644 --- a/src/compute-engine/types-definitions.ts +++ b/src/compute-engine/types-definitions.ts @@ -9,9 +9,6 @@ import type { } from './types-expression'; import type { EvaluateOptions as KernelEvaluateOptions, - Rule as KernelRule, - BoxedRule as KernelBoxedRule, - BoxedRuleSet as KernelBoxedRuleSet, Scope as KernelScope, } from './types-kernel-evaluation'; @@ -27,9 +24,6 @@ import type { export interface ComputeEngine {} type EvaluateOptions = KernelEvaluateOptions; -type Rule = KernelRule; -type BoxedRule = KernelBoxedRule; -type BoxedRuleSet = KernelBoxedRuleSet; type Scope = KernelScope; /** @@ -517,46 +511,6 @@ export interface BaseDefinition { readonly isConstant?: boolean; } -/** Options for `Expression.simplify()` - * - * @category Boxed Expression - */ -export type SimplifyOptions = { - /** - * The set of rules to apply. If `null`, use no rules. If not provided, - * use the default simplification rules. - */ - rules?: null | Rule | ReadonlyArray | BoxedRuleSet; - - /** - * Use this cost function to determine if a simplification is worth it. - * - * If not provided, `ce.costFunction`, the cost function of the engine is - * used. - */ - costFunction?: (expr: Expression) => number; - - /** - * The simplification strategy to use. - * - * - `'default'`: Use standard simplification rules (default) - * - `'fu'`: Use the Fu algorithm for trigonometric simplification. - * This is more aggressive for trig expressions and may produce - * different results than the default strategy. - * - * **Note:** When using the `'fu'` strategy, the `costFunction` and `rules` - * options are ignored. The Fu algorithm uses its own specialized cost - * function that prioritizes minimizing the number of trigonometric - * functions. Standard simplification is applied before and after the - * Fu transformations using the engine's default rules. - * - `'trig'`: Rewrite exponentials of an imaginary argument to - * trigonometric form via Euler's formula (`e^{iθ} → cos θ + i·sin θ`), - * then simplify. This is the opt-in inverse of the default behavior, which - * keeps `e^{iθ}` in exponential form for a symbolic angle `θ`. - */ - strategy?: 'default' | 'fu' | 'trig'; -}; - /** * A table mapping symbols to their definition. * diff --git a/src/compute-engine/types-evaluation.ts b/src/compute-engine/types-evaluation.ts index c832b293..649626e2 100644 --- a/src/compute-engine/types-evaluation.ts +++ b/src/compute-engine/types-evaluation.ts @@ -8,10 +8,13 @@ import type { BoxedRule as KernelBoxedRule, BoxedRuleSet as KernelBoxedRuleSet, EvaluateOptions as KernelEvaluateOptions, + SimplifyOptions as KernelSimplifyOptions, + TransformOptions as KernelTransformOptions, EvalContext as KernelEvalContext, ExpressionMapInterface as KernelExpressionMapInterface, Rule as KernelRule, RuleConditionFunction as KernelRuleConditionFunction, + MatchConditionFunction as KernelMatchConditionFunction, RuleFunction as KernelRuleFunction, RuleReplaceFunction as KernelRuleReplaceFunction, RuleStep as KernelRuleStep, @@ -30,6 +33,18 @@ export type { AssumeResult }; */ export type EvaluateOptions = KernelEvaluateOptions; +export type SimplifyOptions = KernelSimplifyOptions< + Expression, + ExpressionInput, + ComputeEngine +>; + +export type TransformOptions = KernelTransformOptions< + Expression, + ExpressionInput, + ComputeEngine +>; + /** * Map-like interface keyed by boxed expressions. * @@ -70,6 +85,8 @@ export type RuleConditionFunction = KernelRuleConditionFunction< ComputeEngine >; +export type MatchConditionFunction = KernelMatchConditionFunction; + /** * Dynamic rule callback. * diff --git a/src/compute-engine/types-expression.ts b/src/compute-engine/types-expression.ts index 253b416b..3bd54431 100644 --- a/src/compute-engine/types-expression.ts +++ b/src/compute-engine/types-expression.ts @@ -24,10 +24,11 @@ import type { } from './types-kernel-serialization'; import type { EvaluateOptions as KernelEvaluateOptions, - BoxedRule as KernelBoxedRule, Rule as KernelRule, BoxedRuleSet as KernelBoxedRuleSet, Scope as KernelScope, + TransformOptions as KernelTransformOptions, + SimplifyOptions as KernelSimplifyOptions, } from './types-kernel-evaluation'; /** @@ -167,16 +168,20 @@ type BoxedDefinition = type Scope = KernelScope; type EvaluateOptions = KernelEvaluateOptions; +type TransformOptions = KernelTransformOptions< + Expression, + ExpressionInput, + ExpressionComputeEngine +>; +type SimplifyOptions = KernelSimplifyOptions< + Expression, + ExpressionInput, + ExpressionComputeEngine +>; + type Rule = KernelRule; -type BoxedRule = KernelBoxedRule; type BoxedRuleSet = KernelBoxedRuleSet; -type SimplifyOptions = { - rules?: null | Rule | ReadonlyArray | BoxedRuleSet; - costFunction?: (expr: Expression) => number; - strategy?: 'default' | 'fu' | 'trig'; -}; - // // ── Tensor & Compilation Types ────────────────────────────────────────── // @@ -1344,6 +1349,72 @@ export interface Expression { options?: Partial ): null | Expression; + /** + * + * Process and transform this expression *recursively* by applying one of a set of predefined + * transformations (`simplify`, `canonical`, `evaluate`, `N`, `replace`, `structural`) to matching + * (or targeted) subexpressions (or the input expression). + * + * This method is a wrapper around method `replace()` - which always applies recursively - whilst + * jointly offering an alternative, 'declarative' sytnax which also conveniently permits easier + * sub-expression targeting and common/fundamental replacement requirements, without the + * requirement of custom logic (`RuleFunctions`) and long-winded 'replace()' calls. + * + * Similarly to replace, input or sub-expressions do *not* have to be canonical (but will anyway + * be pre-made canonical for those transformations which require this as such). + * + * In addition to matching target-expressions via traditional pattern-matching (`match`), this + * method uniquely permits an alternate specification of 'targets' - permitting spec. of + * sub-expressions via either exact-matching (referential-identity), or a custom predicate. + * For each transformation, type-specific options may also be paired or required (required + * 'replace' for 'replace'; optional 'simplifyOptions' for 'simplify'). Notably, transformation + * `'replace'` uniquely permits specification of resultant 'form' (and will fall back to usual + * calculation of its value in its absence). + * + * Note that `null` is returned in various scenarios: such as where there is no match for a given pattern or + * targets; or where transformations are not applicable, or some cases where these do not produce a change, e.g.: + * - Application of 'canonical' or 'structural' to targets which are already canonical/structural. + * - Application of 'evaluate' or 'N' to targets in which the resultant value is the same as input. + * - Application of 'simplify' with no applicable rules. + * + * If no `match` or `targets` is specified, the target is taken to be the *input expression* (and + * in this sole case will not recursively). + * Only one of `match` or `targets` should be specified (if otherwise, an exception will be + * raised). + * + * ::!Caveats + * - Currently, naturally it is not possible to target engine-common expressions, such as those representing `1`, `0`, `True`, `False`, `Pi`, since these all reference the *same common expressions* upon boxing. + * + * + * + * + * + * + * + * */ + transform(options: TransformOptions): Expression | null; + /** * True if the expression includes a symbol `v` or a function operator `v`. * diff --git a/src/compute-engine/types-kernel-evaluation.ts b/src/compute-engine/types-kernel-evaluation.ts index bcfe6ee8..8c71c4cc 100644 --- a/src/compute-engine/types-kernel-evaluation.ts +++ b/src/compute-engine/types-kernel-evaluation.ts @@ -2,7 +2,12 @@ import type { MathJsonSymbol } from '../math-json'; import type { TypeReference } from '../common/type/types'; import type { BoxedType } from '../common/type/boxed-type'; import type { LatexString } from './latex-syntax/types'; -import type { BoxedSubstitution } from './types-kernel-serialization'; +import type { + BoxedSubstitution, + CanonicalOptions, + PatternMatchOptions, + ReplaceOptions, +} from './types-kernel-serialization'; /** @category Assumptions */ export interface Assumption { @@ -87,6 +92,55 @@ export type EvaluateOptions = { signal: AbortSignal; }; +/** Options for `Expression.simplify()` + * + * @category Boxed Expression + */ + +export type SimplifyOptions< + Expr = unknown, + SemiExpr = unknown, + CE = unknown, +> = { + /** + * The set of rules to apply. If `null`, use no rules. If not provided, + * use the default simplification rules. + */ + rules?: + | null + | Rule + | ReadonlyArray | Rule> + | BoxedRuleSet; + + /** + * Use this cost function to determine if a simplification is worth it. + * + * If not provided, `ce.costFunction`, the cost function of the engine is + * used. + */ + costFunction?: (expr: Expr) => number; + + /** + * The simplification strategy to use. + * + * - `'default'`: Use standard simplification rules (default) + * - `'fu'`: Use the Fu algorithm for trigonometric simplification. + * This is more aggressive for trig expressions and may produce + * different results than the default strategy. + * + * **Note:** When using the `'fu'` strategy, the `costFunction` and `rules` + * options are ignored. The Fu algorithm uses its own specialized cost + * function that prioritizes minimizing the number of trigonometric + * functions. Standard simplification is applied before and after the + * Fu transformations using the engine's default rules. + * - `'trig'`: Rewrite exponentials of an imaginary argument to + * trigonometric form via Euler's formula (`e^{iθ} → cos θ + i·sin θ`), + * then simplify. This is the opt-in inverse of the default behavior, which + * keeps `e^{iθ}` in exponential form for a symbolic angle `θ`. + */ + strategy?: 'default' | 'fu' | 'trig'; +}; + /** * Given an expression and set of wildcards, return a replacement expression. * @@ -97,7 +151,12 @@ export type RuleReplaceFunction = ( wildcards: BoxedSubstitution ) => Expr | undefined; -/** @category Rules */ +export type MatchConditionFunction = (expr: Expr) => boolean; + +/** + * Check whether the wildcards of a successful pattern match satisfy a custom condition. + * + * @category Rules */ export type RuleConditionFunction = ( wildcards: BoxedSubstitution, ce: CE @@ -252,3 +311,113 @@ export type EvalContext = { assumptions: ExpressionMapInterface; name: undefined | string; }; + +/** Kernel-options for `Expression.transform()` + * + * @category Boxed Expression + */ +export type TransformOptions< + Expr = unknown, + SemiExpr = unknown, + CE = unknown, +> = BaseTransformOptions & + ( + | ReplaceTransformOptions + | CanonicalTransformOptions + | StructuralTransformOptions + | EvaluateTransformOptions + | SimplifyTransformOptions + ); + +/** Available transformation types for `Expression.transform()` + * + * @category Boxed Expression */ +export type Transformation = + | 'structural' + | 'canonical' + | 'evaluate' + | 'N' + | 'simplify' + | 'replace'; + +interface BaseTransformOptions { + /** The specified transformation type. */ + type: string; + + /** Test candidate transform targets against a *pattern* (may contain wildcards); in contrast to + * that of `'targets'. + * + * Specify an object to specify a pattern alongside applicable {@linkcode MatchOptions} (e.g. + * `useVariations`, `matchPermutations`...), or a condition for testing wildcards + * + * Match-options assume `PatternMatchOptions` *defaults* in their absence. + * + * A *condition* may also be specified for vetting captured 'wildcards' + * + * (Mutually exclusive with `'targets'`). */ + match?: SemiExpr | LatexString | TransformMatchOptions; + + /** Specify *exact* (referential-identity) transformation targets (sub-expressions), or specify a + * predicate for matching. Mutually exclusive with 'match' (pattern-based targeting). + * + * ::Note + * The 'extended' matching routes available here are unique to *transform()* and facilitate + * convenient and more expressive matching in the context of recursive traversal.*/ + targets?: Expr | Expr[] | MatchConditionFunction; + + /** The _traversal_ direction for matching and therefore replacements (transformations) targets (**Default**: '*left-right*') */ + direction?: ReplaceOptions['direction']; +} + +/** Specify a match-condition alongside an optional condition (usually specifiable only in context of + * 'replace'), and 'transform'-applicable match options. */ +type TransformMatchOptions = { + pattern: LatexString | SemiExpr; + condition?: LatexString | RuleConditionFunction; +} & Pick< + PatternMatchOptions, + 'useVariations' | 'matchPermutations' | 'matchMissingTerms' +>; + +/** Options for standard 'replace'. + * Note that in the absence of a specified 'form', the default `expr.replace()` 'form'-computation + * procedure is used (dependent on form of input; and recursive transformation of operands). + */ +interface ReplaceTransformOptions extends Partial< + Pick +> { + type: 'replace'; + + /** Replace matched transformation targets using either a `LatexString`, `Expression`, + * `RuleFunction`, or `RuleReplaceFunction`. + * + * Beware that *wildcards* in a given replacement only apply for standard pattern-matching + * (non-available if matching with 'targets'). + */ + replace: Expr | LatexString | RuleReplaceFunction | RuleFunction; +} + +interface CanonicalTransformOptions { + type: 'canonical'; + + /** The applied canonicalization degree (must have a 'degree' (fully-canonical or a + * `CanonicalForm`)): inline with the aim of this transformation. */ + canonical: Exclude; +} + +interface StructuralTransformOptions { + type: 'structural'; +} + +interface EvaluateTransformOptions { + type: 'evaluate' | 'N'; + // @note: only 'materialization' is relevant, because 'numericApproximation' decided by 'type'; and + // 'signal' applicable only to an async call. + + evalOptions?: Pick, 'materialization'>; +} + +interface SimplifyTransformOptions { + type: 'simplify'; + simplifyOptions?: SimplifyOptions; +} diff --git a/src/compute-engine/types-kernel-serialization.ts b/src/compute-engine/types-kernel-serialization.ts index 6eaf805e..58da40bd 100644 --- a/src/compute-engine/types-kernel-serialization.ts +++ b/src/compute-engine/types-kernel-serialization.ts @@ -131,6 +131,8 @@ export type ReplaceOptions = { * If false, continue applying remaining rules. */ once: boolean; + //@consider:? + // once: 'one-rule' | 'one-replacement'; /** * If true, rules may match equivalent variants.