| 1247 | return { |
| 1248 | ...result, |
| 1249 | value: computeValue(result.value), |
| 1250 | purpose: result.purpose ?? purpose, |
| 1251 | }; |
| 1252 | } |
| 1253 | |
| 1254 | if (!isExpression(result)) { |
| 1255 | throw new Error( |
| 1256 | 'Invalid rule replacement result: expected a Expression or RuleStep' |
| 1257 | ); |
| 1258 | } |
| 1259 | |
| 1260 | return stepOf(computeValue(result)); |
| 1261 | } |
| 1262 | |
| 1263 | /** |
| 1264 | * Apply the rules in the ruleset and return a modified expression |
| 1265 | * and the set of rules that were applied. |
| 1266 | * |
| 1267 | * The `replace` function can be used to apply a rule to a non-canonical |
| 1268 | * expression. |
| 1269 | * |
| 1270 | * **Error handling contract.** A single misbehaving rule never aborts the |
| 1271 | * whole pass. An exception thrown while checking a rule's `condition` or while |
| 1272 | * running its `replace` function is logged and that one rule is skipped; the |
| 1273 | * remaining rules are still tried (see `applyRule`). Only a |
| 1274 | * `CancellationError` (deadline/interrupt) propagates out, so timeouts are not |
| 1275 | * swallowed as "the rule failed". |
| 1276 | * |
| 1277 | * **Ordering contract** (SYMBOLIC P3-14). Rules are tried in declaration |
| 1278 | * order. Within a pass, after rule *k* fires, only rules with ordinal > k |
| 1279 | * are tried on the result — earlier rules do not see later rules' output |
| 1280 | * unless another iteration runs. The default `iterationLimit` is **1**, so |
| 1281 | * by default there is a single pass; pass a larger `iterationLimit` (or use |
| 1282 | * `simplify()`, which iterates to a fixed point with its own guards) when |
| 1283 | * rules are meant to feed each other. |
| 1284 | * |
| 1285 | * **Capture convention** (SYMBOLIC P3-13). When several distinct bindings |
| 1286 | * of a pattern's sequence wildcards would match, which one is produced is |
| 1287 | * operator-dependent: the commutative-anchor path and the plain |
| 1288 | * argument-list path resolve greedy-vs-lazy differently. Both results are |
| 1289 | * valid matches; replacements built from captures should not rely on a |
| 1290 | * specific split. |
| 1291 | */ |
| 1292 | export function replace( |
| 1293 | expr: Expression, |
| 1294 | rules: Rule | (Rule | BoxedRule)[] | BoxedRuleSet, |
| 1295 | options?: Partial<ReplaceOptions> |
| 1296 | ): RuleSteps { |
| 1297 | if (!rules) throw new Error('replace(): Expected one or more rules'); |
| 1298 | |
| 1299 | const iterationLimit = options?.iterationLimit ?? 1; |
| 1300 | let iterationCount = 0; |
| 1301 | const once = options?.once ?? false; |
| 1302 | normalizeReplaceForm(options); |
| 1303 | |
| 1304 | // Normalize the ruleset |
| 1305 | let ruleSet: ReadonlyArray<BoxedRule>; |
| 1306 | if (typeof rules === 'object' && 'rules' in rules) ruleSet = rules.rules; |