Repository navigation
Conversation
ParagraphBidiInfo::has_rtl returned !self.is_pure_ltr, while the three
sibling call sites in the same two files ask level::has_rtl(&self.levels):
src/lib.rs:595 BidiInfo::reorder_line
src/lib.rs:683 BidiInfo::has_rtl
src/lib.rs:821 ParagraphBidiInfo::reorder_line
src/lib.rs:858 ParagraphBidiInfo::has_rtl <- the odd one out
is_pure_ltr is a character-scan flag, documented as "set to false if any
RTL characters or bidi control characters are encountered". It is blind
to the paragraph level, which is the other source of RTL levels. So for
neutral-only text in an RTL paragraph:
ParagraphBidiInfo::new("()", Some(Level::rtl())).has_rtl() #=> false
while both structs compute the identical levels [Level(1), Level(1)] and
BidiInfo::has_rtl() returns true. The doc comments on the two has_rtl
methods are byte-identical and both promise "any computed RTL levels",
so a caller doing what they say - skipping re-ordering when no RTL level
is present - skips it on text that is entirely RTL.
Across the 91,707 BidiCharacterTest inputs the two disagree 7,533 times,
7,515 of them in this direction.
The existing test asserted only BidiInfo and BidiInfoU16, and every row
was ASCII or contained a real Hebrew character, so no row separated the
two predicates. Add neutral-only rows and assert the Paragraph types.
Member
|
Thanks for the investigation. I'd like to retain the fast path, so I adapted your code into #154. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The inconsistency
Four places decide "does this text have RTL levels". Three ask the levels; one asks a flag:
src/lib.rs:595BidiInfo::reorder_linelevel::has_rtl(&self.levels[line])src/lib.rs:683BidiInfo::has_rtllevel::has_rtl(&self.levels)src/lib.rs:821ParagraphBidiInfo::reorder_linelevel::has_rtl(&self.levels[line])src/lib.rs:858ParagraphBidiInfo::has_rtl!self.is_pure_ltrIdentical split in
src/utf16.rs::271,:359,:497use the levels,:532uses the flag.is_pure_ltris documented as "Set to false if any RTL characters or bidi control characters areencountered in the paragraph." It is a character scan. It cannot see the paragraph level, which
is the other thing that produces RTL levels.
And the two
has_rtldoc comments are byte-identical — both say "If processed text has anycomputed RTL levels / This information is usually used to skip re-ordering of text when no RTL level
is present" — while computing different predicates.
What a caller sees
()has no RTL character and no bidi control, sois_pure_ltrstays true. But in an RTL paragraphthe neutrals resolve to level 1, so the text is entirely RTL. Both structs agree on that:
A caller doing exactly what the doc says — skip re-ordering when no RTL level is present — skips it
on text that is entirely RTL, and renders it in the wrong visual order.
Across all 91,707
BidiCharacterTest.txtinputs the two methods disagree 7,533 times:7,515 under-reports (
Paragraphsays false whereBidiInfosays true) and 18 the other way.ParagraphBidiInfo::reorder_linein the same struct protects itself by callinglevel::has_rtldirectly, which is why this is invisible internally and only reaches external callers.
Why the tests did not catch it
test_bidi_info_has_rtlasserts onlyBidiInfoandBidiInfoU16, never eitherParagraphtype.And every one of its 17 rows is ASCII or contains a real Hebrew character — there is no
neutral-only row, which is the single shape that separates a character scan from a level check.
This PR adds
("()", RTL, true),("()", LTR, false),("()", None, false)and extends the loopto assert
ParagraphBidiInfoandParagraphBidiInfoU16, guarded byif !t.0.contains('\n')sincethat type is single-paragraph.
Reverting either fix fails the RTL row (
left: false, right: true); replacing the body withtruefails the LTR and ASCII rows (
left: true, right: false). Both sides pinned.Verification
Every CI row run, not inferred:
cargo testcargo test --features serdecargo test --no-default-featuresrequired-features)cargo test --no-default-features --features=hardcoded-datacargo +1.47.0 test(MSRV)cargo +1.47.0 test --no-default-features --features=hardcoded-datacargo fmt --checkThe
Verifyjob is unaffected:git statusshows onlysrc/lib.rsandsrc/utf16.rs, andsrc/char_data/tables.rsis untouched — this changes a predicate over already-computed levels, notany Unicode data.
Not benchmarked. It replaces a bool read with an
#[inline]O(n) scan over a slice that is alreadyin cache, on a method whose whole purpose is to let callers skip an O(n) reorder. Say the word if
you want numbers.
The alternative
You could instead keep
!self.is_pure_ltrand change the doc to describe a "has RTL characters"probe. I would argue against it —
ParagraphBidiInfo::reorder_lineeleven lines above already votesfor the levels-based answer, and the 7,515:18 asymmetry means the levels answer is the one that
prevents mis-rendering — but it is your call and it is a one-line docs change if you prefer it.
Note
has_rtl()is public, so a caller using the current over-broadtrueas a cheaphas-any-bidi-content probe would see fewer
trues in 18 of the 91,707 cases. The doc promises"any computed RTL levels", so I think that is the correct direction, but it is worth stating.
How I found it
While checking whether refreshing the vendored conformance fixtures from Unicode 15 to 17 would
surface anything. It would not — the U17
BidiTest.txtdata is byte-identical to the vendored U15copy, and
BidiCharacterTest.txtonly removes two lines (both already gone in Unicode 16), so arefresh would drop two currently-passing cases and add none. Instead I cross-checked the public APIs
the conformance suite never touches, and
ParagraphBidiInfoturned out to be a genuine parallelimplementation —
reordered_levels,reordered_levels_per_char,reorder_lineandparagraph_levelall agree withBidiInfoacross all 91,707 inputs.has_rtlwas the only onethat did not.
Disclosure
I used an AI assistant to help find and prepare this change. I reviewed and tested it myself, and
every number above is from a run I performed.