Itijah: Pure Zig Unicode Bidirectional Algorithm — passes full conformance suite

Zig-native implementation of the Unicode Bidirectional Algorithm (UAX #9).

Codepoint-level API with explicit allocator passing and no global state.

Why:
I needed bidi support for RTL text layout in Ghostty, and there wasn’t a pure Zig option. FriBidi and ICU work well, but they add C dependencies and don’t match Zig’s allocator-first style.

Key points:

  • Full conformance in current suite: BidiTest failed=0, BidiCharacterTest failed=0
  • Competitive performance vs FriBidi/ICU across benchmark profiles
  • Scratch APIs for allocation-light render loops
  • Terminal-oriented resolveVisualLayout for one-call line layout

Links:

Feedback welcome, especially from people working on text rendering in Zig.

15 Likes

Nice! I did something in that space myself. I didn’t do serious benchmarking though. I’d love if there could be some comparisons between the two libs.

2 Likes

Observations:

  • Other than summary in the README, no documentation or examples.

  • In a number of structs, I see a pattern of embedding the allocator. This overhead shouldn’t be necessary.

  • What are the tradeoffs between the scratch and non-scratch APIs? Why the complexity of two APIs at all?

  • Is there a good reason why the ShapeFlags and ReorderFlags structs are padded to 32 bits? The largest requires only 4 bits.

2 Likes

zabadi was actually the first thing I came across when looking into this space! Ended up going a different route though , needed codepoint input directly (terminals already have them decoded, felt weird to re-encode to UTF-8) and wanted scratch APIs that don’t allocate per call for rendering loops.

Added zabadi to the benchmark harness btw - different tradeoffs for different use cases.

1 Like

Thanks for the feedback

  • Docs/examples: fair , focused too much on getting conformance and performance, but docs on the list, will reply here once they are out.

  • Embedded allocator: the newer scratch APIs don’t do this. the older result types store it for convenient deinit(), could revisit.

  • Scratch vs non-scratch,: scratch reuses buffers across calls so we get zero allocations in steady state. matters for hot loops like terminal rendering (ghostty). Non-scratch is simpler if you just need a one-off call. Felt like both were worth having.

  • Flags padding: it’s leftover from early fribidi-inspired design, no reason to keep. needs cleaning up .

2 Likes

All addressed:

Appreciate the review, it made the api cleaner

3 Likes