diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..1299e88
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,122 @@
+name: CI
+
+on:
+ pull_request:
+ branches: [main]
+
+env:
+ CARGO_TERM_COLOR: always
+ RUST_BACKTRACE: 1
+
+jobs:
+ linux:
+ name: Linux (all checks)
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Install Rust toolchain
+ uses: dtolnay/rust-toolchain@stable
+ with:
+ components: clippy, rustfmt
+
+ - name: Cache cargo registry
+ uses: actions/cache@v4
+ with:
+ path: |
+ ~/.cargo/registry
+ ~/.cargo/git
+ target
+ key: ${{ runner.os }}-cargo-ci-${{ hashFiles('**/Cargo.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-cargo-ci-
+ ${{ runner.os }}-cargo-
+
+ - name: Check formatting
+ run: cargo fmt --all --check
+
+ - name: Check (default features)
+ run: cargo check
+
+ - name: Check (all features)
+ run: cargo check --features full
+
+ - name: Clippy
+ run: cargo clippy --features full --all-targets -- -D warnings
+
+ - name: Run tests (default features)
+ run: cargo test
+
+ - name: Run tests (all features)
+ run: cargo test --features full
+
+ - name: Build documentation
+ run: cargo doc --features full --no-deps
+ env:
+ RUSTDOCFLAGS: -D warnings
+
+ windows:
+ name: Windows
+ runs-on: windows-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Install Rust toolchain
+ uses: dtolnay/rust-toolchain@stable
+
+ - name: Cache cargo registry
+ uses: actions/cache@v4
+ with:
+ path: |
+ ~/.cargo/registry
+ ~/.cargo/git
+ target
+ key: ${{ runner.os }}-cargo-ci-${{ hashFiles('**/Cargo.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-cargo-ci-
+ ${{ runner.os }}-cargo-
+
+ - name: Check (all features)
+ run: cargo check --features full
+
+ - name: Run tests (all features)
+ run: cargo test --features full
+
+ macos:
+ name: macOS (Intel)
+ runs-on: macos-15-intel
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Install Rust toolchain
+ uses: dtolnay/rust-toolchain@stable
+
+ - name: Cache cargo registry
+ uses: actions/cache@v4
+ with:
+ path: |
+ ~/.cargo/registry
+ ~/.cargo/git
+ target
+ key: ${{ runner.os }}-cargo-ci-${{ hashFiles('**/Cargo.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-cargo-ci-
+ ${{ runner.os }}-cargo-
+
+ - name: Check (all features)
+ run: cargo check --features full
+
+ - name: Run tests (all features)
+ run: cargo test --features full
+
+ msrv:
+ name: MSRV (1.83)
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Install Rust 1.83
+ uses: dtolnay/rust-toolchain@1.83
+
+ - name: Check MSRV
+ run: cargo check --features full
diff --git a/.github/workflows/jit.yml b/.github/workflows/jit.yml
new file mode 100644
index 0000000..8146b3f
--- /dev/null
+++ b/.github/workflows/jit.yml
@@ -0,0 +1,201 @@
+name: JIT CI
+
+on:
+ push:
+ branches:
+ - '**/jit/**'
+ - 'jit/**'
+ - '**/jit'
+ - 'jit-*'
+ - '*-jit'
+ - '*-jit-*'
+ pull_request:
+ branches:
+ - main
+ paths:
+ - 'src/jit/**'
+ - 'src/vm/*/jit/**'
+ - 'src/nfa/tagged/jit/**'
+
+env:
+ CARGO_TERM_COLOR: always
+ RUST_BACKTRACE: 1
+
+jobs:
+ linux:
+ name: Linux
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Install Rust toolchain
+ uses: dtolnay/rust-toolchain@stable
+
+ - name: Cache cargo registry
+ uses: actions/cache@v4
+ with:
+ path: |
+ ~/.cargo/registry
+ ~/.cargo/git
+ target
+ key: ${{ runner.os }}-cargo-jit-${{ hashFiles('**/Cargo.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-cargo-jit-
+ ${{ runner.os }}-cargo-
+
+ - name: Check (no features)
+ run: cargo check --no-default-features
+
+ - name: Check (jit only)
+ run: cargo check --features jit
+
+ - name: Check (full)
+ run: cargo check --features full
+
+ - name: Test (no JIT)
+ run: cargo test --no-default-features
+
+ - name: Test (JIT only)
+ run: cargo test --features jit
+
+ - name: Test (full)
+ run: cargo test --features full
+
+ linux-arm64:
+ name: Linux (ARM64)
+ runs-on: ubuntu-24.04-arm
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Install Rust toolchain
+ uses: dtolnay/rust-toolchain@stable
+
+ - name: Cache cargo registry
+ uses: actions/cache@v4
+ with:
+ path: |
+ ~/.cargo/registry
+ ~/.cargo/git
+ target
+ key: ${{ runner.os }}-arm64-cargo-jit-${{ hashFiles('**/Cargo.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-arm64-cargo-jit-
+ ${{ runner.os }}-arm64-cargo-
+
+ - name: Check (no features)
+ run: cargo check --no-default-features
+
+ - name: Check (jit only)
+ run: cargo check --features jit
+
+ - name: Test (no JIT)
+ run: cargo test --no-default-features
+
+ - name: Test (JIT only)
+ run: cargo test --features jit
+
+ windows:
+ name: Windows
+ runs-on: windows-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Install Rust toolchain
+ uses: dtolnay/rust-toolchain@stable
+
+ - name: Cache cargo registry
+ uses: actions/cache@v4
+ with:
+ path: |
+ ~/.cargo/registry
+ ~/.cargo/git
+ target
+ key: ${{ runner.os }}-cargo-jit-${{ hashFiles('**/Cargo.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-cargo-jit-
+ ${{ runner.os }}-cargo-
+
+ - name: Check (full)
+ run: cargo check --features full
+
+ - name: Test (full)
+ run: cargo test --features full
+
+ macos-intel:
+ name: macOS (Intel x86_64)
+ runs-on: macos-15-intel
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Install Rust toolchain
+ uses: dtolnay/rust-toolchain@stable
+
+ - name: Cache cargo registry
+ uses: actions/cache@v4
+ with:
+ path: |
+ ~/.cargo/registry
+ ~/.cargo/git
+ target
+ key: ${{ runner.os }}-intel-cargo-jit-${{ hashFiles('**/Cargo.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-intel-cargo-jit-
+ ${{ runner.os }}-intel-cargo-
+
+ - name: Check (full)
+ run: cargo check --features full
+
+ - name: Test (full)
+ run: cargo test --features full
+
+ macos-arm64:
+ name: macOS (ARM64 Apple Silicon)
+ runs-on: macos-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Install Rust toolchain
+ uses: dtolnay/rust-toolchain@stable
+
+ - name: Cache cargo registry
+ uses: actions/cache@v4
+ with:
+ path: |
+ ~/.cargo/registry
+ ~/.cargo/git
+ target
+ key: ${{ runner.os }}-arm64-cargo-jit-${{ hashFiles('**/Cargo.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-arm64-cargo-jit-
+ ${{ runner.os }}-arm64-cargo-
+
+ - name: Check (no features)
+ run: cargo check --no-default-features
+
+ - name: Check (jit only)
+ run: cargo check --features jit
+
+ - name: Check (full)
+ run: cargo check --features full
+
+ - name: Test (no JIT)
+ run: cargo test --no-default-features
+
+ - name: Test (JIT only) - Debug ARM64
+ run: |
+ echo "=== Running JIT tests on ARM64 ==="
+ echo "Architecture: $(uname -m)"
+ cargo test --features jit -- --test-threads=1 2>&1 || {
+ echo "=== JIT tests failed, running individual test files ==="
+ cargo test --features jit --lib -- --test-threads=1 || true
+ cargo test --features jit --test api -- --test-threads=1 || true
+ cargo test --features jit --test engines -- --test-threads=1 || true
+ cargo test --features jit --test features -- --test-threads=1 || true
+ cargo test --features jit --test patterns -- --test-threads=1 || true
+ cargo test --features jit --test unicode -- --test-threads=1 || true
+ echo "=== Individual test runs complete ==="
+ exit 1
+ }
+
+ - name: Test (full)
+ run: cargo test --features full
diff --git a/Cargo.lock b/Cargo.lock
index c0de343..401be53 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -545,7 +545,7 @@ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
[[package]]
name = "regexr"
-version = "0.1.0"
+version = "0.1.0-beta.3"
dependencies = [
"aho-corasick",
"criterion",
diff --git a/Cargo.toml b/Cargo.toml
index b1b325f..4d69905 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "regexr"
-version = "0.1.0"
+version = "0.1.0-beta.3"
edition = "2021"
authors = ["Farhan Syah"]
description = "A high-performance regex engine built from scratch with JIT compilation and SIMD acceleration"
diff --git a/README.md b/README.md
index 6886964..cbebc62 100644
--- a/README.md
+++ b/README.md
@@ -163,9 +163,20 @@ assert_eq!(result, "abc NUM def NUM");
## Feature Flags
- `simd` (default): Enables SIMD-accelerated literal search
-- `jit`: Enables JIT compilation for x86-64
+- `jit`: Enables JIT compilation (x86-64 and ARM64)
- `full`: Enables both JIT and SIMD
+### Platform Support
+
+| Platform | JIT Support | SIMD Support |
+|----------|-------------|--------------|
+| Linux x86-64 | ✓ | ✓ (AVX2) |
+| Linux ARM64 | ✓ | ✗ |
+| macOS x86-64 | ✓ | ✓ (AVX2) |
+| macOS ARM64 (Apple Silicon) | ✓ | ✗ |
+| Windows x86-64 | ✓ | ✓ (AVX2) |
+| Other | ✗ | ✗ |
+
Build without default features for a minimal installation:
```bash
diff --git a/benches/utils/test_data.rs b/benches/utils/test_data.rs
index fbd8d88..af24e52 100644
--- a/benches/utils/test_data.rs
+++ b/benches/utils/test_data.rs
@@ -78,7 +78,7 @@ pub fn get_test_data() -> &'static TestDataCache {
}
fn generate_email_data(target_size: usize) -> String {
- let valid_emails = vec![
+ let valid_emails = [
"john.doe@example.com",
"jane_smith@company.org",
"test+tag@subdomain.example.co.uk",
@@ -87,7 +87,7 @@ fn generate_email_data(target_size: usize) -> String {
"admin@localhost.localdomain",
];
- let invalid_emails = vec![
+ let invalid_emails = [
"not-an-email",
"@example.com",
"missing@domain",
@@ -113,7 +113,7 @@ fn generate_email_data(target_size: usize) -> String {
}
fn generate_url_data(target_size: usize) -> String {
- let urls = vec![
+ let urls = [
"https://www.example.com/path/to/resource",
"http://subdomain.test.org:8080/api/v1/users?id=123",
"https://github.com/user/repo/blob/main/src/file.rs",
@@ -134,8 +134,8 @@ fn generate_url_data(target_size: usize) -> String {
}
fn generate_log_data(target_size: usize) -> String {
- let levels = vec!["INFO", "WARNING", "ERROR", "CRITICAL", "DEBUG"];
- let messages = vec![
+ let levels = ["INFO", "WARNING", "ERROR", "CRITICAL", "DEBUG"];
+ let messages = [
"Request processed successfully",
"Connection timeout after 30s",
"Database query failed",
@@ -206,7 +206,7 @@ fn generate_ip_data(target_size: usize) -> String {
}
fn generate_html_data(target_size: usize) -> String {
- let tags = vec![
+ let tags = [
"
",
"
Some paragraph text
",
"
Click here",
@@ -229,7 +229,7 @@ fn generate_html_data(target_size: usize) -> String {
}
fn generate_text_data(target_size: usize) -> String {
- let words = vec![
+ let words = [
"the", "quick", "brown", "fox", "jumps", "over", "lazy", "dog", "and", "then", "runs",
"through", "forest", "near", "river", "where", "birds", "sing", "their", "morning",
"songs",
@@ -247,7 +247,7 @@ fn generate_text_data(target_size: usize) -> String {
}
fn generate_code_data(target_size: usize) -> String {
- let code_snippets = vec![
+ let code_snippets = [
r#"let x = "hello world";"#,
r#"const y = 'single quoted string';"#,
r#"var z = "escaped \"quotes\" inside";"#,
diff --git a/docs/architecture.md b/docs/architecture.md
index d267948..f9a677c 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -79,7 +79,7 @@ Pattern String → AST → HIR → NFA → Engine-Specific Representation
### JIT Engines (Native Code Generation)
-Available on x86-64 with the `jit` feature.
+Available on x86-64 (Linux, macOS, Windows) and ARM64 (Linux, macOS) with the `jit` feature.
#### DFA JIT (`src/jit/`)
- Compiles DFA to native machine code
@@ -245,7 +245,7 @@ full = ["jit", "simd"]
```
- **default**: SIMD acceleration only
-- **jit**: Adds JIT compilation (x86-64 only)
+- **jit**: Adds JIT compilation (x86-64 and ARM64)
- **full**: Both JIT and SIMD
### Conditional Compilation
@@ -253,11 +253,11 @@ full = ["jit", "simd"]
JIT engines use conditional compilation:
```rust
-#[cfg(all(feature = "jit", target_arch = "x86_64"))]
+#[cfg(all(feature = "jit", any(target_arch = "x86_64", target_arch = "aarch64")))]
pub mod jit;
```
-This ensures JIT code is only compiled on supported platforms.
+This ensures JIT code is only compiled on supported platforms (x86-64 and ARM64).
## Performance Considerations
diff --git a/docs/engine_structure.md b/docs/engine_structure.md
index aac4b70..c6e9383 100644
--- a/docs/engine_structure.md
+++ b/docs/engine_structure.md
@@ -32,7 +32,7 @@ src/{engine_type}/{engine_name}/
│ ├── mod.rs
│ ├── {name}.rs # JIT struct and public API
│ ├── x86_64.rs # x86-64 code generation
-│ ├── aarch64.rs # ARM64 code generation (future)
+│ ├── aarch64.rs # ARM64 code generation
│ └── helpers.rs # Extern helper functions for JIT
└── *.rs # [optional] Engine-specific files as needed
```
@@ -100,7 +100,7 @@ src/vm/shift_or/
### JIT-Gated
```rust
-#[cfg(all(feature = "jit", target_arch = "x86_64"))]
+#[cfg(all(feature = "jit", any(target_arch = "x86_64", target_arch = "aarch64")))]
pub mod jit;
```
@@ -133,7 +133,7 @@ pub use aarch64::compile;
pub mod interpreter;
mod engine;
-#[cfg(all(feature = "jit", target_arch = "x86_64"))]
+#[cfg(all(feature = "jit", any(target_arch = "x86_64", target_arch = "aarch64")))]
pub mod jit;
// Re-exports
@@ -198,10 +198,10 @@ The `src/jit/mod.rs` module exists for backwards compatibility and convenience.
```rust
// Re-export from canonical locations
-#[cfg(all(feature = "jit", target_arch = "x86_64"))]
+#[cfg(all(feature = "jit", any(target_arch = "x86_64", target_arch = "aarch64")))]
pub use crate::nfa::tagged::jit::{TaggedNfaJit, compile_tagged_nfa};
-#[cfg(all(feature = "jit", target_arch = "x86_64"))]
+#[cfg(all(feature = "jit", any(target_arch = "x86_64", target_arch = "aarch64")))]
pub use crate::dfa::lazy::jit::{DfaJit, compile_dfa};
```
diff --git a/docs/features.md b/docs/features.md
index 02036c2..0564991 100644
--- a/docs/features.md
+++ b/docs/features.md
@@ -265,7 +265,7 @@ JIT compilation is beneficial when:
### JIT Requirements
-- Only available on x86-64 architecture
+- Available on x86-64 (Linux, macOS, Windows) and ARM64 (Linux, macOS)
- Requires `jit` feature flag
- Automatically falls back to interpreted engines if compilation fails
@@ -506,15 +506,21 @@ Ensure the engine matches your expectations for the pattern type.
### Current Limitations
-1. **JIT**: Only available on x86-64 architecture
+1. **SIMD**: Only available on x86-64 with AVX2 support
2. **Multiline mode**: Currently `.` never matches newline
3. **Backreferences**: Cannot be combined with JIT DFA (uses BacktrackingJit instead)
4. **Variable-width lookbehind**: Limited support (fixed-width lookbehind only)
### Platform Support
-- **x86-64**: All features including JIT
-- **Other architectures**: Interpreted engines only (no JIT)
+| Platform | JIT Support | SIMD Support |
+|----------|-------------|--------------|
+| Linux x86-64 | ✓ | ✓ (AVX2) |
+| Linux ARM64 | ✓ | ✗ |
+| macOS x86-64 | ✓ | ✓ (AVX2) |
+| macOS ARM64 (Apple Silicon) | ✓ | ✗ |
+| Windows x86-64 | ✓ | ✓ (AVX2) |
+| Other | ✗ | ✗ |
### Feature Compatibility
diff --git a/src/dfa/eager/interpreter/dfa.rs b/src/dfa/eager/interpreter/dfa.rs
index 159220a..a3140c9 100644
--- a/src/dfa/eager/interpreter/dfa.rs
+++ b/src/dfa/eager/interpreter/dfa.rs
@@ -256,7 +256,7 @@ impl EagerDfa {
return Some((0, end));
}
for (i, &byte) in input.iter().enumerate() {
- if byte == b'\n' && i + 1 <= input.len() {
+ if byte == b'\n' && i < input.len() {
if let Some(end) = self.find_at(input, i + 1) {
return Some((i + 1, end));
}
diff --git a/src/dfa/lazy/interpreter/dfa.rs b/src/dfa/lazy/interpreter/dfa.rs
index 230af38..91980e4 100644
--- a/src/dfa/lazy/interpreter/dfa.rs
+++ b/src/dfa/lazy/interpreter/dfa.rs
@@ -146,11 +146,7 @@ impl LazyDfa {
};
let pos_ctx = if self.ctx.has_anchors {
- if self.ctx.has_multiline_anchors && byte == b'\n' {
- Some(PositionContext::middle())
- } else {
- Some(PositionContext::middle())
- }
+ Some(PositionContext::middle())
} else {
None
};
@@ -208,7 +204,7 @@ impl LazyDfa {
let next_id = get_or_create_state_with_class(&mut self.ctx, next_closure, curr_class);
let next_idx = state_index(next_id);
- let is_match = self.ctx.states.get(next_idx).map_or(false, |s| s.is_match);
+ let is_match = self.ctx.states.get(next_idx).is_some_and(|s| s.is_match);
let cache_idx = (state + byte as u32) as usize;
if cache_idx < self.ctx.transitions.len() {
@@ -332,15 +328,13 @@ impl LazyDfa {
result[byte as usize] = Some(next_id);
let next_idx = state_index(next_id);
- let is_match = self.ctx.states.get(next_idx).map_or(false, |s| s.is_match);
+ let is_match = self.ctx.states.get(next_idx).is_some_and(|s| s.is_match);
let cache_idx = (state + byte as u32) as usize;
if cache_idx < self.ctx.transitions.len() {
self.ctx.transitions[cache_idx] = tag_state(next_id, is_match);
}
- } else {
- if cache_idx < self.ctx.transitions.len() {
- self.ctx.transitions[cache_idx] = DEAD_STATE;
- }
+ } else if cache_idx < self.ctx.transitions.len() {
+ self.ctx.transitions[cache_idx] = DEAD_STATE;
}
}
}
@@ -405,7 +399,7 @@ impl LazyDfa {
return Some((0, end));
}
for (i, &byte) in input.iter().enumerate() {
- if byte == b'\n' && i + 1 <= input.len() {
+ if byte == b'\n' && i < input.len() {
if let Some(end) = self.find_at(input, i + 1) {
return Some((i + 1, end));
}
@@ -467,11 +461,7 @@ impl LazyDfa {
let mut start_set = BTreeSet::new();
start_set.insert(self.ctx.nfa.start);
- let is_at_boundary = if self.ctx.has_word_boundary {
- None
- } else {
- None
- };
+ let is_at_boundary: Option
= None;
let start_closure = epsilon_closure_with_context(
&self.ctx.nfa,
@@ -755,7 +745,7 @@ impl LazyDfa {
if let Some(nfa_state) = self.ctx.nfa.get(nfa_id) {
if nfa_state.is_match {
// Check if this match state has any pending END anchor
- let has_end_anchor = nfa_state.instruction.as_ref().map_or(false, |instr| {
+ let has_end_anchor = nfa_state.instruction.as_ref().is_some_and(|instr| {
matches!(instr, NfaInstruction::EndOfLine | NfaInstruction::EndOfText)
});
if !has_end_anchor {
@@ -778,6 +768,7 @@ impl LazyDfa {
/// For example, in pattern `(?m)^A|B$`:
/// - Branch 1 reaches match through ^A (no end anchor)
/// - Branch 2 reaches match through B$ (EndOfLine)
+ ///
/// After matching, the DFA state may include states from both branches.
/// If EndOfLine was filtered out during epsilon closure (because we're not at EOL),
/// we shouldn't require it - branch 1's path is still valid.
@@ -786,9 +777,10 @@ impl LazyDfa {
F: Fn(&NfaInstruction) -> bool,
{
nfa_states.iter().any(|&nfa_id| {
- self.ctx.nfa.get(nfa_id).map_or(false, |nfa_state| {
- nfa_state.instruction.as_ref().map_or(false, &pred)
- })
+ self.ctx
+ .nfa
+ .get(nfa_id)
+ .is_some_and(|nfa_state| nfa_state.instruction.as_ref().is_some_and(&pred))
})
}
diff --git a/src/engine/executor.rs b/src/engine/executor.rs
index 08eb214..16909a7 100644
--- a/src/engine/executor.rs
+++ b/src/engine/executor.rs
@@ -15,7 +15,7 @@ use crate::vm::{
BacktrackingVm, CodepointClassMatcher, PikeVm, PikeVmContext, ShiftOr, ShiftOrWide,
};
-#[cfg(all(feature = "jit", target_arch = "x86_64"))]
+#[cfg(all(feature = "jit", any(target_arch = "x86_64", target_arch = "aarch64")))]
use crate::jit;
use super::{select_engine, select_engine_from_hir, EngineType};
@@ -45,7 +45,7 @@ pub struct CompiledRegex {
/// BacktrackingJit for fast single-pass capture extraction in JIT mode.
/// Used by JitShiftOr when pattern has captures.
/// This is the JIT equivalent of backtracking_vm.
- #[cfg(all(feature = "jit", target_arch = "x86_64"))]
+ #[cfg(all(feature = "jit", any(target_arch = "x86_64", target_arch = "aarch64")))]
backtracking_jit: Option,
}
@@ -58,6 +58,7 @@ impl std::fmt::Debug for CompiledRegex {
}
}
+#[allow(clippy::large_enum_variant)]
enum CompiledInner {
PikeVm(PikeVm),
ShiftOr(ShiftOr),
@@ -77,19 +78,19 @@ enum CompiledInner {
/// Uses liveness analysis for efficient single-pass capture extraction.
/// Always available (no JIT required).
TaggedNfaInterp(TaggedNfaEngine),
- #[cfg(all(feature = "jit", target_arch = "x86_64"))]
+ #[cfg(all(feature = "jit", any(target_arch = "x86_64", target_arch = "aarch64")))]
Jit(jit::CompiledRegex),
/// Tagged NFA JIT engine for patterns with lookaround or non-greedy.
/// Uses liveness analysis for efficient single-pass capture extraction.
/// JIT compiles the NFA to native code for better performance.
- #[cfg(all(feature = "jit", target_arch = "x86_64"))]
+ #[cfg(all(feature = "jit", any(target_arch = "x86_64", target_arch = "aarch64")))]
TaggedNfaJit(jit::TaggedNfaJit),
/// Backtracking JIT engine for patterns with backreferences.
/// Uses PCRE-style backtracking for fast backreference matching.
- #[cfg(all(feature = "jit", target_arch = "x86_64"))]
+ #[cfg(all(feature = "jit", any(target_arch = "x86_64", target_arch = "aarch64")))]
Backtracking(jit::BacktrackingJit),
/// JIT-compiled Shift-Or engine for word boundary patterns.
- #[cfg(all(feature = "jit", target_arch = "x86_64"))]
+ #[cfg(all(feature = "jit", any(target_arch = "x86_64", target_arch = "aarch64")))]
JitShiftOr(jit::JitShiftOr),
}
@@ -105,13 +106,13 @@ impl CompiledRegex {
CompiledInner::CodepointClass(_) => "CodepointClass",
CompiledInner::BacktrackingVm(_) => "BacktrackingVm",
CompiledInner::TaggedNfaInterp(_) => "TaggedNfa",
- #[cfg(all(feature = "jit", target_arch = "x86_64"))]
+ #[cfg(all(feature = "jit", any(target_arch = "x86_64", target_arch = "aarch64")))]
CompiledInner::Jit(_) => "Jit",
- #[cfg(all(feature = "jit", target_arch = "x86_64"))]
+ #[cfg(all(feature = "jit", any(target_arch = "x86_64", target_arch = "aarch64")))]
CompiledInner::TaggedNfaJit(_) => "TaggedNfaJit",
- #[cfg(all(feature = "jit", target_arch = "x86_64"))]
+ #[cfg(all(feature = "jit", any(target_arch = "x86_64", target_arch = "aarch64")))]
CompiledInner::Backtracking(_) => "BacktrackingJit",
- #[cfg(all(feature = "jit", target_arch = "x86_64"))]
+ #[cfg(all(feature = "jit", any(target_arch = "x86_64", target_arch = "aarch64")))]
CompiledInner::JitShiftOr(_) => "JitShiftOr",
}
}
@@ -158,13 +159,13 @@ impl CompiledRegex {
CompiledInner::CodepointClass(matcher) => matcher.is_match(input),
CompiledInner::BacktrackingVm(vm) => vm.find(input).is_some(),
CompiledInner::TaggedNfaInterp(engine) => engine.is_match(input),
- #[cfg(all(feature = "jit", target_arch = "x86_64"))]
+ #[cfg(all(feature = "jit", any(target_arch = "x86_64", target_arch = "aarch64")))]
CompiledInner::Jit(jit) => jit.is_match(input),
- #[cfg(all(feature = "jit", target_arch = "x86_64"))]
+ #[cfg(all(feature = "jit", any(target_arch = "x86_64", target_arch = "aarch64")))]
CompiledInner::TaggedNfaJit(engine) => engine.is_match(input),
- #[cfg(all(feature = "jit", target_arch = "x86_64"))]
+ #[cfg(all(feature = "jit", any(target_arch = "x86_64", target_arch = "aarch64")))]
CompiledInner::Backtracking(jit) => jit.is_match(input),
- #[cfg(all(feature = "jit", target_arch = "x86_64"))]
+ #[cfg(all(feature = "jit", any(target_arch = "x86_64", target_arch = "aarch64")))]
CompiledInner::JitShiftOr(jit) => jit.find(input).is_some(),
}
}
@@ -212,11 +213,12 @@ impl CompiledRegex {
// Find the first word boundary in the lookback window
for i in (start_pos..inner_pos).rev() {
- if i == 0 || !is_word_byte(input[i - 1]) {
- if i < input.len() && is_word_byte(input[i]) {
- candidate = i;
- break;
- }
+ if (i == 0 || !is_word_byte(input[i - 1]))
+ && i < input.len()
+ && is_word_byte(input[i])
+ {
+ candidate = i;
+ break;
}
}
@@ -253,13 +255,13 @@ impl CompiledRegex {
CompiledInner::CodepointClass(matcher) => matcher.find(input),
CompiledInner::BacktrackingVm(vm) => vm.find(input),
CompiledInner::TaggedNfaInterp(engine) => engine.find(input),
- #[cfg(all(feature = "jit", target_arch = "x86_64"))]
+ #[cfg(all(feature = "jit", any(target_arch = "x86_64", target_arch = "aarch64")))]
CompiledInner::Jit(jit) => jit.find(input),
- #[cfg(all(feature = "jit", target_arch = "x86_64"))]
+ #[cfg(all(feature = "jit", any(target_arch = "x86_64", target_arch = "aarch64")))]
CompiledInner::TaggedNfaJit(engine) => engine.find(input),
- #[cfg(all(feature = "jit", target_arch = "x86_64"))]
+ #[cfg(all(feature = "jit", any(target_arch = "x86_64", target_arch = "aarch64")))]
CompiledInner::Backtracking(jit) => jit.find(input),
- #[cfg(all(feature = "jit", target_arch = "x86_64"))]
+ #[cfg(all(feature = "jit", any(target_arch = "x86_64", target_arch = "aarch64")))]
CompiledInner::JitShiftOr(jit) => jit.find(input),
}
}
@@ -283,17 +285,17 @@ impl CompiledRegex {
// TaggedNfa interpreter does single-pass capture extraction
engine.captures(input)
}
- #[cfg(all(feature = "jit", target_arch = "x86_64"))]
+ #[cfg(all(feature = "jit", any(target_arch = "x86_64", target_arch = "aarch64")))]
CompiledInner::TaggedNfaJit(engine) => {
// TaggedNfa JIT does single-pass capture extraction
engine.captures(input)
}
- #[cfg(all(feature = "jit", target_arch = "x86_64"))]
+ #[cfg(all(feature = "jit", any(target_arch = "x86_64", target_arch = "aarch64")))]
CompiledInner::Backtracking(jit) => {
// Backtracking JIT does single-pass capture extraction
jit.captures(input)
}
- #[cfg(all(feature = "jit", target_arch = "x86_64"))]
+ #[cfg(all(feature = "jit", any(target_arch = "x86_64", target_arch = "aarch64")))]
CompiledInner::Jit(_) => {
// Fast path: if we have BacktrackingVm, use it for single-pass capture extraction
if let Some(ref backtracking_vm) = self.backtracking_vm {
@@ -313,11 +315,9 @@ impl CompiledRegex {
// Use the optimized context-based method
vm.captures_from_start_with_context(&input[start..], ctx)
.map(|mut caps| {
- for slot in &mut caps {
- if let Some((s, e)) = slot {
- *s += start;
- *e += start;
- }
+ for (s, e) in caps.iter_mut().flatten() {
+ *s += start;
+ *e += start;
}
caps
})
@@ -346,16 +346,14 @@ impl CompiledRegex {
vm.captures_from_start_with_context(&input[start..], ctx)
.map(|mut caps| {
// Adjust capture positions to absolute offsets
- for slot in &mut caps {
- if let Some((s, e)) = slot {
- *s += start;
- *e += start;
- }
+ for (s, e) in caps.iter_mut().flatten() {
+ *s += start;
+ *e += start;
}
caps
})
}
- #[cfg(all(feature = "jit", target_arch = "x86_64"))]
+ #[cfg(all(feature = "jit", any(target_arch = "x86_64", target_arch = "aarch64")))]
CompiledInner::JitShiftOr(_) => {
// Use BacktrackingJit for capture extraction if available
// This is the JIT equivalent of BacktrackingVm used by non-JIT ShiftOr
@@ -372,11 +370,9 @@ impl CompiledRegex {
let ctx = ctx_ref.as_mut()?;
vm.captures_from_start_with_context(&input[start..], ctx)
.map(|mut caps| {
- for slot in &mut caps {
- if let Some((s, e)) = slot {
- *s += start;
- *e += start;
- }
+ for (s, e) in caps.iter_mut().flatten() {
+ *s += start;
+ *e += start;
}
caps
})
@@ -417,13 +413,13 @@ impl CompiledRegex {
}
CompiledInner::BacktrackingVm(vm) => vm.find_at(input, pos),
CompiledInner::TaggedNfaInterp(engine) => engine.find_at(input, pos),
- #[cfg(all(feature = "jit", target_arch = "x86_64"))]
+ #[cfg(all(feature = "jit", any(target_arch = "x86_64", target_arch = "aarch64")))]
CompiledInner::Jit(jit) => jit.find_at(input, pos),
- #[cfg(all(feature = "jit", target_arch = "x86_64"))]
+ #[cfg(all(feature = "jit", any(target_arch = "x86_64", target_arch = "aarch64")))]
CompiledInner::TaggedNfaJit(engine) => engine.find_at(input, pos),
- #[cfg(all(feature = "jit", target_arch = "x86_64"))]
+ #[cfg(all(feature = "jit", any(target_arch = "x86_64", target_arch = "aarch64")))]
CompiledInner::Backtracking(jit) => jit.find_at(input, pos),
- #[cfg(all(feature = "jit", target_arch = "x86_64"))]
+ #[cfg(all(feature = "jit", any(target_arch = "x86_64", target_arch = "aarch64")))]
CompiledInner::JitShiftOr(jit) => jit.try_match_at(input, pos),
}
}
@@ -487,7 +483,7 @@ pub fn compile(nfa: Nfa) -> Result {
capture_vm: RwLock::new(None),
capture_ctx: RwLock::new(None),
backtracking_vm: None,
- #[cfg(all(feature = "jit", target_arch = "x86_64"))]
+ #[cfg(all(feature = "jit", any(target_arch = "x86_64", target_arch = "aarch64")))]
backtracking_jit: None,
})
}
@@ -508,7 +504,7 @@ pub fn compile_from_hir(hir: &Hir) -> Result {
capture_vm: RwLock::new(None),
capture_ctx: RwLock::new(None),
backtracking_vm: None,
- #[cfg(all(feature = "jit", target_arch = "x86_64"))]
+ #[cfg(all(feature = "jit", any(target_arch = "x86_64", target_arch = "aarch64")))]
backtracking_jit: None,
});
}
@@ -529,7 +525,7 @@ pub fn compile_from_hir(hir: &Hir) -> Result {
capture_vm: RwLock::new(None),
capture_ctx: RwLock::new(None),
backtracking_vm: None,
- #[cfg(all(feature = "jit", target_arch = "x86_64"))]
+ #[cfg(all(feature = "jit", any(target_arch = "x86_64", target_arch = "aarch64")))]
backtracking_jit: None,
});
}
@@ -663,7 +659,7 @@ pub fn compile_from_hir(hir: &Hir) -> Result {
capture_vm: RwLock::new(None),
capture_ctx: RwLock::new(None),
backtracking_vm,
- #[cfg(all(feature = "jit", target_arch = "x86_64"))]
+ #[cfg(all(feature = "jit", any(target_arch = "x86_64", target_arch = "aarch64")))]
backtracking_jit: None,
})
}
@@ -686,7 +682,7 @@ pub fn compile_with_pikevm(hir: &Hir) -> Result {
capture_vm: RwLock::new(None),
capture_ctx: RwLock::new(None),
backtracking_vm: None,
- #[cfg(all(feature = "jit", target_arch = "x86_64"))]
+ #[cfg(all(feature = "jit", any(target_arch = "x86_64", target_arch = "aarch64")))]
backtracking_jit: None,
})
}
@@ -713,7 +709,7 @@ pub fn compile_with_jit(hir: &Hir) -> Result {
capture_vm: RwLock::new(None),
capture_ctx: RwLock::new(None),
backtracking_vm: None,
- #[cfg(all(feature = "jit", target_arch = "x86_64"))]
+ #[cfg(all(feature = "jit", any(target_arch = "x86_64", target_arch = "aarch64")))]
backtracking_jit: None,
});
}
@@ -721,7 +717,7 @@ pub fn compile_with_jit(hir: &Hir) -> Result {
// 1. Complex Unicode patterns with large unicode classes → TaggedNfa JIT
// These patterns use CodepointClass instructions which DFA cannot handle.
// Route them to TaggedNfa JIT which supports CodepointClass.
- #[cfg(all(feature = "jit", target_arch = "x86_64"))]
+ #[cfg(all(feature = "jit", any(target_arch = "x86_64", target_arch = "aarch64")))]
if hir.props.has_large_unicode_class {
let literals = extract_literals(hir);
let prefilter = Prefilter::from_literals(&literals);
@@ -757,7 +753,7 @@ pub fn compile_with_jit(hir: &Hir) -> Result {
}
// Non-JIT: Large unicode classes go to TaggedNfa interpreter
- #[cfg(not(all(feature = "jit", target_arch = "x86_64")))]
+ #[cfg(not(all(feature = "jit", any(target_arch = "x86_64", target_arch = "aarch64"))))]
if hir.props.has_large_unicode_class {
let literals = extract_literals(hir);
let prefilter = Prefilter::from_literals(&literals);
@@ -775,7 +771,7 @@ pub fn compile_with_jit(hir: &Hir) -> Result {
// 2. Patterns with backreferences → Backtracking JIT (only way to handle backrefs)
// Backtracking JIT is required for backreferences since DFA cannot handle them.
- #[cfg(all(feature = "jit", target_arch = "x86_64"))]
+ #[cfg(all(feature = "jit", any(target_arch = "x86_64", target_arch = "aarch64")))]
if hir.props.has_backrefs && !hir.props.has_lookaround {
let literals = extract_literals(hir);
let prefilter = Prefilter::from_literals(&literals);
@@ -788,7 +784,10 @@ pub fn compile_with_jit(hir: &Hir) -> Result {
capture_vm: RwLock::new(None),
capture_ctx: RwLock::new(None),
backtracking_vm: None,
- #[cfg(all(feature = "jit", target_arch = "x86_64"))]
+ #[cfg(all(
+ feature = "jit",
+ any(target_arch = "x86_64", target_arch = "aarch64")
+ ))]
backtracking_jit: None,
});
}
@@ -808,7 +807,7 @@ pub fn compile_with_jit(hir: &Hir) -> Result {
// because DFA JIT is much faster. DFA JIT handles captures via two-pass:
// 1. Fast DFA JIT for find()
// 2. PikeVM on matched substring for captures() only when needed
- #[cfg(all(feature = "jit", target_arch = "x86_64"))]
+ #[cfg(all(feature = "jit", any(target_arch = "x86_64", target_arch = "aarch64")))]
if hir.props.has_lookaround || hir.props.has_non_greedy {
let literals = extract_literals(hir);
let prefilter = Prefilter::from_literals(&literals);
@@ -849,7 +848,7 @@ pub fn compile_with_jit(hir: &Hir) -> Result {
// Fall back to TaggedNfa interpreter when JIT feature is not available
// Note: TaggedNfa interpreter is now always available (faster than PikeVm for lookaround)
- #[cfg(not(all(feature = "jit", target_arch = "x86_64")))]
+ #[cfg(not(all(feature = "jit", any(target_arch = "x86_64", target_arch = "aarch64"))))]
if hir.props.has_lookaround || hir.props.has_non_greedy {
let literals = extract_literals(hir);
let prefilter = Prefilter::from_literals(&literals);
@@ -866,7 +865,7 @@ pub fn compile_with_jit(hir: &Hir) -> Result {
}
// For backrefs without JIT, fall back to PikeVM
- #[cfg(not(all(feature = "jit", target_arch = "x86_64")))]
+ #[cfg(not(all(feature = "jit", any(target_arch = "x86_64", target_arch = "aarch64"))))]
if hir.props.has_backrefs {
return compile_with_pikevm(hir);
}
@@ -875,7 +874,7 @@ pub fn compile_with_jit(hir: &Hir) -> Result {
// ShiftOr's bit-parallel algorithm is faster than DFA JIT for patterns with
// many alternations and no common prefix (no effective prefilter).
// DFA JIT excels when there's a good prefilter to skip non-matching positions.
- #[cfg(all(feature = "jit", target_arch = "x86_64"))]
+ #[cfg(all(feature = "jit", any(target_arch = "x86_64", target_arch = "aarch64")))]
{
use crate::vm::is_shift_or_compatible;
let literals = extract_literals(hir);
@@ -928,7 +927,7 @@ pub fn compile_with_jit(hir: &Hir) -> Result {
// 4. Simple patterns with effective prefilter → DFA JIT
// DFA JIT benefits from prefilter to quickly skip non-matching positions.
- #[cfg(all(feature = "jit", target_arch = "x86_64"))]
+ #[cfg(all(feature = "jit", any(target_arch = "x86_64", target_arch = "aarch64")))]
{
let literals = extract_literals(hir);
let prefilter = Prefilter::from_literals(&literals);
@@ -1115,7 +1114,7 @@ mod tests {
// TaggedNfa integration tests (backrefs, lookaround, non-greedy)
// These patterns trigger the TaggedNfaEngine path when JIT is enabled
- #[cfg(all(feature = "jit", target_arch = "x86_64"))]
+ #[cfg(all(feature = "jit", any(target_arch = "x86_64", target_arch = "aarch64")))]
mod tagged_nfa_integration {
use super::*;
use crate::engine::compile_with_jit;
diff --git a/src/engine/selector.rs b/src/engine/selector.rs
index 43fed39..33ac246 100644
--- a/src/engine/selector.rs
+++ b/src/engine/selector.rs
@@ -143,8 +143,8 @@ impl Capabilities {
#[cfg(feature = "jit")]
fn detect_jit() -> bool {
- // JIT is available on x86-64 Linux/macOS/Windows
- cfg!(target_arch = "x86_64")
+ // JIT is available on x86-64 and aarch64 (Linux/macOS/Windows)
+ cfg!(any(target_arch = "x86_64", target_arch = "aarch64"))
}
}
diff --git a/src/hir/builder.rs b/src/hir/builder.rs
index fca60f4..14c09c2 100644
--- a/src/hir/builder.rs
+++ b/src/hir/builder.rs
@@ -599,6 +599,7 @@ impl HirTranslator {
/// Builds a trie-based HIR expression for UTF-8 sequences.
/// This shares common prefixes to minimize NFA states.
+ #[allow(clippy::only_used_in_recursion)]
fn build_utf8_trie(&self, sequences: &[Utf8Sequence]) -> HirExpr {
if sequences.is_empty() {
return HirExpr::Empty;
diff --git a/src/hir/mod.rs b/src/hir/mod.rs
index 0acd2b8..6f8dedb 100644
--- a/src/hir/mod.rs
+++ b/src/hir/mod.rs
@@ -71,7 +71,7 @@ pub struct CodepointClass {
/// Whether this class is negated.
pub negated: bool,
/// Precomputed ASCII bitmap for fast lookup of codepoints 0-127.
- /// ascii_bitmap[0] covers bits 0-63, ascii_bitmap[1] covers bits 64-127.
+ /// `ascii_bitmap[0]` covers bits 0-63, `ascii_bitmap[1]` covers bits 64-127.
/// A set bit means the codepoint is IN the class (before negation is applied).
pub ascii_bitmap: [u64; 2],
}
diff --git a/src/hir/prefix_opt.rs b/src/hir/prefix_opt.rs
index 78a4441..9f6c516 100644
--- a/src/hir/prefix_opt.rs
+++ b/src/hir/prefix_opt.rs
@@ -50,7 +50,7 @@ impl TrieNode {
fn insert(&mut self, bytes: &[u8], capture_index: Option, capture_name: Option) {
let mut node = self;
for &byte in bytes {
- node = node.children.entry(byte).or_insert_with(TrieNode::new);
+ node = node.children.entry(byte).or_default();
}
node.is_terminal = true;
node.capture_index = capture_index;
@@ -61,11 +61,7 @@ impl TrieNode {
fn to_hir(&self) -> HirExpr {
// If this is a terminal with no children, return empty
if self.children.is_empty() {
- return if self.is_terminal {
- HirExpr::Empty
- } else {
- HirExpr::Empty
- };
+ return HirExpr::Empty;
}
// Collect all children
diff --git a/src/jit/aarch64.rs b/src/jit/aarch64.rs
new file mode 100644
index 0000000..4915fe9
--- /dev/null
+++ b/src/jit/aarch64.rs
@@ -0,0 +1,850 @@
+//! AArch64 (ARM64) code generation using dynasm.
+//!
+//! This module emits native ARM64 assembly code for DFA state machines.
+//! All code is W^X compliant and optimized for performance.
+//!
+//! # Platform Support
+//!
+//! Uses AAPCS64 calling convention on all ARM64 platforms:
+//! - **Arguments**: X0-X7
+//! - **Return value**: X0
+//! - **Callee-saved**: X19-X28, SP
+//! - **Link register**: X30
+//! - **Frame pointer**: X29
+
+use crate::dfa::DfaStateId;
+use crate::error::{Error, ErrorKind, Result};
+use crate::jit::codegen_aarch64::{MaterializedDfa, MaterializedState};
+use dynasm::dynasm;
+use dynasmrt::{
+ aarch64::Assembler, AssemblyOffset, DynamicLabel, DynasmApi, DynasmLabelApi, ExecutableBuffer,
+};
+
+/// Compiles a materialized DFA to ARM64 machine code.
+///
+/// # Calling Convention (AAPCS64)
+///
+/// The function accepts two arguments:
+/// - X0: input pointer (const uint8_t*)
+/// - X1: length (size_t)
+///
+/// # Internal Register Usage
+/// - `x19` = current position in input (mutable, incremented during execution)
+/// - `x20` = input base pointer (immutable)
+/// - `x21` = input end pointer (base + len, immutable)
+/// - `x22` = last match position, initialized to -1 (for longest-match semantics)
+/// - `x23` = search start position (for unanchored search)
+/// - `x24` = prev_char_class (0 = NonWord, 1 = Word) for word boundary patterns
+/// - `x9-x15` = scratch registers
+///
+/// # Function Signature
+/// ```c
+/// int64_t match_fn(const uint8_t* input, size_t len);
+/// ```
+///
+/// Returns:
+/// - >= 0: Match found, packed as (start << 32 | end)
+/// - -1: No match
+pub fn compile_states(
+ dfa: &MaterializedDfa,
+) -> Result<(ExecutableBuffer, AssemblyOffset, Option)> {
+ let mut asm = Assembler::new().map_err(|e| {
+ Error::new(
+ ErrorKind::Jit(format!("Failed to create assembler: {:?}", e)),
+ "",
+ )
+ })?;
+
+ // Create state label lookup using Vec for O(1) lookup
+ let max_state_id = dfa.states.iter().map(|s| s.id).max().unwrap_or(0) as usize;
+ let mut state_labels: Vec