Skip to content

Add parlance::BidiLevel - #710

Merged
tomcur merged 11 commits into
linebender:mainfrom
tomcur:push-wqvtnrxmlnkl
Jul 27, 2026
Merged

Add parlance::BidiLevel#710
tomcur merged 11 commits into
linebender:mainfrom
tomcur:push-wqvtnrxmlnkl

Conversation

@tomcur

@tomcur tomcur commented Jul 24, 2026

Copy link
Copy Markdown
Member

The motivation is to have a repr(transparent) wrapper for bidi levels to hold documentation and useful methods (e.g., letting users call level.is_rtl() instead of manually writing !level.is_multiple_of(2), or having our APIs vary between level() -> u8 and is_rtl() -> bool methods), while allowing bytemucking between BidiLevel <=> u8 for interop.

parley_engine now returns BidiLevel, but I've kept parley's API unchanged.

Comment on lines 77 to -87
/// The base bidi level of the paragraph of text.
#[inline(always)]
pub fn paragraph_level(&self) -> u8 {
pub fn paragraph_level(&self) -> BidiLevel {
self.paragraph_level
}

/// Whether the paragraph's resolved base direction is right-to-left.
#[inline(always)]
pub fn is_rtl(&self) -> bool {
!self.paragraph_level.is_multiple_of(2)
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Removing Analysis::is_rtl in favor of Analysis::paragraph_level().is_rtl(), as per #708 (comment).

In that same thread, Bruce pointed out we have some inconsistency in the code base, sometimes calling this "base level", sometimes "paragraph level". The spec uses both words, but uses "paragraph level" a bit more. The code uses "base level" more. In any case, we probably should still rename this, either to something like base_bidi_level or paragraph_bidi_level.

The motivation is to have a `repr(transparent)` wrapper for bidi levels
to hold documentation and useful methods (e.g., letting users call
`level.is_rtl()` instead of manually writing
`!level.is_multiple_of(2)`), while being able to bytemuck between
`BidiLevel` <=> `u8` for interop.
@tomcur
tomcur force-pushed the push-wqvtnrxmlnkl branch from e483121 to 2d1f829 Compare July 24, 2026 14:56
@tomcur

tomcur commented Jul 24, 2026

Copy link
Copy Markdown
Member Author

We could similarly implement bytemuck traits for NormalizedCoord (see #679 (comment) and #679 (comment)).

@nicoburns nicoburns left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Very nice. Good use of parlance.

Comment thread parley_engine/src/bidi.rs Outdated
Comment on lines +906 to +914
#[inline(always)]
const fn next_odd(level: BidiLevel) -> BidiLevel {
BidiLevel::new((level.to_u8() + 1) | 1)
}

#[inline(always)]
const fn next_even(level: BidiLevel) -> BidiLevel {
BidiLevel::new((level.to_u8() + 2) & !1)
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

These could optionally be methods on the type

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.

The lack of capping to 125 here would be my main hesitance, but I think on balance I agree (also with it being optional/up to your discretion)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I've added the methods, with some documentation about wrapping.

@DJMcNab DJMcNab 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.

Nothing blocking, some code style nits.

I'm really happy to see this type properly documented. I did try and start on that task, but it got too big for what I had time to do.

This probably should have changelog entries (what is parley_engine's changelog story?)

Comment thread parlance/src/bidi.rs
/// These are numbers indicating how deeply bidirectional embeddings are nested in the text, and the
/// default direction of text on that level. Even levels are left-to-right, odd levels are
/// right-to-left. Normally, the minimum level is 0 (left-to-right), and the maximum level,
/// according to [UAX #9 § 3.1.1 BD2][uax-bd2], is 125.

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.

Incidentally, this being 125 does give us a bit to muck about with. I don't know of any use for that (maybe for whether rule L1 would apply to this?).

Comment thread parlance/src/bidi.rs
#[repr(transparent)]
pub struct BidiLevel(u8);

impl BidiLevel {

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.

Does it make sense for max level to be stored here, either a u8 or BidiLevel constant?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

That sounds sensible. I've added BidiLevel::MAX, but perhaps you have feedback on the naming.

In particular, it's not the greatest value BidiLevel itself can represent.

Comment thread parlance/src/bidi.rs
pub struct BidiLevel(u8);

impl BidiLevel {
/// Construct a new bidi level.

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.

I'd claim that we should debug assert that this is less than or equal to 125, but not blockingly so.

@tomcur tomcur Jul 27, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Perhaps that makes sense. This ties back into #710 (comment).

Deferring this for now.

Comment on lines +42 to +48
// Safety: The struct is `repr(transparent)`, wrapping a `u8`.
//
// While generally BidiLevels have a maximum of 125, no value is unsound.
unsafe impl Pod for BidiLevel {}

// Safety: The struct is `repr(transparent)`, wrapping a `u8`.
unsafe impl Zeroable for BidiLevel {}

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 probably should have the usual safety tests for these. But also this is so simple it seems hard to imagine it going wrong! I'm not even sure what the tests would look like? Maybe even just that the size is 1 to force this to be revisited?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I've added the size test. I'm also not sure whether we can do better.

Comment thread parley/src/layout/alignment.rs Outdated
Comment thread parley/src/layout/run.rs
Comment on lines 111 to 114
/// Returns `true` if the run has right-to-left directionality.
pub fn is_rtl(&self) -> bool {
self.shaped.bidi_level & 1 != 0
self.shaped.bidi_level.is_rtl()
}

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.

I guess nominally it might be better to just return the bidi level here, but that's not done here to avoid breaking changes?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yeah, I intentionally did not change the parley API here.

That said, we probably should break it. (But not in this PR.)

Comment thread parley/src/tests/test_analysis.rs Outdated
}

fn expect_bidi_embed_level_list(self, expected: Vec<u8>) -> Self {
fn expect_bidi_embed_level_list(self, expected: &[u8]) -> Self {

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.

🔥

Comment thread parley_engine/src/bidi.rs Outdated
Comment thread parley_engine/src/bidi.rs Outdated
Comment on lines +906 to +914
#[inline(always)]
const fn next_odd(level: BidiLevel) -> BidiLevel {
BidiLevel::new((level.to_u8() + 1) | 1)
}

#[inline(always)]
const fn next_even(level: BidiLevel) -> BidiLevel {
BidiLevel::new((level.to_u8() + 2) & !1)
}

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.

The lack of capping to 125 here would be my main hesitance, but I think on balance I agree (also with it being optional/up to your discretion)

@tomcur
tomcur enabled auto-merge July 27, 2026 22:40
@tomcur

tomcur commented Jul 27, 2026

Copy link
Copy Markdown
Member Author

This probably should have changelog entries (what is parley_engine's changelog story?)

I've added a changelog entry for Parlance. I think we should write something in the changelog for parley_engine when we do the initial release (more or less describing the state it's in at that point).

@tomcur
tomcur added this pull request to the merge queue Jul 27, 2026
Merged via the queue into linebender:main with commit f9b500a Jul 27, 2026
24 checks passed
@tomcur
tomcur deleted the push-wqvtnrxmlnkl branch July 27, 2026 22:52
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