From 2fc3e9915682c87fe78400ef4fb57466ee54c086 Mon Sep 17 00:00:00 2001 From: samueltlg Date: Mon, 6 Apr 2026 20:46:16 +0100 Subject: [PATCH 1/5] feat: ReplaceOptions.direction --- src/compute-engine/boxed-expression/rules.ts | 8 +++++++- .../types-kernel-serialization.ts | 17 +++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/compute-engine/boxed-expression/rules.ts b/src/compute-engine/boxed-expression/rules.ts index d68d8a7b..a5fd5b42 100755 --- a/src/compute-engine/boxed-expression/rules.ts +++ b/src/compute-engine/boxed-expression/rules.ts @@ -809,14 +809,20 @@ export function applyRule( let operandsMatched = false; if (isFunction(expr) && options?.recursive) { + const direction = options?.direction ?? 'left-right'; + let newOps = + direction === 'left-right' ? expr.ops : [...expr.ops].reverse(); + // Apply the rule to the operands of the expression - const newOps = expr.ops.map((op) => { + newOps = newOps.map((op) => { const subExpr = applyRule(rule, op, {}, options); if (!subExpr) return op; operandsMatched = true; return subExpr.value; }); + if (direction === 'right-left') (newOps as Expression[]).reverse(); + // At least one operand (directly or recursively) matched: but continue onwards to match against // the top-level expr., test against any 'condition', et cetera. if (operandsMatched) { diff --git a/src/compute-engine/types-kernel-serialization.ts b/src/compute-engine/types-kernel-serialization.ts index e341a190..64283aab 100644 --- a/src/compute-engine/types-kernel-serialization.ts +++ b/src/compute-engine/types-kernel-serialization.ts @@ -153,6 +153,23 @@ export type ReplaceOptions = { * Canonicalization policy after replacement. */ canonical: CanonicalOptions; + + /** *Traversal* direction (through the node 'tree') for both Rule matching & replacement. + * Can be significant in the production of the final, overall replacement result (if operating + * recursively) - if rule is a `RuleFunction` with arbitrary logic (e.g. replacements being + * index-based). + * + * In 'tree' data-structure traversal terminology, possible values span: + * + * - `'left-right'` reflects *post-order* traversal, (left sub-tree first; depth-descending) (LRN). + * - `'right-left'` reflects 'reverse' *post-order* (right sub-tree first; depth-descending) (RLN). + * + * For both cases traversal is always depth-first, and always visits the root/input expr. last . + * + * **Default** is: `'left-right'` (standard post-order) + * + */ + direction: 'left-right' | 'right-left'; }; /** From b40521177990f144b0bf4364b193e932d27e66b3 Mon Sep 17 00:00:00 2001 From: samueltlg Date: Sun, 17 May 2026 17:57:07 +0100 Subject: [PATCH 2/5] refactor (+fix): update ReplaceOptions.canonical to 'form' Retain canonical as a deprecated alias, accept both option names for one release, and keep the replacement-form transition separate from the replacement-sameness change. --- src/compute-engine/boxed-expression/rules.ts | 120 ++++++++++++++---- .../boxed-expression/simplify.ts | 4 +- src/compute-engine/boxed-expression/solve.ts | 6 +- src/compute-engine/symbolic/antiderivative.ts | 2 +- src/compute-engine/types-expression.ts | 28 ++-- .../types-kernel-serialization.ts | 34 ++++- 6 files changed, 146 insertions(+), 48 deletions(-) diff --git a/src/compute-engine/boxed-expression/rules.ts b/src/compute-engine/boxed-expression/rules.ts index a5fd5b42..7418f085 100755 --- a/src/compute-engine/boxed-expression/rules.ts +++ b/src/compute-engine/boxed-expression/rules.ts @@ -15,6 +15,7 @@ import type { Expression, ReplaceOptions, ExpressionInput, + FormOption, } from '../global-types'; import { @@ -774,6 +775,23 @@ export function boxRules( return { rules }; } +function normalizeReplaceForm( + options?: Readonly> +): FormOption | undefined { + if (options?.canonical !== undefined && options?.form !== undefined) + throw new Error( + 'replace(): options.canonical and options.form are mutually exclusive' + ); + + if (options?.canonical !== undefined) { + if (options.canonical === true) return 'canonical'; + if (options.canonical === false) return 'raw'; + return options.canonical; + } + + return options?.form; +} + /** * Apply a rule to an expression, assuming an incoming substitution * @param rule the rule to apply @@ -789,7 +807,7 @@ export function applyRule( options?: Readonly> ): RuleStep | null { if (!rule) return null; - let canonical = options?.canonical ?? (expr.isCanonical || expr.isStructural); + const requestedForm = normalizeReplaceForm(options); // eslint-disable-next-line prefer-const let { match, replace, condition, id, onMatch, onBeforeMatch } = rule; @@ -797,7 +815,12 @@ export function applyRule( const ce = expr.engine; - if (canonical && match) { + const canonicalRequested = + requestedForm !== undefined && + requestedForm !== 'raw' && + requestedForm !== 'structural'; + + if ((canonicalRequested || expr.isCanonical) && match) { const awc = getWildcards(match); const canonicalMatch = match.canonical; const bwc = getWildcards(canonicalMatch); @@ -826,18 +849,20 @@ export function applyRule( // At least one operand (directly or recursively) matched: but continue onwards to match against // the top-level expr., test against any 'condition', et cetera. if (operandsMatched) { - // If new/replaced operands are all canonical, and options do not explicitly specify canonical - // status, then should be safe to mark as fully-canonical - if ( - !canonical && - options?.canonical === undefined && - newOps.every((x) => x.isCanonical) - ) - canonical = true; - - expr = ce.function(expr.operator, newOps, { - form: canonical ? 'canonical' : 'raw', - }); + // (note: No need to consult the input-expr 'form' because, assuming that replaced operands + // assume the same form, this will be upcast in the subsequent branches. + let form: FormOption = 'raw'; + // The current policy for applying a form according to 'options.form' is for this to apply to + // *replacements only* (this ultimately allowing for finer control of replacement operations). + // ...However, if all child operands bear the same form, 'eagerly' assume this form for the + // present expression (if this present expression also later matches, form may be updated + // according to 'options.form'.) + //(@note: check 'canonical' first, because numbers may be jointly marked as structural and + //canonical). + if (newOps.every((x) => x.isCanonical)) form = 'canonical'; + else if (newOps.every((x) => x.isStructural)) form = 'structural'; + + expr = ce.function(expr.operator, newOps, { form }); } } @@ -887,31 +912,70 @@ export function applyRule( } } + /** The computed form value to be assumed by the *directly replaced* expression: assuming either an + 'enforced' value (options), or consultation to the form of the input expression */ + let formValue = + requestedForm ?? + (expr.isStructural ? 'structural' : expr.isCanonical ? 'canonical' : 'raw'); + + // If `true`, then the form is not 'enforced' (via options) and therefore, the prior computed + // form only applies wherein the initially-produced replacement expression has a 'raw' form + // (else retaining whichever form of the replacement) + const dynamicForm = requestedForm === undefined; + + /** Get the overall form type from *formValue* (raw/structural/canonical), accounting for + * 'canonical' potentially assuming multiple values. */ + const getFormType = () => + formValue === 'structural' + ? 'structural' + : formValue === 'raw' + ? 'raw' + : 'canonical'; + // Have a (direct) match: in this case, consider the canonical-status of the replacement, too. if ( - !canonical && - options?.canonical === undefined && + formValue === 'raw' && + dynamicForm && replace instanceof _BoxedExpression && - replace.isCanonical + (replace.isCanonical || replace.isStructural) ) - canonical = true; + formValue = replace.isCanonical ? 'canonical' : 'structural'; //@note: '.subs()' acts like an expr. 'clone' here (in case of an empty substitution) const result = typeof replace === 'function' ? replace(expr, sub) - : replace.subs(sub, { canonical }); + : // @todo: 'expr.subs()' to eventually also assume a 'form' option + // : replace.subs(sub, { form: dynamicForm ? undefined : formValue }); + replace.subs(sub, { canonical: getFormType() === 'canonical' }); - if (!result) - return operandsMatched - ? { value: canonical ? expr.canonical : expr, because } - : null; + if (!result) return operandsMatched ? { value: expr, because } : null; // To aid in debugging, invoke onMatch when the rule matches onMatch?.(rule, expr, result); + /** Return the final *expression* with the correctly computed form. */ + const computeValue = (result: Expression) => { + // If 'raw', leave the expression as-is + // (note that if result has produced a 'non-raw' form, this may not be 'undone'...) + if (formValue === 'raw') return result; + // Non option-enforced form; let replacement/result expression form override + if (dynamicForm === true && (result.isStructural || result.isCanonical)) + return result; + // Enforced form + return getFormType() === 'canonical' + ? result.isCanonical + ? result + : ce.expr(result, { form: formValue }) //Re-box (instead of 'x.canonical'), case of 'CanonicalForm' + : result.structural; + }; + + // (Need to request a 'form' variant (canonical/structural) to account for case of a custom + // replace: which may not have returned the same 'form' calculated here) if (isRuleStep(result)) - return canonical ? { ...result, value: result.value.canonical } : result; + return getFormType() === 'raw' + ? result + : { ...result, value: computeValue(result.value) }; if (!isExpression(result)) { throw new Error( @@ -919,9 +983,10 @@ export function applyRule( ); } - // (Need to request the canonical variant to account for case of a custom replace: which may not - // have returned canonical.) - return { value: canonical ? result.canonical : result, because }; + return { + value: computeValue(result), + because, + }; } /** @@ -942,6 +1007,7 @@ export function replace( const iterationLimit = options?.iterationLimit ?? 1; let iterationCount = 0; const once = options?.once ?? false; + normalizeReplaceForm(options); // Normalize the ruleset let ruleSet: ReadonlyArray; diff --git a/src/compute-engine/boxed-expression/simplify.ts b/src/compute-engine/boxed-expression/simplify.ts index 1483dc18..62e173b4 100644 --- a/src/compute-engine/boxed-expression/simplify.ts +++ b/src/compute-engine/boxed-expression/simplify.ts @@ -341,7 +341,7 @@ function simplifyExpression( if (isSymbol(expr)) { const result = replace(expr, rules, { recursive: false, - canonical: true, + form: 'canonical', useVariations: false, }); if (result.length > 0) return [...steps, ...result]; @@ -394,7 +394,7 @@ function simplifyNonCommutativeFunction( ): RuleSteps { const result = replace(expr, rules, { recursive: false, - canonical: true, + form: 'canonical', useVariations: options.useVariations ?? false, }); diff --git a/src/compute-engine/boxed-expression/solve.ts b/src/compute-engine/boxed-expression/solve.ts index bcbd83e0..16499673 100755 --- a/src/compute-engine/boxed-expression/solve.ts +++ b/src/compute-engine/boxed-expression/solve.ts @@ -1209,7 +1209,7 @@ export function findUnivariateRoots( expr, rules, { _x: ce.symbol('_x') }, - { useVariations: true, canonical: true } + { useVariations: true, form: 'canonical' } ) ); @@ -1232,7 +1232,7 @@ export function findUnivariateRoots( expr, rules, { _x: ce.symbol(x) }, - { useVariations: true, canonical: true } + { useVariations: true, form: 'canonical' } ) ); } @@ -1247,7 +1247,7 @@ export function findUnivariateRoots( expr, rules, { _x: ce.symbol(x) }, - { useVariations: true, canonical: true } + { useVariations: true, form: 'canonical' } ) ); } diff --git a/src/compute-engine/symbolic/antiderivative.ts b/src/compute-engine/symbolic/antiderivative.ts index c34541da..fb1b9719 100644 --- a/src/compute-engine/symbolic/antiderivative.ts +++ b/src/compute-engine/symbolic/antiderivative.ts @@ -2434,7 +2434,7 @@ export function antiderivative(fn: Expression, index: string): Expression { xfn, rules, { _x: ce.symbol('_x') }, - { useVariations: true, canonical: true } + { useVariations: true, form: 'canonical' } ); if (result && result[0]) return result[0].subs({ _x: index }); diff --git a/src/compute-engine/types-expression.ts b/src/compute-engine/types-expression.ts index 2af6f9a1..0b8f050c 100644 --- a/src/compute-engine/types-expression.ts +++ b/src/compute-engine/types-expression.ts @@ -1252,28 +1252,32 @@ export interface Expression { * * - If no rules apply, return `null`. * - * See also `expr.subs()` for a simple substitution of symbols. * - * Procedure for the determining the canonical-status of the input expression and replacements: + * Option *'form'* controls the form of *replacements*. The deprecated option *'canonical'* is also + * accepted for backward compatibility (only one of these should be specified). * - * - If `options.canonical` is set, the *entire expr.* is canonicalized to this degree: whether - * the replacement occurs at the top-level, or within/recursively. + * In the absence of a specified 'form' (or 'canonical') value, then the present policy for + * determining replacement expression form is either [to]: + * - (a) first look to the form of the replacement, as generated by the replacement `Rule`... + * and failing this (i.e. if this has no non-'raw' form), then: * - * - If otherwise, the *direct replacement will be canonical* if either the 'replaced' expression - * is canonical, or the given replacement (- is a Expression and -) is canonical. - * Notably also, if this replacement takes place recursively (not at the top-level), then exprs. - * containing the replaced expr. will still however have their (previous) canonical-status - * *preserved*... unless this expr. was previously non-canonical, and *replacements have resulted - * in canonical operands*. In this case, an expr. meeting this criteria will be updated to - * canonical status. (Canonicalization is opportunistic here, in other words). + * - (b) consult the form of the *replaced* expression (provided that the form is *not + * explicitly specified as `'raw'`*) + * If still then no form is determined, the expression is also left in its original, raw form. + * + * Notably, whilst this option does only apply directly to replaced sub-expressions, the present + * is also to attempt to 'opportunistically' propagate any non-`raw` up the expression 'tree' (but + * with the explicit provision of form 'raw' resulting in the omission of this behaviour). * * :::info[Note] - * Applicable to canonical and non-canonical expressions. + * Applicable to input expressions of any 'form'. * * To match a specific symbol (not a wildcard pattern), the `match` must be * a `Expression` (e.g., `{ match: ce.expr('x'), replace: ... }`). + * * For simple symbol substitution, consider using `subs()` instead. * ::: + * */ replace( rules: BoxedRuleSet | Rule | Rule[], diff --git a/src/compute-engine/types-kernel-serialization.ts b/src/compute-engine/types-kernel-serialization.ts index 64283aab..b483dc1f 100644 --- a/src/compute-engine/types-kernel-serialization.ts +++ b/src/compute-engine/types-kernel-serialization.ts @@ -150,9 +150,38 @@ export type ReplaceOptions = { iterationLimit: number; /** - * Canonicalization policy after replacement. + * Replacement form policy. + * + * For recursive replacements, the requested form applies to the replaced + * subexpression and may be propagated upward when all child operands share + * the same form. + */ + + /** + * @deprecated Use `form` instead. This alias will be removed in the next + * release. + */ + canonical?: CanonicalOptions; + + /** + * `form` policy for replaced expressions. \ + * (For recursive replacements (`recursive == true`), applies only to the replaced subexpressions + * (and not the the entire expression-tree)... However, if a recursive/depth replacement takes + * place, the policy is to 'eagerly' apply the replaced expression form as all the way up to the + * expression root: such that, if a replacement is 'structural' or 'canonical' and consequently + * the operands of the containing function-expression all possess the same form, then the + * containing expression will also take on this same form. + * + * If wishing to therefore ensure a the requested form for the *entire input* expression, either + * ensure the input is already in the requested form before any replacement, or simply request the + * form post-replacement. + * + * ::Additional notes + * - form `'raw'` loses its applicability if the replaced expression - according to replacement mechanics - already assumes a form according to + * replacement rule logic. (for example if the applying rule is of type `RuleFunction` and the + * produced expression has a non-raw form). */ - canonical: CanonicalOptions; + form: FormOption; /** *Traversal* direction (through the node 'tree') for both Rule matching & replacement. * Can be significant in the production of the final, overall replacement result (if operating @@ -167,7 +196,6 @@ export type ReplaceOptions = { * For both cases traversal is always depth-first, and always visits the root/input expr. last . * * **Default** is: `'left-right'` (standard post-order) - * */ direction: 'left-right' | 'right-left'; }; From 53c68a263a3ca5c93cb675f226aebed068e53a39 Mon Sep 17 00:00:00 2001 From: samueltlg Date: Sun, 17 May 2026 17:57:27 +0100 Subject: [PATCH 3/5] fix: when considering replacement expr. sameness, also consider expression 'form' Treat form changes as meaningful during recursive replace iteration so eager form propagation can surface even when structural equality stays the same. This keeps the replacement-form behavior separate from the ReplaceOptions API transition. --- src/compute-engine/boxed-expression/rules.ts | 60 +++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/src/compute-engine/boxed-expression/rules.ts b/src/compute-engine/boxed-expression/rules.ts index 7418f085..6d882b63 100755 --- a/src/compute-engine/boxed-expression/rules.ts +++ b/src/compute-engine/boxed-expression/rules.ts @@ -1028,7 +1028,7 @@ export function replace( if ( result !== null && result.value !== expr && - !result.value.isSame(expr) + (!result.value.isSame(expr) || varyingForm(expr, result.value)) ) { // If `once` flag is set, bail on first matching rule if (once) return [result]; @@ -1049,6 +1049,64 @@ export function replace( iterationCount += 1; } return steps; + + /* + * Local f. + */ + /** + * Assuming *x* and *x2* are **structurally (symbolically) equivalent**, and considering + * expression forms 'structural' and 'canonical': + * + * - If option 'recursive' equals `true` or `'functions-only'` (**default** = `'functions-only'`), + * then, if either 'x' or 'x2', or one of the matching sub-expression pairs of these has a + * differing 'structural' or 'canonical' status, then return `true`. + * (if 'functions-only', then only function-expression operands are considered) + * + * - If 'recursive' === `false`, then this status comparison applies only to/between `x` and `x2` + * directly. + * + * For both cases, if neither `x` nor `x2` (nor compared sub-expressions if recursive) is + * structural or canonical, then return `false`. + * + * **Warning**: will throw an error if it is determined, in case of `recursive !== false`, that + * `x` and `x2` are not structurally equivalent/have an identical tree/branching structure. + * (It is therefore the responsibility of the caller to ensure this beforehand) + */ + function varyingForm( + x: Expression, + x2: Expression, + { + recursive = 'functions-only', + }: { recursive?: boolean | 'functions-only' } = {} + ): boolean { + if (varies(x, x2)) return true; + + if (recursive === false) return false; + + if (isFunction(x) && isFunction(x2)) { + if (x.ops.length !== x2.ops.length) + throw new Error( + `'x' and 'x2' detected to not be structurally equivalent` + ); + if (x.nops === 0) return false; + + return x.ops.some((op, index) => + recursive === true || (!isFunction(op) && !isFunction(x2.ops[index])) + ? false + : varyingForm(op, x2.ops[index], { recursive }) + ); + } else if (isFunction(x) || isFunction(x2)) return true; + + return false; + + function varies(x: Expression, x2: Expression): boolean { + if (x.isStructural || x.isCanonical) { + if (x.isStructural) return !x2.isStructural; + return !x2.isCanonical; + } + return x2.isStructural || x2.isCanonical ? true : false; + } + } } /** From 39334f07b95824d70e5a952fdaf4c7d728de5410 Mon Sep 17 00:00:00 2001 From: samueltlg Date: Fri, 3 Apr 2026 01:17:55 +0100 Subject: [PATCH 4/5] fix (workaround): ensure intended canonical - non-canonical - match pattern variant comparison (rule application) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (this change is marked as a *workaround* rather than a 'fix' per se, since the issue it tackles instead likely has its root in the current behaviour regarding the binding of 'wildcard' symbol variants (-outside the context of pattern matching / replacement. (See the closing of this message for a further remark on this point.)) Explanation: - Currently, because canonicalization of expresssions involves wildcard binding as part of symbol binding (this may be undesirable / a bug), it is presently necessary that a new scope be created upon 'match' pattern canonicalization (performed for optimization purposes) in rule application: with this otherwise running the risk of present wildcard symbols being bound to definitions in the current scope... For example, before this change: ``` ce.expr(['Add', 'x', 3]).replace({ match: ['_', '__'], replace: 'y'}); // →Expectedly captures wildcards, and replaces // top-level expression. // *However*, the canonical-request of the 'match' expression within 'applyRule' // results in symbol/wildcard '_' being attributed a function-definition // (type='function'): within the scope in which this 'expr.replace()' was called // Consequently, a subsequent replace call involving a universal wildcard ce.expr(['Add', 'x', 3]).replace({ match: ['Add', 'x', '_')], replace: 5 }); // → Returns 'null'... (assuming this call made with an unmodified/same scope as the previous) // Ultimately because the same point-of-canonicalization results in the Universal // Wildcard in this instance uptaking the previous, 'function' definition, resulting // in a MathJson-internal type-error for the canonicalized match-pattern variant // in this instance, resulting in an absent wildcard in the canonical variant, // and leading to an early exit from rule-application 'applyRule()' //For illustration, the canonical-variant of the initial, non-canonically boxed // `['Add', 'x', 3]` pattern results in the canonical-variant as: [ "Add", "x", [ "Error", [ "ErrorCode", "'incompatible-type'", "number", "function", ], ], ] ``` *Note*: - This 'fix' may no longer be necessary, if canonicalization of wildcard-containing expressions were to no longer perform name-binding on these (this may be unintentional?) --- src/compute-engine/boxed-expression/rules.ts | 32 ++++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/src/compute-engine/boxed-expression/rules.ts b/src/compute-engine/boxed-expression/rules.ts index 6d882b63..9cb21e9a 100755 --- a/src/compute-engine/boxed-expression/rules.ts +++ b/src/compute-engine/boxed-expression/rules.ts @@ -658,16 +658,9 @@ function boxRule( ); } - // Push a clean scope that only inherits from the system scope (index 0), - // not from the global scope or user-defined scopes. This prevents user-defined - // symbols (like `x` used as a function name in `x(y+z)`) from interfering with - // rule parsing. The system scope contains all built-in definitions. - const systemScope = ce.contextStack[0]?.lexicalScope; - if (systemScope) { - ce.pushScope({ parent: systemScope, bindings: new Map() }); - } else { - ce.pushScope(); - } + // Ensure a clean scope (that only inherits from the system scope) before boxing or parsing: + // preventing wildcards & user-defined from inheriting definitions in rules. + pushSafeScope(ce); let matchExpr: Expression | undefined; let replaceExpr: Expression | RuleReplaceFunction | RuleFunction | undefined; @@ -743,6 +736,25 @@ function boxRule( }; } +/** + * Push a clean scope - safe for the boxing of rules - that only inherits from the system scope + * (index 0), not from the global scope or user-defined scopes. This prevents user-defined symbols + * (like `x` used as a function name in `x(y+z)`) from interfering with rule parsing. The system + * scope contains all built-in definitions. + * + * This also crucially prevents wildcards from being given definitions where captured & bound. + * + * @param ce + */ +function pushSafeScope(ce: ComputeEngine) { + const systemScope = ce.contextStack[0]?.lexicalScope; + if (systemScope) { + ce.pushScope({ parent: systemScope, bindings: new Map() }); + } else { + ce.pushScope(); + } +} + /** * Create a boxed rule set from a collection of non-boxed rules */ From 8c41a8a44321ddb76a4221b0d707ff0e43c9c131 Mon Sep 17 00:00:00 2001 From: samueltlg Date: Sun, 17 May 2026 17:57:47 +0100 Subject: [PATCH 5/5] doc: ReplaceOptions; and CHANGELOG --- CHANGELOG.md | 24 +++++++++++++++++-- src/compute-engine/boxed-expression/rules.ts | 6 +++-- .../types-kernel-serialization.ts | 23 +++++++----------- 3 files changed, 35 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1291d312..b46487ad 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,23 @@ +## Upcoming changes + +### Added + +- **ReplaceOptions.direction** — replacement traversal can now be controlled + from left-to-right or right-to-left. + +### Fixed + +- **ReplaceOptions form handling** — `Expression.replace()` now accepts the new + `form` option, while keeping deprecated `canonical` as a backward- compatible + alias for one release. +- **Replacement form propagation** — recursive replacements preserve and + propagate the requested form upward when child operands already share it. + +### Changes + +- **ReplaceOptions.canonical is deprecated** — prefer `form`; specifying both + `form` and `canonical` now raises an error. + ### 0.58.0 _2026-05-12_ #### Added @@ -1230,8 +1250,8 @@ GPU compile). `oklab(L a b / alpha)` syntax, matching the existing `oklch()` support. - **GPU compilation**: `ColorMix`, `ColorContrast`, `ContrastingColor`, `ColorToColorspace`, and `ColorFromColorspace` now compile to GLSL and WGSL. - Preamble functions provide sRGB ↔ OKLab ↔ OKLCh conversion, color mixing with - shorter-arc hue interpolation, and APCA contrast on the GPU. + Preamble functions provide sRGB ↔ OKLab ↔ OKLCh conversion, color mixing + with shorter-arc hue interpolation, and APCA contrast on the GPU. - Added `rgbToHsl()` conversion function. Exported `hslToRgb()` (previously private). diff --git a/src/compute-engine/boxed-expression/rules.ts b/src/compute-engine/boxed-expression/rules.ts index 9cb21e9a..35864e13 100755 --- a/src/compute-engine/boxed-expression/rules.ts +++ b/src/compute-engine/boxed-expression/rules.ts @@ -861,8 +861,10 @@ export function applyRule( // At least one operand (directly or recursively) matched: but continue onwards to match against // the top-level expr., test against any 'condition', et cetera. if (operandsMatched) { - // (note: No need to consult the input-expr 'form' because, assuming that replaced operands - // assume the same form, this will be upcast in the subsequent branches. + // (note: so not consult the input-expr 'form' because, assuming that replaced operands assume + // the same form, this will be upcast in the subsequent branches. + // ^Another reason to avoid this, is if the form of replacements differ from the input expr., + // then likely it is not the intention to preserve the form of the parent) let form: FormOption = 'raw'; // The current policy for applying a form according to 'options.form' is for this to apply to // *replacements only* (this ultimately allowing for finer control of replacement operations). diff --git a/src/compute-engine/types-kernel-serialization.ts b/src/compute-engine/types-kernel-serialization.ts index b483dc1f..cb973dae 100644 --- a/src/compute-engine/types-kernel-serialization.ts +++ b/src/compute-engine/types-kernel-serialization.ts @@ -150,27 +150,22 @@ export type ReplaceOptions = { iterationLimit: number; /** - * Replacement form policy. + * Specify the canonical-status of _replaced_ sub-expressions. + * + * The specified canonical value/form may propagate upward to the input expression/root according + * to 'eager' replacement polcicy. See `replace()` documentation for details. * - * For recursive replacements, the requested form applies to the replaced - * subexpression and may be propagated upward when all child operands share - * the same form. - */ /** - * @deprecated Use `form` instead. This alias will be removed in the next - * release. + * @deprecated This is a legacy property: see option `form` for a wider span of forms. */ canonical?: CanonicalOptions; /** - * `form` policy for replaced expressions. \ - * (For recursive replacements (`recursive == true`), applies only to the replaced subexpressions - * (and not the the entire expression-tree)... However, if a recursive/depth replacement takes - * place, the policy is to 'eagerly' apply the replaced expression form as all the way up to the - * expression root: such that, if a replacement is 'structural' or 'canonical' and consequently - * the operands of the containing function-expression all possess the same form, then the - * containing expression will also take on this same form. + * `form` policy for *replaced* expressions. \ + * + * (If there is a recursive replacement -) Does not automatically apply to the entire input expression... However, the present `replace()` policy is to 'eagerly' propagate any specified replaced-expression replacement form the entire way 'up' an expression-tree. + * A value of * * If wishing to therefore ensure a the requested form for the *entire input* expression, either * ensure the input is already in the requested form before any replacement, or simply request the