Skip to content

perf(parse): an operator is looked up by its first character, and a dotted name is checked by its dots - #2061

Merged
nicolas-abril merged 5 commits into
bendlang:mainfrom
nicolas-abril:perf/parser-fast-reject
Nov 5, 2026
Merged

nicolas-abril merged 5 commits into
bendlang:mainfrom
nicolas-abril:perf/parser-fast-reject

Conversation

@nicolas-abril

@nicolas-abril nicolas-abril commented Nov 5, 2026 •

Copy link
Copy Markdown
Collaborator

This PR edits bend2/bend.ts, which AGENTS.md marks as human-written. It was opened at Nicolas's request, as a proposal for the file's owner to review, rewrite or close.

Two hot spots in the parser, found while profiling why the generics_3200 checker cell sits at 1.12–1.21× its pin. Base plays no part: none of the checker benches imports Base, and swapping base.bend for the pin's version or an empty file leaves the time unchanged.

  1. Operator lookup (parse_term_ops). After every term the parser sliced the next three characters and shortened the slice until INFIX knew it. At most positions no operator starts, so that was three slices and three missed lookups each time: about 29 ms of generics_3200, the parser's hottest lines (the pin's operator search took about 13 ms). INFIX_BY_HEAD now lists the INFIX keys by first character, longest first: the parser reads the next character, tries only the keys it heads with startsWith (no slices), and a term ends at once when it heads none, end of input included. A new operator must be added there too.
  2. Name check (parse_lexeme). Every name was tested against the whole-name regex ^[A-Za-z_]\w*(\.[A-Za-z_]\w*)*$. The loop before it only consumes letters, digits, _ and . after a head character, so a name breaks the words-joined-by-dots rule exactly when a dot is not followed by a letter or _ (foo., a..b, Vec.2d). The check is now k.includes(".") && /\.(?![A-Za-z_])/.test(k): names without a dot never reach a regex, and the regex says the rule directly. It agrees with the old regex on every name the loop can produce up to 4 characters (14.1M names).

Both changes keep the parser's behaviour: the first picks the longest key that matches, as the old loop did, and returns exactly where it would have shrunk to ""; the second rejects exactly the names the old regex rejected.

Checker benches (local, min of 9 runs)

bench main this PR
generics_3200 0.397 s 0.380 s
defs_12800 0.316 s 0.288 s
proofs_3200 0.820 s 0.772 s
compute_1600 0.515 s 0.513 s
trees_400 0.631 s 0.625 s

On the cluster, generics_3200 read 1.09× in a verbose perf run under full gate load (it read 1.12–1.21× on main). Together with #2060 it should land about 1.03×.

Checks

  • tests/parse and tests/check (458 files) print identical output, error messages included.
  • INFIX_BY_HEAD holds exactly the 23 INFIX keys, each under its first character, longest first.
  • bend.ts 45425 → 45539 ttok (cap 48000).
  • Gates (_run.ts, 3 runs): repo 56/56; ping 81/83, failing 2 BendHub publish checks, as on main; test 1831/1838, failing 7 io/gfx tests with Permission denied, as on main: stale /tmp/bend_io_* files owned by another user on cluster nodes 58 and 72–78; perf 117–119/124, and main gives 118/124 on the same cluster at the same time: merkle and nbody's compile and GPU cells (1.23–1.54× on both) and noisy SEQ-CPU cells. The same base gave 124/124 earlier in the day.

🤖 Generated with Claude Code

nicolas-abril and others added 2 commits November 5, 2026 15:59
…otless name skips the name regex

parse_term_ops looked for an operator after every term by slicing the next
three characters and shortening the slice until INFIX knew it: at most
positions no operator starts, so that was three slices and three missed
lookups each time, the parser's hottest lines (about 29 ms of
generics_3200). A term now ends at once when the next character heads no
INFIX key (INFIX_HEADS, derived from the table).

parse_lexeme tested every name against the words-joined-by-dots regex;
the loop before it only takes letters, digits, _ and ., after a head, so
a name without a dot always matched. The regex now runs only on a name
with a dot.

Checker benches (local, min of 9): generics_3200 0.397 -> 0.376 s,
defs_12800 0.311 -> 0.293, proofs_3200 0.816 -> 0.770, compute_1600 and
trees_400 unchanged. tests/parse and tests/check (400 files) print the
same output, error messages included. bend.ts 45425 -> 45490 ttok.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The loop before the check only takes letters, digits, _ and . after a
head, so a name breaks the words-joined-by-dots rule exactly when a dot
is not followed by a letter or _ (foo., a..b, Vec.2d). The check now
says that, under the dot test, in place of the whole-name regex.
Equivalent on every name the loop can take up to 4 characters (14.1M);
tests/parse and tests/check print the same output. bend.ts 45490 ->
45478 ttok.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nicolas-abril nicolas-abril changed the title perf(parse): a term ends at once where no operator can start, and a dotless name skips the name regex perf(parse): a term ends at once where no operator can start, and a dotted name is checked by its dots Nov 5, 2026
nicolas-abril and others added 2 commits November 5, 2026 16:49
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ter heads

INFIX_BY_HEAD lists the INFIX keys by first character, longest first, and
replaces INFIX_HEADS: the parser tries only the keys the next character
heads, with startsWith instead of shrinking slices, and a term ends where
none matches, end of input included.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nicolas-abril nicolas-abril changed the title perf(parse): a term ends at once where no operator can start, and a dotted name is checked by its dots perf(parse): an operator is looked up by its first character, and a dotted name is checked by its dots Nov 5, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nicolas-abril
nicolas-abril merged commit b5e6b2e into bendlang:main Nov 5, 2026
1 check passed
@nicolas-abril
nicolas-abril deleted the perf/parser-fast-reject branch November 5, 2026 17:54
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