diff --git a/src/api.md b/src/api.md index 99fb7d03..10cb833d 100644 --- a/src/api.md +++ b/src/api.md @@ -1,4 +1,4 @@ - +# 01 ## Compute Engine @@ -1273,6 +1273,10 @@ is canonical. :::info[Note] Applicable to canonical and non-canonical expressions. + +If this is a function, an empty substitution is given, and the computed value of `canonical` +does not differ from that of this expr.: then a call this method is analagous to requesting a +*clone*. ::: ####### sub @@ -1344,11 +1348,24 @@ Transform the expression by applying one or more replacement rules: See also `expr.subs()` for a simple substitution of symbols. -If `options.canonical` is not set, the result is canonical if `this` -is canonical. +Procedure for the determining the canonical-status of the input expression and replacements: + +- If `options.canonical` is set, the *entire expr.* is canonicalized to this degree: whether +the replacement occurs at the top-level, or within/recursively. + +- If otherwise, the *direct replacement will be canonical* if either the 'replaced' expression +is canonical, or the given replacement (- is a BoxedExpression 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). :::info[Note] Applicable to canonical and non-canonical expressions. + +To specify a match for single symbol (non wildcard), it must be boxed (e.g. `{ match: +ce.box('x'), ... }`), but it is suggested to use method *'subs()'* for this. ::: ####### rules @@ -2647,9 +2664,9 @@ type PatternMatchOptions = { Control how a pattern is matched to an expression. -- `substitution`: if present, assumes these values for the named wildcards, - and ensure that subsequent occurrence of the same wildcard have the same - value. +- `substitution`: if present, assumes these values for a subset of + named wildcards, and ensure that subsequent occurrence of the same + wildcard have the same value. - `recursive`: if true, match recursively, otherwise match only the top level. - `useVariations`: if false, only match expressions that are structurally identical. @@ -2784,18 +2801,22 @@ type Rule = }; ``` -A rule describes how to modify an expressions that matches a pattern `match` +A rule describes how to modify an expression that matches a pattern `match` into a new expression `replace`. - `x-1` \( \to \) `1-x` - `(x+1)(x-1)` \( \to \) `x^2-1 -The patterns can be expressed as LaTeX strings or a MathJSON expressions. +The patterns can be expressed as LaTeX strings or `SemiBoxedExpression`'s. +Alternatively, match/replace logic may be specified by a `RuleFunction`, allowing both custom +logic/conditions for the match, and either a *BoxedExpression* (or `RuleStep` if being +descriptive) for the replacement. As a shortcut, a rule can be defined as a LaTeX string: `x-1 -> 1-x`. The expression to the left of `->` is the `match` and the expression to the right is the `replace`. When using LaTeX strings, single character variables -are assumed to be wildcards. +are assumed to be wildcards. The rule LHS ('match') and RHS ('replace') may also be supplied +separately: in this case following the same rules. When using MathJSON expressions, anonymous wildcards (`_`) will match any expression. Named wildcards (`_x`, `_a`, etc...) will match any expression @@ -8042,7 +8063,7 @@ valueOf(): string - +# 02 ## MathJSON @@ -8213,7 +8234,7 @@ The dictionary and function nodes can contain expressions themselves. - +# 03 ## Type diff --git a/src/common/utils.ts b/src/common/utils.ts index 9f5a3ee2..f1fa0d24 100644 --- a/src/common/utils.ts +++ b/src/common/utils.ts @@ -1,11 +1,39 @@ -export function permutations( - xs: ReadonlyArray +/** + * + * + * + * @export + * @template T + * @param xs + * @param [condition] + * @returns + */ +export function permutations( + xs: ReadonlyArray, + condition?: ( + xs: ReadonlyArray /* , generated: Set | Set<[Y,T]>? */ + ) => boolean + // cacheKey?: (T) => Y ): ReadonlyArray> { const result: ReadonlyArray[] = []; - const permute = (arr, m = []) => { + const permute = (arr: T[], m: T[] = []) => { if (arr.length === 0) { - result.push(m); + if (!condition || condition(m)) { + // Use spread operator to create a shallow copy of m + result.push([...m]); + } } else { for (let i = 0; i < arr.length; i++) { const curr = arr.slice(); @@ -15,7 +43,8 @@ export function permutations( } }; - permute(xs); + //@fix: (typing) + permute(xs as T[]); return result; } diff --git a/src/compute-engine/boxed-expression/boxed-patterns.ts b/src/compute-engine/boxed-expression/boxed-patterns.ts index 15fefbbc..ddd81e87 100755 --- a/src/compute-engine/boxed-expression/boxed-patterns.ts +++ b/src/compute-engine/boxed-expression/boxed-patterns.ts @@ -11,6 +11,14 @@ export function isWildcard(expr: BoxedExpression): expr is BoxedSymbol { ); } +/** + * Return the string representing this wildcard, including any optional (one-character) name, or + * `null` if not a wildcard expression. + * + * @export + * @param expr + * @returns + */ export function wildcardName(expr: BoxedExpression): string | null { if (expr.symbol?.startsWith('_')) return expr.symbol; @@ -27,3 +35,46 @@ export function wildcardName(expr: BoxedExpression): string | null { return null; } + +/** + * + * + * + * @export + * @param expr + * @returns + */ +export function wildcardType( + expr: BoxedExpression | string +): 'Wildcard' | 'Sequence' | 'OptionalSequence' | null { + + if (typeof expr === 'string') { + if (expr.startsWith('_')) { + if (expr.startsWith('__')) { + if (expr.startsWith('___')) return 'Sequence'; + return 'OptionalSequence'; + } + return 'Wildcard'; + } + return null; + } + + if (expr.symbol !== null) { + const symbol = expr.symbol!; + if (!symbol.startsWith('_')) return null; + if (!symbol.startsWith('__')) return 'Wildcard'; + return symbol.startsWith('___') ? 'OptionalSequence' : 'Sequence'; + } + + if (expr.isFunctionExpression) { + if (expr.operator === 'Wildcard') return 'Wildcard'; + if (expr.operator === 'WildcardSequence') return 'Sequence'; + if (expr.operator === 'WildcardOptionalSequence') return 'OptionalSequence'; + } + + return null; +} diff --git a/src/compute-engine/boxed-expression/match.ts b/src/compute-engine/boxed-expression/match.ts index cd6f6139..7f934e5e 100644 --- a/src/compute-engine/boxed-expression/match.ts +++ b/src/compute-engine/boxed-expression/match.ts @@ -6,7 +6,7 @@ import type { import { permutations } from '../../common/utils'; -import { isWildcard, wildcardName } from './boxed-patterns'; +import { isWildcard, wildcardName, wildcardType } from './boxed-patterns'; import { isOperatorDef } from './utils'; function hasWildcards(expr: string | BoxedExpression): boolean { @@ -20,6 +20,20 @@ function hasWildcards(expr: string | BoxedExpression): boolean { return false; } +/** + * Return a new substitution based on arg. `substitution`, but with wildcard (of value *expr*) + * added. + * Returns given *substitution* unchanged if wildcard is a unnamed, or is already present in + * substitution. + * + * Returns `null` in cases either of attempting to an existing wildcard/substitution with a + * different value, or if the given *expr* is, or contains, a wildcard expression. + * + * @param wildcard + * @param expr + * @param substitution + * @returns + */ function captureWildcard( wildcard: string, expr: BoxedExpression, @@ -128,6 +142,10 @@ function matchOnce( // // 2. Both operator names match // + //?Arguably, matching permutations should be skipped, in the case of pattern being constituted + //by either (1) at least one optional-sequence wildcard, or 2) two or more sequence wildcards (optional or otherwise). + //^In both of these cases, the implication, it can be argued, is that their is requirement + //that operands should pertain to a *specific* permutation. result = pattern.operatorDefinition!.commutative ? matchPermutation(expr, pattern, substitution, options) : matchArguments(expr, pattern.ops, substitution, options); @@ -299,6 +317,29 @@ function matchVariations( return null; } +/** + * + * Try all needed permutations of the operands of a pattern expression, against the operands of a + * match target. Assumes that *expr* and *pattern* have the same operator. + * + * + * + * @param expr + * @param pattern + * @param substitution + * @param options + * @returns + */ function matchPermutation( expr: BoxedExpression, pattern: BoxedExpression, @@ -306,7 +347,32 @@ function matchPermutation( options: PatternMatchOptions ): BoxedSubstitution | null { console.assert(expr.operator === pattern.operator); - const patterns = permutations(pattern.ops!); + + // Avoid redundant permutations: + // This condition ensures various wildcard sub-permutations - namely a sequence followed by + // another sequence, are skipped. + // ›Consequently, a pattern such as `['Add', 1, 2, '__a', 4, '__b', 7, '_']`, notably with only 3 + // non-optional wildcards, yields only *2040* permutations, in contrast to *5040* (i.e. 7!). + //!@todo: + // - Further optimizations certainly possible here...: consider sub-sequences of expressions with + // same values (particularly number/symbol literals : Consider ['Add', 1, 1, 1, + // 1, 1]. Such a check would reduce permutations from 120 to 1. + // - ^Another, may be the req. of a matching qty. of operands (with expr.), in the case of no + // sequence wildcards in pattern. + const cond = ( + xs: ReadonlyArray /* , generated: Set */ + ) => + !xs.some( + (x, index) => + isWildcard(x) && + wildcardType(x) === 'Sequence' && + xs[index + 1] && + isWildcard(xs[index + 1]) && + wildcardType(xs[index + 1]) === 'Sequence' + ); + + const patterns = permutations(pattern.ops!, cond); + for (const pat of patterns) { const result = matchArguments(expr, pat, substitution, options); if (result !== null) return result; @@ -314,6 +380,17 @@ function matchPermutation( return null; } +/** + * Match a list of patterns against operands of expression, appending wildcards (named) to + * *substitution* (along the way). If a successful match, returns the new substitution (or the same + * if no named wildcard), or *null* for no match. + * + * @param expr + * @param patterns + * @param substitution + * @param options + * @returns + */ function matchArguments( expr: BoxedExpression, patterns: ReadonlyArray, @@ -326,80 +403,217 @@ function matchArguments( } const ce = patterns[0].engine; - let result: BoxedSubstitution | null = { ...substitution }; // We're going to consume the ops array, so make a copy const ops = [...expr.ops!]; - let i = 0; // Index in pattern - - while (i < patterns.length) { - const pat = patterns[i]; - const argName = wildcardName(pat); - - if (argName !== null) { - if (argName.startsWith('__')) { - // Match 0 or more expressions (__) or 1 or more (___) - let j = 0; // Index in subject - if (patterns[i + 1] === undefined) { - // No more args in the pattern after, go till the end - j = ops.length + 1; - } else { - // Capture till the next matching arg in the pattern - let found = false; - while (!found && j < ops.length) { - found = - matchOnce(ops[j], patterns[i + 1], result, options) !== null; - j += 1; + return matchRemaining(patterns, substitution); + + /* + * Local f(). + */ + /** + * Match a list of patterns against *remaining* (locally scoped) `ops` and consume this list along + * the way, whilst appending to `substitution`. If a complete/successful match, return this new + * substitution; else return `null`. + * + * @note: calls recursively (permutations) for sequence wildcards + * + * @param patterns + * @param substitution + * @returns + */ + function matchRemaining( + patterns: ReadonlyArray, + substitution: BoxedSubstitution + ): BoxedSubstitution | null { + let result: BoxedSubstitution | null = { ...substitution }; + + let i = 0; // Index in pattern + + while (i < patterns.length) { + const pat = patterns[i]; + const argName = wildcardName(pat); + + if (argName !== null) { + if (argName.startsWith('__')) { + // Match 1 or more expressions (__) or 0 or more (___) + const nextPattern = patterns[i + 1]; + + const isOptionalSeq = argName.startsWith('___'); + + if (nextPattern === undefined) { + // No more args in the pattern after, go till the end + if (ops.length === 0 && !isOptionalSeq) return null; + result = captureWildcard(argName, captureOps(ops.length), result); + } else { + /** Total qty. of operands to be consumed by this sequence (of those remaining). + * If a non-optional sequence, minimum must be 1. + */ + let j = isOptionalSeq ? 0 : 1; + + // The next pattern should not be another required sequence wildcard + // (^@todo?: validate beforehand; should never be permitted?) + console.assert( + !( + isWildcard(nextPattern) && + wildcardType(nextPattern) === 'Sequence' + ) + ); + + // The next 'applicable' pattern. + // If this sequence is qualified by one, or more optional-sequence wildcards, then these + // should effectively be 'merged' with this sequence, and hence be marked as capturing + // '0' operands. In this case, skip over these, arriving at the next applicable pattern, + // and capture the optional wildcards subsequently. + let nextAppPattern = nextPattern; + let nextAppPatternIndex = i + 1; + + while ( + isWildcard(nextAppPattern) && + wildcardType(nextAppPattern) === 'OptionalSequence' + ) { + if (!patterns[nextAppPatternIndex + 1]) break; + // @note: if pattern has been validated prior, the next should never be a '_' + // (universal) or '__' (regular sequence) wildcard + nextAppPattern = patterns[++nextAppPatternIndex]; + } + + // Set to `true` if there is at least one op. (remaining to be consumed) which matches + // the next pattern. + let found = false; + while (!found && j < ops.length) { + found = + matchOnce(ops[j], nextAppPattern, result, options) !== null; + if (!found) j += 1; + } + + // The next pattern does not match against any of the remaining ops. + if (!found) { + // If not an optional-seq., can assume no overall match. + if (!isOptionalSeq) return null; + // Capture a 0-length match wildcard + //(?Never?Must indicate being at end of ops. at this point...?) + result = captureWildcard(argName, captureOps(0), result); + } else { + // If have encountered optional-sequences following this sequence wildcard, capture + // these (i.e. as corresponding to 0 operands) + if (nextAppPattern !== nextPattern) { + // Index of pattern which is an optional-sequence Wildcard: + let wildcardIndex = i + 1; + while (wildcardIndex < nextAppPatternIndex) { + result = captureWildcard( + wildcardName(patterns[wildcardIndex++])!, + captureOps(0), + result! + ); + } + } + + // A sequence wildcard has matched up until the *first* operand that the next (valid) pattern matches. + // First try matching remaining ops. against remaining patterns with the sequence + // only capturing the operands leading up to this... + // Otherwise, continue to see if the next pattern matches (even) further ahead: and + // allow this sequence wildcard to capture even more operands. + // (^Necessary, for instance, if considering a pattern such as '...a + _n + b' matched + // against '3 + 4 + x + b'...: the sequence will have initially captured just '3', but + // this will result in a final overall no-match. In this case. allow the sequence to + // capture '3 + 4' (finally permitting a 'total' match) + while (j <= ops.length) { + // Attempt the match of remaining patterns against remaining ops. after considering + // the total capture by this seq.-wildcard for this iteration. + const capturedOps = ops.slice(0, j); + + result = matchRemaining( + patterns.slice(nextAppPatternIndex), + captureWildcard(argName, captureOps(j), result!) ?? result! + ); + + // A complete overall match with this sequence capturing 'j' operands. + if (result) break; + + // No match: reset the potential modified ops. array + ops.unshift(...capturedOps); + + j++; + if (j >= ops.length) break; + + // If the next pattern matches yet another/subsequent operand, move to the next + // iteration and try to match remaining patterns. + if (!matchOnce(ops[j - 1], nextAppPattern, result!, options)) + break; + } + + // If successful, have matched til' the end: else, the result will be 'null' + return result; + } } - if (!found && argName.startsWith('___')) return null; - } - - // Unless we had a optional wildcard (matching 0 or more), we must have - // found at least one match - if (!argName.startsWith('___') && j <= 1) return null; - - // Determine the value to return for the wildcard - let value: BoxedExpression; - if (j <= 1) { - if (expr.operator === 'Add') value = ce.Zero; - else if (expr.operator === 'Multiply') value = ce.One; - else value = ce.Nothing; - } else if (j === 2) { - // Capturing a single element + /* + * End of seq. wildcard matching + */ + } else if (argName.startsWith('_')) { + // Match a single expression if (ops.length === 0) return null; - value = ops.shift()!; + result = captureWildcard(argName, ops.shift()!, result); } else { - const def = ce.lookupDefinition(expr.operator); - const args = ops.splice(0, j - 1); - if (def && isOperatorDef(def) && def.operator.associative) { - value = ce.function(expr.operator, args, { canonical: false }); - } else { - value = ce.function('Sequence', args, { canonical: false }); - } + result = matchOnce(ops.shift()!, pat, result, options); } - result = captureWildcard(argName, value, result); - } else if (argName.startsWith('_')) { - // Match a single expression - if (ops.length === 0) return null; - result = captureWildcard(argName, ops.shift()!, result); + /* + * ↓Must be *non-wildcard* + */ } else { - result = matchOnce(ops.shift()!, pat, result, options); + const arg = ops.shift(); + if (!arg) return null; + result = matchOnce(arg, pat, result, options); } - } else { - const arg = ops.shift(); - if (!arg) return null; - result = matchOnce(arg, pat, result, options); + + if (result === null) return null; + i += 1; } + // If there are some arguments left in the subject that were not matched, it's + // not a match + if (ops.length > 0) return null; - if (result === null) return null; - i += 1; - } - // If there are some arguments left in the subject that were not matched, it's - // not a match - if (ops.length > 0) return null; + return result; - return result; + /* + * Local f. + */ + /** + * + * Capture *qty* of remaining expessions/operands from the beginning of `ops` & return an an + * appropriate expression, considering the containing expression operator. + * + * (Assumes that, given a `qty` of 0, the match *must* have been from an optional + * sequence-wildcard match.) + * + * @param qty + * @returns + */ + function captureOps(qty: number): BoxedExpression { + let value: BoxedExpression; + if (qty < 1) { + // Otherwise must be an optional-sequence match: this is permitted. + if (expr.operator === 'Add') value = ce.Zero; + else if (expr.operator === 'Multiply') value = ce.One; + else value = ce.Nothing; + } else if (qty === 1) { + // Capturing a single element/operand + value = ops.shift()!; + } else { + // >1 operands captured + const def = ce.lookupDefinition(expr.operator); + const args = ops.splice(0, qty); + if (def && isOperatorDef(def) && def.operator.associative) { + value = ce.function(expr.operator, args, { canonical: false }); + } else { + value = ce.function('Sequence', args, { canonical: false }); + } + } + + return value; + } + } } /** @@ -411,6 +625,13 @@ function matchArguments( * * If the expression does not match the pattern, it returns `null`. * + * + * */ export function match( subject: BoxedExpression, diff --git a/src/compute-engine/boxed-expression/rules.ts b/src/compute-engine/boxed-expression/rules.ts index d94fdc80..385f672b 100755 --- a/src/compute-engine/boxed-expression/rules.ts +++ b/src/compute-engine/boxed-expression/rules.ts @@ -1,5 +1,6 @@ import type { Expression } from '../../math-json/types'; +import { _BoxedExpression } from './abstract-boxed-expression'; import type { BoxedRule, BoxedRuleSet, @@ -353,7 +354,10 @@ function parseRulePart( ); return expr; } - return ce.box(rule, { canonical: options?.canonical ?? false }); + const canonical = + options?.canonical ?? + (rule instanceof _BoxedExpression ? rule.isCanonical : false); + return ce.box(rule, { canonical }); } /** A rule can be expressed as a string of the form @@ -415,7 +419,7 @@ function parseRule( // Check for conditions const conditions = parseModifierExpression(parser); - if (conditions === null) return null; + if (conditions === null) return `${prefix}${id}`; if (!wildcardConditions[id]) wildcardConditions[id] = conditions; else wildcardConditions[id] += ',' + conditions; @@ -689,8 +693,7 @@ export function applyRule( substitution: BoxedSubstitution, options?: Readonly> ): RuleStep | null { - const canonical = - options?.canonical ?? (expr.isCanonical || expr.isStructural); + let canonical = options?.canonical ?? (expr.isCanonical || expr.isStructural); let operandsMatched = false; @@ -703,8 +706,20 @@ export function applyRule( return subExpr.value; }); - if (operandsMatched) + // 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 = expr.engine.function(expr.operator, newOps, { canonical }); + } } // eslint-disable-next-line prefer-const @@ -713,12 +728,11 @@ export function applyRule( if (canonical && match) { const awc = getWildcards(match); - const originalMatch = match; - match = match.canonical; - const bwc = getWildcards(match); + const canonicalMatch = match.canonical; + const bwc = getWildcards(canonicalMatch); if (!awc.every((x) => bwc.includes(x))) throw new Error( - `\n| Invalid rule "${rule.id}"\n| The canonical form of ${dewildcard(originalMatch).toString()} is "${dewildcard(match).toString()}" and it does not contain all the wildcards of the original match.\n| This could indicate that the match expression in canonical form is already simplified and this rule may not be necessary` + `\n| Invalid rule "${rule.id}"\n| The canonical form of ${dewildcard(canonicalMatch).toString()} is "${dewildcard(match).toString()}" and it does not contain all the wildcards of the original match.\n| This could indicate that the match expression in canonical form is already simplified and this rule may not be necessary` ); } @@ -728,7 +742,7 @@ export function applyRule( onBeforeMatch?.(rule, expr); const sub = match - ? expr.match(match, { substitution, ...options, useVariations }) + ? expr.match(match, { substitution, useVariations, recursive: false }) : {}; // If the `expr` does not match the pattern, the rule doesn't apply @@ -762,6 +776,16 @@ export function applyRule( } } + // Have a (direct) match: in this case, consider the canonical-status of the replacement, too. + if ( + !canonical && + options?.canonical === undefined && + replace instanceof _BoxedExpression && + replace.isCanonical + ) + canonical = true; + + //@note: '.subs()' acts like an expr. 'clone' here (in case of an empty substitution) const result = typeof replace === 'function' ? replace(expr, sub) @@ -775,6 +799,8 @@ export function applyRule( if (isRuleStep(result)) return canonical ? { ...result, value: result.value.canonical } : result; + // (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 }; } @@ -783,7 +809,7 @@ export function applyRule( * and the set of rules that were applied. * * The `replace` function can be used to apply a rule to a non-canonical - * expression. @fixme: account for options.canonical + * expression. * */ export function replace( diff --git a/src/compute-engine/boxed-expression/validate.ts b/src/compute-engine/boxed-expression/validate.ts index 6e532f5b..1473c9be 100644 --- a/src/compute-engine/boxed-expression/validate.ts +++ b/src/compute-engine/boxed-expression/validate.ts @@ -225,6 +225,15 @@ export function checkPure( * * Otherwise return a list of expressions indicating the mismatched * arguments. + * + * * */ export function validateArguments( diff --git a/src/compute-engine/global-types.ts b/src/compute-engine/global-types.ts index 20254cbc..c062efbd 100644 --- a/src/compute-engine/global-types.ts +++ b/src/compute-engine/global-types.ts @@ -1063,6 +1063,10 @@ export interface BoxedExpression { * * :::info[Note] * Applicable to canonical and non-canonical expressions. + * + * If this is a function, an empty substitution is given, and the computed value of `canonical` + * does not differ from that of this expr.: then a call this method is analagous to requesting a + * *clone*. * ::: * */ @@ -1103,11 +1107,24 @@ export interface BoxedExpression { * * See also `expr.subs()` for a simple substitution of symbols. * - * If `options.canonical` is not set, the result is canonical if `this` - * is canonical. + * Procedure for the determining the canonical-status of the input expression and replacements: + * + * - If `options.canonical` is set, the *entire expr.* is canonicalized to this degree: whether + * the replacement occurs at the top-level, or within/recursively. + * + * - If otherwise, the *direct replacement will be canonical* if either the 'replaced' expression + * is canonical, or the given replacement (- is a BoxedExpression 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). * * :::info[Note] * Applicable to canonical and non-canonical expressions. + * + * To specify a match for single symbol (non wildcard), it must be boxed (e.g. `{ match: + * ce.box('x'), ... }`), but it is suggested to use method *'subs()'* for this. * ::: */ replace( @@ -1915,9 +1932,9 @@ export type JsonSerializationOptions = { /** * Control how a pattern is matched to an expression. * - * - `substitution`: if present, assumes these values for the named wildcards, - * and ensure that subsequent occurrence of the same wildcard have the same - * value. + * - `substitution`: if present, assumes these values for a subset of + * named wildcards, and ensure that subsequent occurrence of the same + * wildcard have the same value. * - `recursive`: if true, match recursively, otherwise match only the top * level. * - `useVariations`: if false, only match expressions that are structurally identical. @@ -2939,18 +2956,22 @@ export type RuleStep = { export type RuleSteps = RuleStep[]; /** - * A rule describes how to modify an expressions that matches a pattern `match` + * A rule describes how to modify an expression that matches a pattern `match` * into a new expression `replace`. * * - `x-1` \( \to \) `1-x` * - `(x+1)(x-1)` \( \to \) `x^2-1 * - * The patterns can be expressed as LaTeX strings or a MathJSON expressions. + * The patterns can be expressed as LaTeX strings or `SemiBoxedExpression`'s. + * Alternatively, match/replace logic may be specified by a `RuleFunction`, allowing both custom + * logic/conditions for the match, and either a *BoxedExpression* (or `RuleStep` if being + * descriptive) for the replacement. * * As a shortcut, a rule can be defined as a LaTeX string: `x-1 -> 1-x`. * The expression to the left of `->` is the `match` and the expression to the * right is the `replace`. When using LaTeX strings, single character variables - * are assumed to be wildcards. + * are assumed to be wildcards. The rule LHS ('match') and RHS ('replace') may also be supplied + * separately: in this case following the same rules. * * When using MathJSON expressions, anonymous wildcards (`_`) will match any * expression. Named wildcards (`_x`, `_a`, etc...) will match any expression diff --git a/test/compute-engine/patterns.test.ts b/test/compute-engine/patterns.test.ts index 3f34d5e9..3fe9cb00 100755 --- a/test/compute-engine/patterns.test.ts +++ b/test/compute-engine/patterns.test.ts @@ -1,11 +1,10 @@ -// import { expression, latex } from './utils'; - import { BoxedExpression, PatternMatchOptions, SemiBoxedExpression, Substitution, } from '../../src/compute-engine'; +import { _BoxedExpression } from '../../src/compute-engine/boxed-expression/abstract-boxed-expression'; import { Expression } from '../../src/math-json/types'; import { engine, latex } from '../utils'; @@ -16,9 +15,18 @@ function match( expr: BoxedExpression | Expression, options?: PatternMatchOptions ): Substitution | null { - const result = engine - .box(expr) - .match(ce.box(pattern), { useVariations: true, ...options }); + // Avoid re-boxing both expr & pattern so as to preserve canonical-status + expr = + expr instanceof _BoxedExpression ? (expr as BoxedExpression) : ce.box(expr); + pattern = + pattern instanceof _BoxedExpression + ? (pattern as BoxedExpression) + : ce.box(pattern); + + const result = expr.match(pattern, { + useVariations: true, + ...options, + }); if (result === null) return null; const r = {}; for (const key of Object.keys(result)) r[key] = result[key]; @@ -82,104 +90,663 @@ describe('Examples from Patterns and Rules guide', () => { `)); }); +// Should match *single expr.* only describe('PATTERNS MATCH - Universal wildcard', () => { - // Return not null (i.e. `{}`) when there is a match - const pattern: Expression = ['Add', 1, '__']; - test('Simple match', () => - expect(match(pattern, ['Add', 1, 2])).toMatchInlineSnapshot(`{}`)); - test('Commutative', () => - expect(match(pattern, ['Add', 2, 1])).toMatchInlineSnapshot(`{}`)); - test('Commutative, multiple ops', () => - expect(match(pattern, ['Add', 2, 1, 3])).toMatchInlineSnapshot(`{}`)); - // Associative - test('Associative', () => - expect(match(pattern, ['Add', 1, ['Add', 2, 3]])).toMatchInlineSnapshot( - `{}` - )); -}); + let pattern: Expression; + + describe('Named', () => { + pattern = ['Add', 1, '_q']; + + test('Simple match (literals)', () => { + expect(match(pattern, ['Add', 1, 2])).toMatchInlineSnapshot(` + { + _q: 2, + } + `); + expect(match(pattern, ['Add', 1, 'GoldenRatio'])).toMatchInlineSnapshot(` + { + _q: GoldenRatio, + } + `); + }); -describe('PATTERNS MATCH - Named wildcard', () => { - const pattern: Expression = ['Add', 1, '__a']; - test('Commutative wildcards', () => { - expect(match(pattern, ['Add', 1, 2])).toMatchInlineSnapshot(` - { - __a: 2, - } - `); - // Commutative - expect(match(pattern, ['Add', 2, 1])).toMatchInlineSnapshot(` - { - __a: 2, - } - `); - }); - test('Associative wildcards', () => { - expect(match(pattern, ['Add', 2, 1, 3])).toMatchInlineSnapshot(` - { - __a: ["Add", 2, 3], - } - `); - expect(match(pattern, ['Add', 1, ['Add', 2, 3]])).toMatchInlineSnapshot(` - { - __a: ["Add", 2, 3], - } - `); - }); -}); + test('Matches functions, tensors...', () => { + // Associative + expect(match(pattern, ['Add', 1, ['Delimiter', ['List', 2, 3]]])) + .toMatchInlineSnapshot(` + { + _q: ["List", 2, 3], + } + `); + + expect(match(pattern, ['Add', 1, ['Multiply', 2, 'j']])) + .toMatchInlineSnapshot(` + { + _q: ["Multiply", 2, "j"], + } + `); + + expect(match(pattern, ['Add', 1, ['List', 5, 6]])).toMatchInlineSnapshot(` + { + _q: ["List", 5, 6], + } + `); + }); -describe('PATTERNS MATCH - Sequence wildcard', () => { - test('Sequence wildcard at the end', () => { - expect(match(['Add', 1, '__a'], ['Add', 1, 2, 3, 4])) - .toMatchInlineSnapshot(` - { - __a: ["Add", 2, 3, 4], - } - `); - }); - test('Sequence wildcard in the middle', () => { - expect(match(['Add', 1, '__a', 4], ['Add', 1, 2, 3, 4])) - .toMatchInlineSnapshot(` - { - __a: ["Add", 2, 3], - } - `); + test('Commutative (should match operand permutations', () => { + expect(match(pattern, ['Add', 2, 1])).toMatchInlineSnapshot(` + { + _q: 2, + } + `); + expect(match(pattern, ['Add', 'x', 1])).toMatchInlineSnapshot(` + { + _q: x, + } + `); + }); + + test('Multiple wildcards', () => { + /* + * Commutative FN. + * + */ + pattern = ['Add', '_a', 'n', '_c']; + expect(match(pattern, ['Add', 'n', 3, 3])).toMatchInlineSnapshot(` + { + _a: 3, + _c: 3, + } + `); + + expect(match(pattern, ['Add', ['Multiply', 6, 'm'], 'n', ['Sqrt', 6]])) + .toMatchInlineSnapshot(` + { + _a: ["Multiply", 6, "m"], + _c: ["Sqrt", 6], + } + `); + + // Control + // --------- + // Absence of 'n' operand + expect(match(pattern, ['Add', 'm', 3, 3])).toMatchInlineSnapshot(`null`); + + // Non-matching repeat-match + pattern = ['Add', '_a', 'n', '_a']; + expect(match(pattern, ['Add', 7, 9, 'n'])).toMatchInlineSnapshot(`null`); + + /* + * Function-name + Operand simultaneously + * + */ + pattern = ['_n', '_a', 'x']; + expect(match(pattern, ['Power', '1', 'x'])).toMatchInlineSnapshot(` + { + _a: 1, + _n: Power, + } + `); + + expect(match(pattern, ['Power', ['Divide', '1', 'y'], 'x'])) + .toMatchInlineSnapshot(` + { + _a: ["Divide", 1, "y"], + _n: Power, + } + `); + + expect(match(pattern, ['Divide', 'q', 'x'])).toMatchInlineSnapshot(` + { + _a: q, + _n: Divide, + } + `); + + // Control + // --------- + // Not enough operands + expect(match(pattern, ['Negate', 'x'])).toMatchInlineSnapshot(`null`); + + // No 'x' + expect( + match(pattern, ['Add', 'w', ['Negate', 'x']]) + ).toMatchInlineSnapshot(`null`); + }); + + // @todo?: Repeated-match cases, too? + // (^some examples already in 'Examples from Patterns and Rules guide') }); - test('Sequence wildcard in the middle, matching nothing', () => { + + // Should return not null (i.e. `{}`) when there is a match + test('Non-named', () => { + /* + * Selection of tests from 'Named' (replaced with unnamed cards) + * + */ + pattern = ['Add', 1, '_']; + expect(match(pattern, ['Add', 1, 2])).toMatchInlineSnapshot(`{}`); + expect(match(pattern, ['Add', 1, 'GoldenRatio'])).toMatchInlineSnapshot( + `{}` + ); + + // Associative expect( - match(['Add', 1, 2, '__a', 3], ['Add', 1, 2, 3]) - ).toMatchInlineSnapshot(`null`); - }); + match(pattern, ['Add', 1, ['Delimiter', ['List', 2, 3]]]) + ).toMatchInlineSnapshot(`{}`); + expect( + match(pattern, ['Add', 1, ['Multiply', 2, 'j']]) + ).toMatchInlineSnapshot(`{}`); + expect(match(pattern, ['Add', 1, ['List', 5, 6]])).toMatchInlineSnapshot( + `{}` + ); + expect(match(pattern, ['Add', 2, 1])).toMatchInlineSnapshot(`{}`); - test('Optional sequence wildcard mathching 0', () => { - expect(match(['Add', 1, 2, '___a', 3], ['Add', 1, 2, 3])) - .toMatchInlineSnapshot(` - { - ___a: 0, - } - `); - }); + // Multiple wildcards + pattern = ['_', '_', 'x']; + expect(match(pattern, ['Power', '1', 'x'])).toMatchInlineSnapshot(`{}`); - test('Optional sequence wildcard matching 1', () => { - expect(match(['Multiply', 1, 2, '___a', 3], ['Multiply', 1, 2, 3])) - .toMatchInlineSnapshot(` - { - ___a: 1, - } - `); + pattern = ['Add', '_', 'n', '_']; + expect( + match(pattern, ['Add', ['Multiply', 6, 'm'], 'n', ['Sqrt', 6]]) + ).toMatchInlineSnapshot(`{}`); + + /* + * Some extras... + */ + pattern = ['Factorial', '_']; + // (note: err. in terms of type/signature) + expect(match(pattern, ['Factorial', 'a'])).toMatchInlineSnapshot(`{}`); + // Nested + pattern = ['Power', Infinity, ['Power', 'Pi', '_']]; + expect( + match(pattern, ['Power', Infinity, ['Power', 'Pi', 'e']]) + ).toMatchInlineSnapshot(`{}`); + // Function-name + pattern = ['_', 'g']; + expect(match(pattern, ['Negate', 'g'])).toMatchInlineSnapshot(`{}`); }); +}); - test('Sequence wildcard in the middle', () => { - expect(match(['Add', 1, 2, '__a', 4], ['Add', 1, 2, 3, 4])) - .toMatchInlineSnapshot(` - { - __a: 3, - } - `); +describe('PATTERNS MATCH - Sequence wildcards', () => { + let pattern: Expression; + + describe('Regular/non-optional sequence', () => { + describe('Named wildcard', () => { + test('Matches commutative-function operands', () => { + pattern = ['Add', 1, '__a']; + /* + * 1 Operand + * + */ + expect(match(pattern, ['Add', 1, 2])).toMatchInlineSnapshot(` + { + __a: 2, + } + `); + + /* + * >1 Operand + * + */ + // !@fix: + // Does not match... + // (further cases throughout sequence-wildcard matching are present, too. See instances of + // '@fix: (commutative matching)') + // !The canonicalized expr. (['Add', 'x', 1, 2, 3, 5]) neither matches against pattern + // permuation ['Add', 1, '__a'] or ['Add', '__a', 1]. + // (In other words, using permutations of patterns here is not sufficient to match what + // would otherwise be considered a valid match: given the context of commutativity.) + + // expect(match(pattern, ['Add', 1, 2, 3, 'x', 5])).toMatchInlineSnapshot( + // `null` + // ); + + /* + * Function-expression operand/s + * + */ + expect(match(pattern, ['Add', 1, ['Multiply', 2, 'x']])) + .toMatchInlineSnapshot(` + { + __a: ["Multiply", 2, "x"], + } + `); + + /* + * Operand permutations + */ + // !@fix: (commutative matching) + // expect(match(pattern, ['Add', 'x', 1, 3, 'y'])).toMatchInlineSnapshot( + // `null` + // ); + + expect(match(pattern, ['Add', ['Square', 'x'], 1])) + .toMatchInlineSnapshot(` + { + __a: ["Square", "x"], + } + `); + }); + + test('Matches associative-function operands', () => { + pattern = ['Multiply', '__m', ['Sqrt', 'x']]; + + expect(match(pattern, ['Multiply', 7, ['Sqrt', 'x']])) + .toMatchInlineSnapshot(` + { + __m: 7, + } + `); + + // !@fix: (commutative matching) + // expect( + // match(pattern, ['Multiply', 2, ['Sqrt', 'x'], ['Sqrt', 'x']]) + // ).toMatchInlineSnapshot(`null`); + + // Match by commutative-permutation + expect(match(pattern, ['Multiply', ['Sqrt', 'x'], ['Add', 2, 3]])) + .toMatchInlineSnapshot(` + { + __m: ["Add", 2, 3], + } + `); + + // Control + // (√y, instead of √x) + expect( + match(pattern, ['Multiply', 3, ['Sqrt', 'y']]) + ).toMatchInlineSnapshot(`null`); + }); + + test('Matches other function/categories', () => { + /* + * Non associative/commutative cases + */ + //(@note: matching for this expr. ('Subtract') to be be done for *non-canonical* variants: + //so as to not have Subtract canonicalized as Add) + pattern = ['Subtract', '__a', 'y', '_b']; + + //(?Prettified MathJSON results in ["Square", "z"] here... ?) + expect( + match( + ce.box(pattern, { canonical: false }), + ce.box(['Subtract', 'x', 'y', ['Power', 'z', 2]], { + canonical: false, + }) + ) + ).toMatchInlineSnapshot(` + { + __a: x, + _b: ["Square", "z"], + } + `); + + // Control/should be no-match + // ------------------------- + // Too many operands RHS (right of 'y') + expect( + match( + ce.box(ce.box(pattern, { canonical: false }), { canonical: false }), + ce.box(['Subtract', 'x', 'y', ['Power', 'z', 2], 'w'], { + canonical: false, + }) + ) + ).toMatchInlineSnapshot(`null`); + + // Missing capture of '__a' + expect( + match(ce.box(pattern, { canonical: false }), [ + 'Subtract', + 'y', + ['Power', 'z', 2], + ]) + ).toMatchInlineSnapshot(`null`); + + pattern = ['Max', '__a', 10]; + expect(match(pattern, ['Max', 1, 10])).toMatchInlineSnapshot(` + { + __a: 1, + } + `); + expect(match(pattern, ['Max', 1, ['Range', 3, 7, 2], 9, 10])) + .toMatchInlineSnapshot(` + { + __a: ["Sequence", 1, ["Range", 3, 7, 2], 9], + } + `); + + // Should not match + expect( + match(pattern, ['Max', 10, 9, 8]) //Non-commutative + ).toMatchInlineSnapshot(`null`); + }); + }); + + describe('Non-named wildcard', () => { + test(`Matches, but without capturing (substitutions)`, () => { + /* + * (Selection of cases from 'Named wildcard': but unnamed sequence cards) + */ + + pattern = ['Add', 1, '__']; + + expect(match(pattern, ['Add', 1, 2])).toMatchInlineSnapshot(`{}`); + + // !@fix: (commutative matching; see prior cases) + // expect(match(pattern, ['Add', 1, 2, 3, 'x', 5])).toMatchInlineSnapshot( + // `null` + // ); + + // !@fix: (commutative matching; see prior cases) + // expect(match(pattern, ['Add', 'x', 1, 3, 'y'])).toMatchInlineSnapshot( + // `null` + // ); + + pattern = ['Multiply', '__', ['Sqrt', 'x']]; + + // !@fix: (commutative matching; see prior cases) + // expect( + // match(pattern, ['Multiply', 2, ['Sqrt', 'x'], ['Sqrt', 'x']]) + // ).toMatchInlineSnapshot(`null`); + + pattern = ['Subtract', '__', 'y', '_']; + + expect( + match( + ce.box(pattern, { canonical: false }), + ce.box(['Subtract', 'x', 'y', ['Power', 'z', 2]], { + canonical: false, + }) + ) + ).toMatchInlineSnapshot(`{}`); + }); + }); + + test(`Matches operands which match further wildcards`, () => { + /* + * Case 1 + * + */ + pattern = ['Add', ['Power', 'x', '_'], '__w']; + + expect( + match(pattern, ['Add', ['Power', 'x', 'ExponentialE'], 'ImaginaryUnit']) + ).toMatchInlineSnapshot(` + { + __w: ImaginaryUnit, + } + `); + expect(match(pattern, ['Add', ['Square', 'y'], ['Power', 'x', 2], 3])) + .toMatchInlineSnapshot(` + { + __w: ["Add", ["Square", "y"], 3], + } + `); + + // No match (non-matching Power) + expect( + match(pattern, ['Add', ['Power', 'y', 3], 'z']) + ).toMatchInlineSnapshot(`null`); + + // No match (well; additive identity) + expect(match(pattern, ['Add', ['Power', 'x', 2]])).toMatchInlineSnapshot(` + { + __w: 0, + } + `); + + /* + * Case 2 + * + */ + pattern = ['Multiply', ['Log', '__'], '_z', 10]; + + expect(match(pattern, ['Multiply', ['Log', 64, 8], 'y', 10])) + .toMatchInlineSnapshot(` + { + _z: y, + } + `); + expect(match(pattern, ['Multiply', 3, 10, ['Log', 100]])) + .toMatchInlineSnapshot(` + { + _z: 3, + } + `); + + // No match (Missing '_z' operand) + expect( + match(pattern, ['Multiply', ['Log', '__'], 10]) + ).toMatchInlineSnapshot(`null`); + }); + + test(`Varying wildcard (operand) positions`, () => { + /* + * + * Non-commutative FN's. + * + */ + // At end + expect(match(['Max', 1, '__a'], ['Max', 1, 2, 3, 4])) + .toMatchInlineSnapshot(` + { + __a: ["Sequence", 2, 3, 4], + } + `); + + // Placed in middle + // ('Sequence wildcard in the middle, full of sound and fury, signifying nothing' 😆) + expect( + match( + ['GCD', '_', '__a', 18], + ['GCD', ['Factorial', 6], ['Power', 6, 3], ['Subtract', 74, 2], 18] + ) + ).toMatchInlineSnapshot(` + { + __a: ["Sequence", ["Power", 6, 3], ["Subtract", 74, 2]], + } + `); + + // Placed at beginning + expect( + match( + //@note: non-canonical for both, because do not want Subtract to become 'Add' + ce.box(['Subtract', '__s', 5], { canonical: false }), + ce.box(['Subtract', 8, 7, 6, 5], { canonical: false }) + ) + ).toMatchInlineSnapshot(` + { + __s: ["Sequence", 8, 7, 6], + } + `); + + // Controls + // --------- + expect(match(['Max', 1, '__a'], ['Max', 2, 3, 4])).toMatchInlineSnapshot( + `null` + ); + + expect( + match(['LCM', '_', '__a', 18], ['LCM', ['Factorial', 6], 18]) + ).toMatchInlineSnapshot(`null`); + + expect(match(['Random', '__R'], ['Random'])).toMatchInlineSnapshot( + `null` + ); + }); + + test(`Multiple sequence wildcards (for one set of operands)`, () => { + /* + * Non-commutative. + * + */ + pattern = ['Tuple', '__t', ['_', '__'], '__q']; + + expect(match(pattern, ['Tuple', ['Sqrt', 'x'], ['Add', 2, 3], 'y'])) + .toMatchInlineSnapshot(` + { + __q: y, + __t: ["Sqrt", "x"], + } + `); + + // 3+ seq. + pattern = ['List', 1, '__a', 4, '__b', 7, '__c']; + expect(match(pattern, ['List', 1, 2, 3, 4, 5, 6, 7, 8])) + .toMatchInlineSnapshot(` + { + __a: ["Sequence", 2, 3], + __b: ["Sequence", 5, 6], + __c: 8, + } + `); // 👍 + + // With universal wildcards, too. + pattern = [ + 'Set', + "'some text'", + '_', + '__a', + ['Less', '_', '_'], + '__b', + 9, + ]; + expect( + match(pattern, [ + 'Set', + "'some text'", + 'x', + 'y', + ['Less', 'x', 'y'], + ['About', 'RandomExpression'], + 9, + ]) + ).toMatchInlineSnapshot(` + { + __a: y, + __b: ["About", "RandomExpression"], + } + `); // 👍 + + // Controls + // ---------- + //No match for '__q' + pattern = ['Tuple', '__t', ['_', '__'], '__q']; + expect( + match(pattern, ['Tuple', ['Sqrt', 'x'], ['Add', 2, 3]]) + ).toMatchInlineSnapshot(`null`); + + //Missing middle sequence ('_b_') + pattern = ['List', 1, '__a', 4, '__b', 7, '__c']; + expect( + match(pattern, ['List', 1, 2, 3, 5, 6, 7, 8]) + ).toMatchInlineSnapshot(`null`); + + /* + * Commutative. + * + */ + //!@feat?: this use-case (multiplt seq.-cards) illustrates the utility of a 'matchPermutations' + //!(Replace) option + + //@todo + }); }); - test('Sequence wildcard matching nothing', () => { - expect( - match(['Add', 1, 2, '__a', 3], ['Add', 1, 2, 3]) - ).toMatchInlineSnapshot(`null`); + + describe(`Optional sequence`, () => { + test('Matches nothing', () => { + /* + * Named optional-sequence + */ + expect(match(['List', 1, 2, '___a', 3], ['List', 1, 2, 3])) + .toMatchInlineSnapshot(` + { + ___a: Nothing, + } + `); + + /* + * Un-named optional-sequence + */ + expect(match(['Log', '_l', '___'], ['Log', ['Power', 10, 10]])) + .toMatchInlineSnapshot(` + { + _l: ["Power", 10, 10], + } + `); + + // Matches nothing, twice (because subsequent to regular/non-optional sequence, which should + // 'greedily' match) + expect( + match( + ['Tuple', 1, '__u', '___v', 4, '__w', '___x', 7], + ['Tuple', 1, 2, 3, 4, 5, 6, 7] + ) + ).toMatchInlineSnapshot(` + { + ___v: Nothing, + ___x: Nothing, + __u: ["Sequence", 2, 3], + __w: ["Sequence", 5, 6], + } + `); // 👍 + }); + + test('Matches >1 operands', () => { + //i.e. behaves like an ordinary sequence wildcard + expect( + match(['Add', '___', 3, '___'], ['Add', 'x', 3, 'Pi']) + ).toMatchInlineSnapshot(`{}`); + + expect( + match( + ['Matrix', ['List', '___m', ['List', 7.1]]], + [ + 'Matrix', + [ + 'List', + ['List', 9.3], + ['List', ['Complex', 6, 3.1]], + ['List', 7.1], + ], + ] + ) + ).toMatchInlineSnapshot(` + { + ___m: ["Sequence", ["List", 9.3], ["List", ["Complex", 6, 3.1]]], + } + `); + }); + + test("Special case: matches '0' (as additive identity)", () => { + expect(match(['Add', 1, 2, '___a', 3], ['Add', 1, 2, 3])) + .toMatchInlineSnapshot(` + { + ___a: 0, + } + `); + + // Control (Unnamed wildcard: so empty subst.) + expect( + match(['Add', 1, 2, '___', 3], ['Add', 1, 2, 3]) + ).toMatchInlineSnapshot(`{}`); + }); + + test("Special case: matches '1' (as multiplicative identity)", () => { + // (Two optional seqs., too...) + expect( + match( + ['Multiply', '___u', 'q', 'r', 's', '___v'], + ['Multiply', 'q', 'r', 's'] + ) + ).toMatchInlineSnapshot(` + { + ___u: 1, + ___v: 1, + } + `); + }); }); }); @@ -257,7 +824,7 @@ describe('NOT SAME', () => { }); describe('WILDCARDS', () => { - it('should match a wildcard', () => { + it('number should match a wildcard', () => { const result = match('_x', ce.box(1)); expect(result).toMatchInlineSnapshot(` { @@ -266,7 +833,7 @@ describe('WILDCARDS', () => { `); }); - it('should match a wildcard', () => { + it('symbol should match a wildcard', () => { const result = match('_x', ce.box('a')); expect(result).toMatchInlineSnapshot(` { @@ -275,7 +842,7 @@ describe('WILDCARDS', () => { `); }); - it('should match a wildcard', () => { + it('function should match a wildcard', () => { const result = match('_x', ce.box(['Add', 1, 'a'])); expect(result).toMatchInlineSnapshot(` { @@ -284,7 +851,7 @@ describe('WILDCARDS', () => { `); }); - it('should match a wildcard of a commutative function', () => { + it('wildcard matched as an argument of a commutative function', () => { const result = match(['Add', '_x', 1], ce.box(['Add', 1, 'a'])); expect(result).toMatchInlineSnapshot(` {