Skip to content

syntax: fix class negation across the surrogate gap - #1399

Open
CarloTUM wants to merge 1 commit into
rust-lang:masterfrom
CarloTUM:negate-surrogate-gap
Open

CarloTUM wants to merge 1 commit into
rust-lang:masterfrom
CarloTUM:negate-surrogate-gap

Conversation

@CarloTUM

@CarloTUM CarloTUM commented Oct 5, 2026

Copy link
Copy Markdown

Fixes #1397.

IntervalSet::negate computes the gap between two adjacent canonical ranges with increment()/decrement(). For ranges on either side of the surrogate codepoints, such as ..=\u{D7FF} and \u{E000}.., both steps jump over the surrogates, which yields lower = \u{E000} and upper = \u{D7FF}. Interval::create then swaps the inverted bounds and produces \u{D7FF}..=\u{E000}, so both codepoints end up matching their own negation:

Regex::new(r"^[^\x{D7FF}\x{E000}]$").unwrap().is_match("\u{D7FF}") // true

canonicalize never merges these two ranges, since is_contiguous compares the integer values and 0xD7FF + 1 != 0xE000. So the fix handles the empty gap in negate itself: when lower > upper there is nothing between the ranges and no range is pushed. Byte classes are unaffected, as u8 has no such gap.

Tests:

  • unit tests in hir/mod.rs for the three shapes: the two codepoints alone, the full set whose negation must be empty, and a mixed set,
  • regression tests in testdata/regression.toml for U+D7FF, U+E000 and the neighbouring U+D7FE.

I left the CHANGELOG untouched; happy to add an entry if you prefer that in the PR.


Disclosure: assisted by GitHub Copilot and Claude (including this description). I reviewed and tested the change myself.

When an interval set contains ranges on either side of the surrogate
codepoints, such as `..=\u{D7FF}` and `\u{E000}..`, negation computed
the gap between them with `increment`/`decrement`, which skip over the
surrogates. That yields `lower = \u{E000}` and `upper = \u{D7FF}`, and
`Interval::create` then swaps the inverted bounds into the range
`\u{D7FF}..=\u{E000}`. So both codepoints ended up matching their own
negation:

    Regex::new(r"^[^\x{D7FF}\x{E000}]$").unwrap().is_match("\u{D7FF}")

Such ranges are never merged by `canonicalize`, since `is_contiguous`
compares the integer values and `0xD7FF + 1 != 0xE000`. So handle the
empty gap in `negate` instead and skip it.

Fixes rust-lang#1397

This branch has not been deployed

No deployments
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.

Negated code points near the surrogate gap still match

1 participant