Sort out and refactor reduction scanner system #2

Open
opened 2026-05-14 18:20:21 +00:00 by mikael-lovqvist · 0 comments

Note

This note was composed by Claude Code, Sonnet 4.6 (Anthropic)

Reduction System Design Notes

Problem at hand

The reduction scanner needs to support hierarchical rule groups (precedence levels) while remaining composable and traceable. A flat rule list (reduction-scanner-1/2) works for simple cases but doesn't express same-level POSITION_MAJOR semantics cleanly — declaration order within a level shouldn't affect which operator wins.

What reduction-scanner-3 demonstrates

Rules are organized into precedence levels via sub_system(...). The outer scanner is RULE_MAJOR — it tries each level in order until one matches. Each level is a Sub_Scan_Rule wrapping an inner POSITION_MAJOR scanner over its own rules. This gives correct left-to-right associativity within a level regardless of rule declaration order.

rss.rules.push(
    sub_system(rule_caret, rule_caron),    // highest precedence
    sub_system(rule_asterisk, rule_slash),
    sub_system(rule_plus, rule_minus),     // lowest precedence
)

Current implementation — what we have

Classes in the experiment:

  • Rule(condition, handler) — pairs a condition with an opaque handler function. match(sequence) returns a Rule_Match or null.
  • Rule_Match(rule, match) — wraps a rule and its condition match. action getter returns rule.handler.
  • Sub_Scan_Rule(sub_system) — delegates match() to inner scanner's find_reduction_candidate. Returns a Sub_Scan_Rule_Match.
  • Sub_Scan_Rule_Match(rule, sub_scan_candidate) — wraps the outer rule and the inner candidate. action getter chains into sub_scan_candidate.match.action. match getter chains into sub_scan_candidate.match.match.

In reduction-scanner.mjs:

  • find_reduction_candidate(sequence) — returns { sequence, rule, match } where match is a Rule_Match
  • perform_reduction — destructures candidate, calls match.action({ reduction_system, rule, sequence, match: match.match })

Migration: positional → keyed action callbacks

Previously actions were called as rule.action(scanner, sequence, match) — positional arguments. This doesn't scale as the context grows and makes refactoring fragile. The current direction is a single keyed object:

handler({ reduction_system, sequence, match, rule, ... })

This lets handlers destructure only what they need, and new fields can be added to the context without breaking existing handlers.

Problems with current shapes

  1. Double .matchcandidate.match is a Rule_Match, so the underlying condition match is candidate.match.match. Two levels of the same name.
  2. rule is redundantcandidate.rule and candidate.match.rule are the same object in normal cases.
  3. Promotion is implicitSub_Scan_Rule_Match chains into the inner candidate via getters, but if the inner candidate gains new fields there's no explicit decision about which propagate outward.
  4. Traceability — when an error occurs inside a nested rule, there's no record of the path taken through the rule hierarchy.

Proposed unified approach

Reduction_Candidate(match_start, match_end, rule_chain, match, sequence)

A single defined shape returned by any rule.match(sequence), at any nesting level:

  • match_start, match_end — position in sequence, same as the leaf condition match
  • rule_chain — array of rules from outermost to innermost, rule_chain.at(-1) is the leaf rule that actually matched
  • match — the raw condition match from the leaf condition
  • sequence — reference to the sequence being reduced (useful in action context and error reporting)
get rule() { return this.rule_chain.at(-1); }

nest(outer_rule) {
    // match_start/match_end are always identical between inner and outer
    // since Sub_Scan_Rule delegates entirely — assertion could guard this
    return new Reduction_Candidate(
        this.match_start, this.match_end,
        [outer_rule, ...this.rule_chain],
        this.match, this.sequence
    );
}

Rule.match(sequence) — creates a fresh candidate:

const match = this.condition.match(sequence);
if (match) return new Reduction_Candidate(match.match_start, match.match_end, [this], match, sequence);

Sub_Scan_Rule.match(sequence) — gets inner candidate, nests it:

const inner = this.sub_system.find_reduction_candidate(sequence);
return inner?.nest(this) ?? null;

perform_reduction — dispatches via leaf rule, passes full candidate as context:

const candidate = this.find_reduction_candidate(sequence);
if (candidate) {
    candidate.rule.action({ reduction_system: this, ...candidate });
    return true;
}

Action handlers receive { reduction_system, match_start, match_end, rule_chain, match, sequence } — fully traceable, no positionals, nesting is transparent.

Traceability via rule_chain

For a nested match on 10 ^ 5:

rule_chain = [sub_system_level_0, rule_caret]

rule_chain[0] identifies the precedence level, rule_chain.at(-1) is the specific rule. Arbitrary nesting depth works the same way — always walk from outermost to leaf. Error messages can render the full chain without manually traversing the sub-system graph.

Compatibility with flat rule systems

The flat case (no sub-systems) produces candidates with rule_chain = [rule] — a chain of length one. All existing experiment patterns still work, just with the new candidate shape and keyed action callbacks.

> [!NOTE] > This note was composed by Claude Code, Sonnet 4.6 (Anthropic) ## Reduction System Design Notes ### Problem at hand The reduction scanner needs to support hierarchical rule groups (precedence levels) while remaining composable and traceable. A flat rule list (reduction-scanner-1/2) works for simple cases but doesn't express same-level POSITION_MAJOR semantics cleanly — declaration order within a level shouldn't affect which operator wins. ### What reduction-scanner-3 demonstrates Rules are organized into precedence levels via `sub_system(...)`. The outer scanner is RULE_MAJOR — it tries each level in order until one matches. Each level is a `Sub_Scan_Rule` wrapping an inner POSITION_MAJOR scanner over its own rules. This gives correct left-to-right associativity within a level regardless of rule declaration order. ```js rss.rules.push( sub_system(rule_caret, rule_caron), // highest precedence sub_system(rule_asterisk, rule_slash), sub_system(rule_plus, rule_minus), // lowest precedence ) ``` ### Current implementation — what we have **Classes in the experiment:** - `Rule(condition, handler)` — pairs a condition with an opaque handler function. `match(sequence)` returns a `Rule_Match` or null. - `Rule_Match(rule, match)` — wraps a rule and its condition match. `action` getter returns `rule.handler`. - `Sub_Scan_Rule(sub_system)` — delegates `match()` to inner scanner's `find_reduction_candidate`. Returns a `Sub_Scan_Rule_Match`. - `Sub_Scan_Rule_Match(rule, sub_scan_candidate)` — wraps the outer rule and the inner candidate. `action` getter chains into `sub_scan_candidate.match.action`. `match` getter chains into `sub_scan_candidate.match.match`. **In `reduction-scanner.mjs`:** - `find_reduction_candidate(sequence)` — returns `{ sequence, rule, match }` where `match` is a `Rule_Match` - `perform_reduction` — destructures candidate, calls `match.action({ reduction_system, rule, sequence, match: match.match })` ### Migration: positional → keyed action callbacks Previously actions were called as `rule.action(scanner, sequence, match)` — positional arguments. This doesn't scale as the context grows and makes refactoring fragile. The current direction is a single keyed object: ```js handler({ reduction_system, sequence, match, rule, ... }) ``` This lets handlers destructure only what they need, and new fields can be added to the context without breaking existing handlers. ### Problems with current shapes 1. **Double `.match`** — `candidate.match` is a `Rule_Match`, so the underlying condition match is `candidate.match.match`. Two levels of the same name. 2. **`rule` is redundant** — `candidate.rule` and `candidate.match.rule` are the same object in normal cases. 3. **Promotion is implicit** — `Sub_Scan_Rule_Match` chains into the inner candidate via getters, but if the inner candidate gains new fields there's no explicit decision about which propagate outward. 4. **Traceability** — when an error occurs inside a nested rule, there's no record of the path taken through the rule hierarchy. ### Proposed unified approach **`Reduction_Candidate(match_start, match_end, rule_chain, match, sequence)`** A single defined shape returned by any `rule.match(sequence)`, at any nesting level: - `match_start`, `match_end` — position in sequence, same as the leaf condition match - `rule_chain` — array of rules from outermost to innermost, `rule_chain.at(-1)` is the leaf rule that actually matched - `match` — the raw condition match from the leaf condition - `sequence` — reference to the sequence being reduced (useful in action context and error reporting) ```js get rule() { return this.rule_chain.at(-1); } nest(outer_rule) { // match_start/match_end are always identical between inner and outer // since Sub_Scan_Rule delegates entirely — assertion could guard this return new Reduction_Candidate( this.match_start, this.match_end, [outer_rule, ...this.rule_chain], this.match, this.sequence ); } ``` **`Rule.match(sequence)`** — creates a fresh candidate: ```js const match = this.condition.match(sequence); if (match) return new Reduction_Candidate(match.match_start, match.match_end, [this], match, sequence); ``` **`Sub_Scan_Rule.match(sequence)`** — gets inner candidate, nests it: ```js const inner = this.sub_system.find_reduction_candidate(sequence); return inner?.nest(this) ?? null; ``` **`perform_reduction`** — dispatches via leaf rule, passes full candidate as context: ```js const candidate = this.find_reduction_candidate(sequence); if (candidate) { candidate.rule.action({ reduction_system: this, ...candidate }); return true; } ``` Action handlers receive `{ reduction_system, match_start, match_end, rule_chain, match, sequence }` — fully traceable, no positionals, nesting is transparent. ### Traceability via `rule_chain` For a nested match on `10 ^ 5`: ``` rule_chain = [sub_system_level_0, rule_caret] ``` `rule_chain[0]` identifies the precedence level, `rule_chain.at(-1)` is the specific rule. Arbitrary nesting depth works the same way — always walk from outermost to leaf. Error messages can render the full chain without manually traversing the sub-system graph. ### Compatibility with flat rule systems The flat case (no sub-systems) produces candidates with `rule_chain = [rule]` — a chain of length one. All existing experiment patterns still work, just with the new candidate shape and keyed action callbacks.
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: efforting.tech/nodejs.esm-library#2