Skip to content

group alternations and escape range endpoints in terminal patterns - #1633

Open
netliomax25-code wants to merge 2 commits into
lark-parser:masterfrom
netliomax25-code:terminal-pattern-grouping
Open

group alternations and escape range endpoints in terminal patterns#1633
netliomax25-code wants to merge 2 commits into
lark-parser:masterfrom
netliomax25-code:terminal-pattern-grouping

Conversation

@netliomax25-code

Copy link
Copy Markdown
  1. TerminalTreeToPattern.expansion joins sub-patterns by plain concatenation, so a regexp literal holding a top-level | binds looser than the join: T: /admin|user/ "_id" compiles to admin|user_id, which rejects admin_id and accepts a bare admin.
  2. PrepareLiterals.range splices the endpoints' raw source text into the character class, so an endpoint that is itself a metacharacter escapes it: T: "^".."z" compiles to [^-z], a negated class matching tab and digits while rejecting z.

Either way a terminal written as a restriction matches outside the language it declares, which is the part that worries me when the grammar is what validates untrusted input.

Grouping is applied only when the item carries a | outside any group, so an escaped \|, one inside a character class, and an already-parenthesized one are left alone. Range endpoints are unescaped and then re-escaped for a class, kept ascii because the pattern may still be encoded to bytes under use_bytes (the dynamic lexer encodes utf-8, the basic one latin-1). The other two spellings of that alternation, /a|b/i and ("a"|"b"), already went through (?:...), so only the third one changes.

Compiled patterns for common.lark, python.lark and lark.lark come out identical to master. Full suite green, mypy clean, and the two tests added in tests/test_grammar.py fail on master and pass here.

@MegaIng MegaIng left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While I agree that this PR implements the intended behavior, this is a breaking change.

People may have (accidentally) relied on this behavior to construct more complex terminals. But I suspect this is rare enough that we can put this off as a bug fix.

Comment thread lark/load_grammar.py Outdated
# A bare '|' binds looser than concatenation, so an item carrying one has to be grouped
# before it's joined, or it swallows its neighbors: /a|b/ "c" must not become 'a|bc'.
regexps = [i.to_regexp() for i in items]
pattern = ''.join('(?:%s)' % r if _has_bare_alternation(r) else r for r in regexps)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can just unconditionally use an anonymous group, trying to manually check if there is an alternation is a recipe for infinite edge cases.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, dropped the helper and the join now wraps every item in (?:...) unconditionally. One consequence worth flagging: the compiled patterns for the bundled grammars are no longer byte-identical to master (they pick up redundant groups), just equivalent. Full suite and mypy still green.

Comment thread lark/load_grammar.py Outdated
codepoint = ord(char)
if char.isascii():
return re.escape(char)
return '\\x%02x' % codepoint if codepoint <= 0xff else '\\U%08x' % codepoint

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets use f-strings here.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, switched to f-strings.

@netliomax25-code

Copy link
Copy Markdown
Author

Agreed on the framing. Anyone relying on the old splicing was depending on the terminal matching outside what its own definition says, so treating it as a bug fix seems right to me. Both review points are addressed in the latest push: the join groups every item unconditionally (the alternation scanner is gone), and the escape helper uses f-strings.

@erezsh

erezsh commented Aug 26, 2026

Copy link
Copy Markdown
Member

Overall looks good, the bugs are real and the fix is in the right direction.

A few things to address:

  1. We should not emit \U… escapes, because interegular can't parse them . Instead it drops the terminal from every collision check. It also breaks anonymous-terminal dedup, which compares pattern strings: start: "а".."я" T with T: /[а-я]/ reused T on master; on the PR it creates a duplicate.
    The use_bytes rationale doesn't apply — a bytes pattern rejects \U anyway, and use_bytes already requires an ASCII grammar. Plain re.escape(char) is all that's needed (it leaves non-ASCII alone), so the helper can go. But I might be wrong.

  2. We should still test if | at least appears in the regex, to avoid unnecessary grouping. It would filter out most of the regexps.

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.

3 participants