Skip to content

A rec pass that never settles is stopped with a message (4.2.47) - #803

Merged
ddehilster merged 1 commit into
VisualText:masterfrom
ddehilster:fix/rec-limit
Oct 26, 2026
Merged

ddehilster merged 1 commit into
VisualText:masterfrom
ddehilster:fix/rec-limit

Conversation

@ddehilster

@ddehilster ddehilster commented Oct 26, 2026 •

Copy link
Copy Markdown
Member

Problem

A rec pass repeats a region while any rule matches. A rule that keeps matching without changing what it matched loops forever, and nothing appears on the console. parse-en-us hung this way on "much I have been to blame." (fixed in VisualText/parse-en-us#10). I found it when Pride and Prejudice never finished.

The engine already has a loop check, RecinfLoop from 2005, but it didn't catch this:

  • it only counts a match that rebuilds a node over the same span;
  • it skips matches that regroup or excise nodes, which is what this loop did;
  • the basic RecmatchNodes variant has no check at all;
  • when it does trigger, it writes only to err.log and abandons the whole pass.

Change

All four rec match loops now count matches per region. Past the limit, the region is stopped, the rest of the pass goes on, and one line goes to stderr and err.log, once per input for each pass and rule:

[Error in rec pass 75 (pos50): rules matched 1120 times in a region of 6 nodes without settling,
 so the region was stopped there. Last rule to match: line 15004. Input: .../w1.txt.
 NLP_REC_LIMIT sets the limit; 0 lifts it.]

Limit: 1,000 matches plus 20 per node in the region. I measured the largest match-to-node ratio in any region across all 18 analyzers in the analyzers repo and the 738 KB novel:

Run Worst region
17 analyzers on their own inputs ≤ 1.0 match per node
parse-en-us on Pride and Prejudice 2.27 (102 matches, 45 nodes)

So the per-node allowance is about nine times the worst case seen. The fixed 1,000 covers small regions.

  • NLP_REC_LIMIT scales the limit (default 1000), and 0 turns it off.
  • The engine's own rules-file grammar (data/rfb) isn't limited. It reads a whole pass file as one region: loading the 17,226-line pos50.nlp takes about 25,000 matches. A new Parse::rulesFile_ flag, set in rfa.cpp, marks those parses.

Tests

  • New fixture tests/rec-limit: a rec pass that takes 3 matches in a 6-node region. The new CI step on Windows, Linux and macOS checks that:
    • at the default it runs silently (matches=3);
    • at NLP_REC_LIMIT=1 it is stopped at 2, with the message, and the next pass still runs.
  • On master that step fails: matches=3, no message.
  • All Windows fixture steps pass locally.
  • parse-en-us output on its three inputs is byte-identical to master's (454 files).
  • No limit errors on any of the 18 analyzers or the novel at the default.
  • The unfixed parse-en-us now stops in about a second, naming pass 75 (pos50) and rule line 15004, where before it hung forever.

Version 4.2.47 (rebased onto 4.2.45, zone mode). #802 is 4.2.46; whichever merges second needs its version line rebased. I plan to release both under one tag.

🤖 Generated with Claude Code

@ddehilster ddehilster changed the title A rec pass that never settles is stopped with a message (4.2.44) A rec pass that never settles is stopped with a message (4.2.47) Oct 26, 2026
A rec pass repeats a region while any rule matches. A rule that keeps
matching without changing what it matched loops forever: parse-en-us
hung on "much I have been to blame." (fixed in parse-en-us VisualText#10), with
nothing on the console. RecinfLoop (2005) only counts a match that
rebuilds a node over the same span, and skips one that regroups or
excises, so it never saw this one; the basic RecmatchNodes has no check
at all.

All four rec match loops now count matches per region. Past the limit
the region is stopped, the rest of the pass goes on, and one line goes
to stderr and err.log, once per input for each pass and rule:

  [Error in rec pass 75 (pos50): rules matched 1120 times in a region of
   6 nodes without settling, so the region was stopped there. Last rule
   to match: line 15004. Input: .../w1.txt. NLP_REC_LIMIT sets the limit;
   0 lifts it.]

The limit is 1,000 matches plus 20 per node in the region. Measured over
all 18 analyzers in the analyzers repo and a 738 KB novel, no region took
more than 2.3 matches a node. NLP_REC_LIMIT scales it (the default is
1000) and 0 lifts it. The engine's own rules-file grammar is not
limited: it reads a whole pass file as one region (pos50.nlp is ~25,000
matches), so Parse::rulesFile_ marks those parses.

CI: tests/rec-limit has a rec pass that takes 3 matches in a 6-node
region. At the default it runs silently; at NLP_REC_LIMIT=1 it is
stopped at 2 with the message and the next pass still runs. On master
that check fails (matches=3, no message).

Checked: all Windows fixture steps pass; parse-en-us output on its three
inputs is byte-identical to master's (454 files); no limit errors on any
of the 18 analyzers or the novel; the unfixed parse-en-us now stops in
about a second where it hung.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ddehilster
ddehilster merged commit d307e08 into VisualText:master Oct 26, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant