-
Notifications
You must be signed in to change notification settings - Fork 58
feat/refactor: 'expr.transform()'; fixes & refactors to 'replace()' and rule-application #301
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 9 commits
adf26f8
1bf80b5
57ce573
d36d165
6e116c1
7d14b6a
ccd32d6
12cabb5
27e8e81
5d974d2
9c95630
7c761b9
bec5201
e7a30e9
2646bf8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,7 +18,6 @@ import type { | |
| } from '../global-types'; | ||
|
|
||
| import { | ||
| asLatexString, | ||
| isInequalityOperator, | ||
| isRelationalOperator, | ||
| } from '../latex-syntax/utils'; | ||
|
|
@@ -630,22 +629,16 @@ function boxRule( | |
| // Normalize the condition to a function | ||
| let condFn: undefined | RuleConditionFunction; | ||
| if (typeof condition === 'string') { | ||
| const latex = asLatexString(condition); | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Taking back on this did not appear to break anything. Concludes that it was one of either an outdated or mistaken check... the 'asLatexString' call appears to presently require that any string [input] be delimited by an '$' or '$$' |
||
| if (latex) { | ||
| // If the condition is a LaTeX string, it should be a predicate | ||
| // (an expression with a Boolean value). | ||
| const condPattern = | ||
| ce.parse(latex, { | ||
| form: options?.canonical ? 'canonical' : 'raw', | ||
| }) ?? ce.expr('Nothing'); | ||
|
|
||
| // Substitute any unbound vars in the condition to a wildcard, | ||
| // then evaluate the condition | ||
| condFn = (x: BoxedSubstitution, _ce: ComputeEngine): boolean => { | ||
| const evaluated = condPattern.subs(x).evaluate(); | ||
| return isSymbol(evaluated, 'True'); | ||
| }; | ||
| } | ||
| // If the condition is a LaTeX string, it should be a predicate | ||
| // (an expression with a Boolean value). | ||
| const condPattern = ce.parse(condition) ?? ce.expr('Nothing'); | ||
|
|
||
| // Substitute any unbound vars in the condition to a wildcard, | ||
| // then evaluate the condition | ||
| condFn = (x: BoxedSubstitution, _ce: ComputeEngine): boolean => { | ||
| const evaluated = condPattern.subs(x).evaluate(); | ||
| return isSymbol(evaluated, 'True'); | ||
| }; | ||
| } else { | ||
| if (condition !== undefined && typeof condition !== 'function') | ||
| throw new Error( | ||
|
|
@@ -664,16 +657,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; | ||
|
|
@@ -749,6 +735,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 | ||
| */ | ||
|
|
@@ -796,19 +801,43 @@ export function applyRule( | |
| options?: Readonly<Partial<ReplaceOptions>> | ||
| ): RuleStep | null { | ||
| if (!rule) return null; | ||
| let canonical = options?.canonical ?? (expr.isCanonical || expr.isStructural); | ||
| //@todo?: consider 'structural' too (separately); maybe by up-typing 'options.canonical' as 'form'? | ||
| let canonical = options?.canonical ?? expr.isCanonical; | ||
|
|
||
| // eslint-disable-next-line prefer-const | ||
| let { match, replace, condition, id, onMatch, onBeforeMatch } = rule; | ||
| const because = id ?? ''; | ||
|
|
||
| const ce = expr.engine; | ||
|
|
||
| if (canonical && match) { | ||
| const awc = getWildcards(match); | ||
| pushSafeScope(ce); | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (See commit message - this change indeed may not be necessary, if the described present behaviour of the wildcard-symbol canonicalization (global-scope) is not as it should be). |
||
| const canonicalMatch = match.canonical; | ||
| ce.popScope(); | ||
| const bwc = getWildcards(canonicalMatch); | ||
| // If the canonical form of the match loses wildcards, this rule cannot match | ||
| // canonical expressions (they would already be simplified). Skip this rule. | ||
| if (!awc.every((x) => bwc.includes(x))) return null; | ||
| } | ||
|
|
||
| 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) { | ||
|
|
@@ -821,26 +850,12 @@ export function applyRule( | |
| ) | ||
| canonical = true; | ||
|
|
||
| expr = expr.engine.function(expr.operator, newOps, { | ||
| expr = ce.function(expr.operator, newOps, { | ||
| form: canonical ? 'canonical' : 'raw', | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| // eslint-disable-next-line prefer-const | ||
| let { match, replace, condition, id, onMatch, onBeforeMatch } = rule; | ||
| const because = id ?? ''; | ||
|
|
||
| if (canonical && match) { | ||
| const awc = getWildcards(match); | ||
| const canonicalMatch = match.canonical; | ||
| const bwc = getWildcards(canonicalMatch); | ||
| // If the canonical form of the match loses wildcards, this rule cannot match | ||
| // canonical expressions (they would already be simplified). Skip this rule. | ||
| if (!awc.every((x) => bwc.includes(x))) | ||
| return operandsMatched ? { value: expr, because } : null; | ||
| } | ||
|
|
||
| const useVariations = rule.useVariations ?? options?.useVariations ?? false; | ||
| const matchPermutations = options?.matchPermutations ?? true; | ||
|
|
||
|
|
@@ -877,7 +892,7 @@ export function applyRule( | |
| }; | ||
|
|
||
| try { | ||
| if (!condition(conditionSub, expr.engine)) | ||
| if (!condition(conditionSub, ce)) | ||
| return operandsMatched ? { value: expr, because } : null; | ||
| } catch (e) { | ||
| console.error( | ||
|
|
@@ -902,7 +917,10 @@ export function applyRule( | |
| ? replace(expr, sub) | ||
| : replace.subs(sub, { canonical }); | ||
|
|
||
| if (!result) return null; | ||
| if (!result) | ||
| return operandsMatched | ||
| ? { value: canonical ? expr.canonical : expr, because } | ||
| : null; | ||
|
|
||
| // To aid in debugging, invoke onMatch when the rule matches | ||
| onMatch?.(rule, expr, result); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -407,7 +407,7 @@ function simplifyNonCommutativeFunction( | |
| let last = result.at(-1)!.value; | ||
| if (last.isSame(expr)) return steps; | ||
|
|
||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Small oversight... but was inadvertently applying the entire default rule-set to operands! (indeed; no test failures result) |
||
| last = simplifyOperands(last); | ||
| last = simplifyOperands(last, options); | ||
|
|
||
| // If the simplified expression is not cheaper, we're done. | ||
| // Exception: power combination results (e.g., -4·2^x → -2^(x+2)) may be | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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'; | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. At your discretion! (this is anyway achievable in a workaround-way with custom rule-functions and what-not). (I did also intend to update the 'once' rule such that it would be typed as something like |
||
| }; | ||
|
|
||
| /** | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Was not a test to this... but changing this appeared to correct some behaviours whilst outlining the tests for transform. Throughout the partial-canonicalization process, appears that sometimes the expression is 'intermediarily' not marked as canonical.