pi-intl-segmenter-fallback
v0.1.0
Published
pi extension that stops the TUI from segfaulting on small-ICU Node builds (RHEL/Fedora nodejs without nodejs-full-i18n) by patching Intl.Segmenter with a pure-JS fallback
Maintainers
Readme
pi-intl-segmenter-fallback
A pi extension that stops the TUI from segfaulting at
startup on Node builds compiled with small-ICU — typically the RHEL/Fedora
nodejs RPM installed without nodejs-full-i18n.
The problem
On small-ICU Node builds, new Intl.Segmenter() succeeds but the first
.segment() call dereferences a null icu::BreakIterator inside V8 and kills
the process with SIGSEGV (nodejs/node#51752). pi's TUI calls .segment()
for grapheme-cluster cursor and width math on its very first render, so pi
dies instantly on those builds (earendil-works/pi#6359).
Check whether your build is affected:
node -e 'console.log(process.config.variables.icu_small)' # true → affectedThe real fix is installing full ICU data (
sudo dnf install nodejs-full-i18n). This extension is a stopgap that keeps pi usable when you can't — no root, immutable image, distro without the package.
Install
pi install npm:pi-intl-segmenter-fallbackOr straight from git, or pinned, or just for one run:
pi install git:github.com/T0mSIlver/pi-intl-segmenter-fallback
pi install npm:[email protected] # pinned, skipped by pi update
pi -e npm:pi-intl-segmenter-fallback # try without installingHow it works
At extension import time, if process.config.variables.icu_small ===
true, the extension replaces Intl.Segmenter.prototype.segment and
.resolvedOptions with a pure-JS fallback. On full-ICU/system-ICU builds it
is a strict no-op — method identity is untouched (verified by the test
suite).
It patches the prototype, not the constructor, because the constructor
works even on broken builds — only .segment() crashes. pi's TUI creates its
segmenter singletons at module load, before extensions run; the prototype
patch covers those pre-existing instances, so the extension only needs to
load before the first .segment() call (the first TUI render), which pi's
extension loader guarantees. Granularity of pre-existing instances is
recovered through the native resolvedOptions, which only reads internal
slots and is safe on broken builds (verified on a real one).
When the fallback activates, a one-time warning is shown at session start.
Fallback semantics
Fine for a terminal UI, not linguistically exact:
| Granularity | Behavior |
|---|---|
| grapheme | One segment per Unicode code point (surrogate-pair safe). ZWJ/skin-tone clusters split into parts — 👍🏽 takes two arrow presses instead of one; cursor math stays self-consistent because pi derives widths from the same segmentation. |
| word | Runs of \p{L}\p{N}\p{M}\p{Pc} tagged isWordLike: true (what pi's Alt-B/Alt-F word navigation reads), runs of whitespace, other characters individually. |
| sentence | Naive split after terminal punctuation (unused by pi). |
Segments objects are re-iterable and support .containing(index); each
segment is {segment, index, input[, isWordLike]}.
Environment overrides
| Variable | Effect |
|---|---|
| PI_SEGMENTER_FALLBACK=1 | Force the patch on even with healthy ICU (testing) |
| PI_SEGMENTER_FALLBACK=0 | Never patch, even on small-ICU builds |
Test evidence
Verified against a real small-ICU build (AlmaLinux 9 nodejs-22.22.2
without nodejs-full-i18n): without the extension, pi segfaults at startup
(exit 139); with it, the TUI renders, cursor movement across héllo 👍🏽 test
is consistent, and word navigation works. On full-ICU Node 24 the extension
provably changes nothing. Full transcripts and methodology:
docs/test-evidence.md.
Development
npm install
npm run verify # typecheck + test suite (needs Node ≥ 22.18)
pi install . # smoke-test the package manifest against a local piThe test suite (test/fallback.mjs) simulates pi's usage pattern — segmenter
instances constructed before the extension imports — and asserts no-op
behavior on healthy builds (noop mode) and full fallback semantics
(forced mode).
