Skip to content

Add a rusty-expressions regex backend (Oniguruma semantics, pure Rust, wasm-capable) - #2368

Open
Ttimmahlax wants to merge 5 commits into
huggingface:mainfrom
Remade-With-Rust:rusty-expressions-backend
Open

Ttimmahlax wants to merge 5 commits into
huggingface:mainfrom
Remade-With-Rust:rusty-expressions-backend

Conversation

@Ttimmahlax

Copy link
Copy Markdown

What

Adds a third optional SysRegex backend behind a rusty-expressions feature,
alongside the existing onig and fancy-regex ones.

rusty_expressions is Oniguruma
reimplemented in pure Rust. It aims at the same semantics as onig — it is
developed against live libonig as a differential oracle — while removing the
C from the build.

Nothing changes by default. onig remains the default backend; the new
feature is additive and off unless asked for. When enabled it takes precedence
over the other two.

Why a third backend rather than a swap

The two existing options each give up something:

C toolchain wasm32 Oniguruma semantics
onig required
fancy-regex none different engine
rusty-expressions none

unstable_wasm exists precisely because onig cannot build for wasm32. That
workaround also changes the engine, which is presumably why it is not the
default. This backend is the combination that was missing.

Verification on this branch

  • 201 lib tests pass with --no-default-features --features rusty-expressions.
  • Pre-tokenizer output is byte-identical to the onig backend. I built the
    same dumper under both backends and diffed: 95 dumps across ByteLevel,
    Whitespace, Punctuation, a \s+(?!\S) negative-lookahead Split, and a
    (\w)\1 backreference Split, over a corpus covering ASCII, CJK, Cyrillic,
    Greek, emoji with skin-tone modifiers, NFD vs NFC, zero-width characters,
    URLs, timestamps and long repeated strings. No differences.
  • wasm32-unknown-unknown compiles with rusty-expressions (given the same
    getrandom configuration unstable_wasm already supplies) and fails with
    onig, as expected.

Notes for review

  • The dependency is taken with default-features = false deliberately:
    rusty_expressions installs rusty_alloc as a #[global_allocator] under
    its default feature, and a library must never impose an allocator on its
    consumers.
  • find_iter advances by a whole character on an empty match, so it cannot
    land inside a UTF-8 sequence, and stops on an engine limit rather than
    propagating — matching the fancy-regex backend's behaviour.
  • The engine is unsafe_code = "deny"; the only unsafe in that crate is an
    optional C-ABI shim which is not enabled here.

Happy to adjust naming, feature precedence, or add the backend to CI if you'd
like it covered there. I can also run a wider differential over real
tokenizer.json files from the Hub if that would help review.

`rusty_expressions` is Oniguruma reimplemented in pure Rust. It gives the same
engine semantics as the `onig` backend -- it is gated differentially against
live libonig -- while removing the C from the build entirely.

Why this is worth a third backend rather than a swap:

- `onig` links libonig through `onig_sys`, so it needs a C toolchain and cannot
  target wasm32 at all. That is why `unstable_wasm` exists.
- `fancy-regex` solves the wasm problem but is a different engine with
  different semantics, which is presumably why it is not the default.
- `rusty_expressions` is neither: same semantics as Oniguruma, no C.

Verified on this branch:

- 201 lib tests pass with `--no-default-features --features rusty-expressions`.
- Pre-tokenizer output is **byte-identical to the `onig` backend** across 95
  dumps (ByteLevel, Whitespace, Punctuation, a `\s+(?!\S)` negative-lookahead
  Split, and a `(\w)\1` backreference Split) over a corpus covering ASCII,
  CJK, Cyrillic, Greek, emoji with modifiers, NFD vs NFC, zero-width
  characters, URLs and long repeats.
- `wasm32-unknown-unknown` compiles with `rusty-expressions` and fails with
  `onig`, as expected.

The feature is additive and off by default; `onig` remains the default backend
and nothing changes for existing users. When enabled it takes precedence over
both existing backends. The dependency is taken with
`default-features = false` because rusty_expressions installs a
`#[global_allocator]` under its default feature, which a library must never
impose on its consumers.

Upstream: https://github.com/Remade-With-Rust/rusty_expressions
`unstable_wasm` currently means two things at once: "configure getrandom for
wasm32" and "use fancy-regex". A wasm build that wants a different pure-Rust
backend has no way to ask for the first without the second.

Adds a `wasm` feature carrying only the getrandom configuration, and redefines
`unstable_wasm = ["fancy-regex", "wasm"]`, so its meaning is unchanged for
existing users. A wasm build can now pick its engine:

    --features unstable_wasm            # fancy-regex, as before
    --features rusty-expressions,wasm   # Oniguruma semantics, still no C

Verified: all three of `unstable_wasm`, `rusty-expressions,wasm` and the
default native `onig` build compile.
@Ttimmahlax

Copy link
Copy Markdown
Author

Pushed a second commit that I think makes the first more useful on its own.

unstable_wasm currently means two things at once — "configure getrandom for
wasm32" and "use fancy-regex". A wasm build cannot ask for the plumbing
without also accepting that particular engine, which is exactly what I ran into
wiring this up downstream.

So this adds a wasm feature carrying only the getrandom configuration, and
redefines unstable_wasm = ["fancy-regex", "wasm"] — unchanged meaning for
anyone using it today. A wasm build can now choose its engine:

--features unstable_wasm            # fancy-regex, exactly as before
--features rusty-expressions,wasm   # Oniguruma semantics, still no C

That half stands alone and is useful even if you'd rather not take the new
backend. Happy to split it into a separate PR if you'd prefer to review them
independently.

tim-almond-house and others added 3 commits August 27, 2026 18:06
The backend features resolve by precedence and `rusty-expressions` wins,
so enabling it changes the engine for every crate in the graph --
including for someone who explicitly asked for `onig`. Cargo features are
additive, so that can happen because a transitive dependency turned it
on, with nothing said. The regex engine decides how text is split, so a
silent swap is worth being able to detect.

`compile_error!` would be the loud version, but it would break any build
where a dependency legally enabled the feature. So this reports rather
than enforces: `utils::REGEX_BACKEND` names the engine that was compiled
in, and `utils::REGEX_BACKEND_OVERRODE_ONIG` flags the case where libonig
was built and linked and then never called. A build that cares can assert
on either.

The Cargo.toml feature comment now says the precedence out loud too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
It asserted the rusty_expressions backend specifically, so it failed
under the default features, where onig is selected. The doctest runs
under whatever features the build has; assert the invariant that holds
in all of them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
0.x minor bumps are breaking in cargo's semver, so the "0.1" pin would
not have picked up 0.2.0. That release swapped the engine's test oracle to
live libonig and fixed what it found -- POSIX BRE groups, three dialect
flag tables, CJK character-boundary and character-class handling,
FIND_LONGEST, and a `find_at` that answered positions past the end of the
haystack.

202 tests green on the rusty-expressions backend. (The four added_tokens
tests need `make test` to download GPT-2 fixtures first; unrelated.)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ArthurZucker

Copy link
Copy Markdown
Collaborator

Hey! ty, we are working on tokenizers v1, see #2119

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