From 6c9066032d9e68ab03949435930a4f2d51da2fe8 Mon Sep 17 00:00:00 2001 From: Matt Turner Date: Sun, 2 Aug 2026 01:23:38 -0400 Subject: [PATCH 1/7] Update sljit to current master Pulls in the DEC Alpha 64-bit JIT backend (sljit #364), which the following commits build on, along with the other sljit changes made since the previous update. --- deps/sljit | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps/sljit b/deps/sljit index 45f910b78..67893de6c 160000 --- a/deps/sljit +++ b/deps/sljit @@ -1 +1 @@ -Subproject commit 45f910b78c6605ebf5b53d3ec7cb00f2312fe417 +Subproject commit 67893de6cf6fb03fc2cb8ce9bf521a083c750e98 From 1bc35e36321122defffb43464617c313ce2b3ac7 Mon Sep 17 00:00:00 2001 From: Matt Turner Date: Fri, 3 Jul 2026 18:50:52 -0400 Subject: [PATCH 2/7] Document and package the DEC Alpha JIT backend List the new sljit Alpha backend sources in EXTRA_DIST and add DEC Alpha 64-bit to the platforms supported by the JIT compiler in the documentation. The corresponding deps/sljit submodule bump is deferred until the backend is merged upstream; validated under qemu-alpha (pcre2_jit_test passes in 8/16/32-bit modes and RunTest passes with and without JIT). --- Makefile.am | 1 + doc/html/pcre2jit.html | 1 + doc/pcre2jit.3 | 1 + maint/manifest-tarball | 1 + 4 files changed, 4 insertions(+) diff --git a/Makefile.am b/Makefile.am index e5409e0dc..5ecb47f07 100644 --- a/Makefile.am +++ b/Makefile.am @@ -503,6 +503,7 @@ EXTRA_DIST += \ deps/sljit/sljit_src/sljitConfigInternal.h \ deps/sljit/sljit_src/sljitLir.c \ deps/sljit/sljit_src/sljitLir.h \ + deps/sljit/sljit_src/sljitNativeALPHA_64.c \ deps/sljit/sljit_src/sljitNativeARM_32.c \ deps/sljit/sljit_src/sljitNativeARM_64.c \ deps/sljit/sljit_src/sljitNativeARM_T2_32.c \ diff --git a/doc/html/pcre2jit.html b/doc/html/pcre2jit.html index cc26cc060..febadf0bf 100644 --- a/doc/html/pcre2jit.html +++ b/doc/html/pcre2jit.html @@ -54,6 +54,7 @@

AVAILABILITY OF JIT SUPPORT

   ARM 32-bit (v7, and Thumb2)
   ARM 64-bit
+  DEC Alpha 64-bit
   IBM s390x 64 bit
   Intel x86 32-bit and 64-bit
   LoongArch 64 bit
diff --git a/doc/pcre2jit.3 b/doc/pcre2jit.3
index 95451b56d..2d9c57cb1 100644
--- a/doc/pcre2jit.3
+++ b/doc/pcre2jit.3
@@ -29,6 +29,7 @@ platforms:
 .sp
   ARM 32-bit (v7, and Thumb2)
   ARM 64-bit
+  DEC Alpha 64-bit
   IBM s390x 64 bit
   Intel x86 32-bit and 64-bit
   LoongArch 64 bit
diff --git a/maint/manifest-tarball b/maint/manifest-tarball
index efd29ff4a..46cd88fc2 100644
--- a/maint/manifest-tarball
+++ b/maint/manifest-tarball
@@ -57,6 +57,7 @@ drwxr-xr-x tarball-dir/pcre2-SNAPSHOT/deps/sljit/sljit_src/allocator_src
 -rw-r--r-- tarball-dir/pcre2-SNAPSHOT/deps/sljit/sljit_src/sljitConfigInternal.h
 -rw-r--r-- tarball-dir/pcre2-SNAPSHOT/deps/sljit/sljit_src/sljitLir.c
 -rw-r--r-- tarball-dir/pcre2-SNAPSHOT/deps/sljit/sljit_src/sljitLir.h
+-rw-r--r-- tarball-dir/pcre2-SNAPSHOT/deps/sljit/sljit_src/sljitNativeALPHA_64.c
 -rw-r--r-- tarball-dir/pcre2-SNAPSHOT/deps/sljit/sljit_src/sljitNativeARM_32.c
 -rw-r--r-- tarball-dir/pcre2-SNAPSHOT/deps/sljit/sljit_src/sljitNativeARM_64.c
 -rw-r--r-- tarball-dir/pcre2-SNAPSHOT/deps/sljit/sljit_src/sljitNativeARM_T2_32.c

From 24d2cbe9df9c5c8d8881ff6cad818dcb1de8499e Mon Sep 17 00:00:00 2001
From: Matt Turner 
Date: Mon, 13 Jul 2026 23:42:02 -0400
Subject: [PATCH 3/7] alpha: add CMPBGE-based fast-forward character scanning

Use the Alpha CMPBGE instruction for 8-byte-at-a-time character
scanning in the JIT compiler, similar to how other architectures use
SIMD instructions. CMPBGE compares 8 byte pairs and produces an 8-bit
mask, which combined with XOR gives us byte-equality matching across a
whole quadword at once.

Implement fast_forward_char_simd, fast_requested_char_simd, and
fast_forward_char_pair_simd. The first-match step uses a base-ISA
binary search (NEGQ+AND to isolate the lowest set bit, then three
AND+SELECT pairs), identical to glibc's Alpha strchr; no CIX required.

For 16/32-bit code units, a post-processing step compacts the per-byte
CMPBGE mask into a per-code-unit mask by ANDing adjacent bits.

The char-pair function handles Alpha's 8-byte alignment requirement for
LDQ by constructing the unaligned second data word from two aligned
loads using SRL/SHL/OR.
---
 src/pcre2_jit_compile.c  |  11 +-
 src/pcre2_jit_simd_inc.h | 513 ++++++++++++++++++++++++++++++++++++++-
 2 files changed, 520 insertions(+), 4 deletions(-)

diff --git a/src/pcre2_jit_compile.c b/src/pcre2_jit_compile.c
index 9013b6864..032e80680 100644
--- a/src/pcre2_jit_compile.c
+++ b/src/pcre2_jit_compile.c
@@ -607,6 +607,15 @@ typedef struct compare_context {
 #define ARGUMENTS     SLJIT_S4
 #define RETURN_ADDR   SLJIT_R4
 
+/* Number of scratch registers the generated code may use. The Alpha CMPBGE
+   scanning loops keep up to five replicated character constants live across a
+   loop, so they use scratch registers above RETURN_ADDR and must declare them. */
+#if (defined SLJIT_CONFIG_ALPHA && SLJIT_CONFIG_ALPHA)
+#define SCRATCH_REGISTERS 10
+#else
+#define SCRATCH_REGISTERS 5
+#endif
+
 #if (defined SLJIT_CONFIG_X86_32 && SLJIT_CONFIG_X86_32)
 #define HAS_VIRTUAL_REGISTERS 1
 #else
@@ -13784,7 +13793,7 @@ common->compiler = compiler;
 
 /* Main pcre2_jit_exec entry. */
 SLJIT_ASSERT((private_data_size & (sizeof(sljit_sw) - 1)) == 0);
-sljit_emit_enter(compiler, 0, SLJIT_ARGS1(W, W), 5 | SLJIT_ENTER_VECTOR(SLJIT_NUMBER_OF_SCRATCH_VECTOR_REGISTERS), 5, private_data_size);
+sljit_emit_enter(compiler, 0, SLJIT_ARGS1(W, W), SCRATCH_REGISTERS | SLJIT_ENTER_VECTOR(SLJIT_NUMBER_OF_SCRATCH_VECTOR_REGISTERS), 5, private_data_size);
 
 /* Register init. */
 reset_ovector(common, (re->top_bracket + 1) * 2);
diff --git a/src/pcre2_jit_simd_inc.h b/src/pcre2_jit_simd_inc.h
index 0a24cdad8..7b4036d90 100644
--- a/src/pcre2_jit_simd_inc.h
+++ b/src/pcre2_jit_simd_inc.h
@@ -44,7 +44,8 @@ POSSIBILITY OF SUCH DAMAGE.
 #if ((defined SLJIT_CONFIG_X86 && SLJIT_CONFIG_X86) \
      || (defined SLJIT_CONFIG_ARM_64 && SLJIT_CONFIG_ARM_64) \
      || (defined SLJIT_CONFIG_S390X && SLJIT_CONFIG_S390X) \
-     || (defined SLJIT_CONFIG_LOONGARCH_64 && SLJIT_CONFIG_LOONGARCH_64))
+     || (defined SLJIT_CONFIG_LOONGARCH_64 && SLJIT_CONFIG_LOONGARCH_64) \
+     || (defined SLJIT_CONFIG_ALPHA && SLJIT_CONFIG_ALPHA))
 
 typedef enum {
   vector_compare_match1,
@@ -71,7 +72,20 @@ return 3;
 #error "Unsupported unit width"
 #endif
 }
-#else /* !SLJIT_CONFIG_X86 */
+#elif (defined SLJIT_CONFIG_ALPHA && SLJIT_CONFIG_ALPHA)
+static SLJIT_INLINE sljit_s32 max_fast_forward_char_pair_offset(void)
+{
+#if PCRE2_CODE_UNIT_WIDTH == 8
+return 7;
+#elif PCRE2_CODE_UNIT_WIDTH == 16
+return 3;
+#elif PCRE2_CODE_UNIT_WIDTH == 32
+return 1;
+#else
+#error "Unsupported unit width"
+#endif
+}
+#else /* !SLJIT_CONFIG_X86, !SLJIT_CONFIG_ALPHA */
 static SLJIT_INLINE sljit_s32 max_fast_forward_char_pair_offset(void)
 {
 #if PCRE2_CODE_UNIT_WIDTH == 8
@@ -101,7 +115,7 @@ return CMP(SLJIT_NOT_EQUAL, reg, 0, SLJIT_IMM, 0xdc00);
 }
 #endif
 
-#endif /* SLJIT_CONFIG_X86 || SLJIT_CONFIG_ARM_64 || SLJIT_CONFIG_S390X || SLJIT_CONFIG_LOONGARCH_64 */
+#endif /* SLJIT_CONFIG_X86 || SLJIT_CONFIG_ARM_64 || SLJIT_CONFIG_S390X || SLJIT_CONFIG_LOONGARCH_64 || SLJIT_CONFIG_ALPHA */
 
 #if (defined SLJIT_CONFIG_X86 && SLJIT_CONFIG_X86)
 
@@ -2503,4 +2517,497 @@ if (common->match_end_ptr != 0)
 
 #endif /* SLJIT_CONFIG_LOONGARCH_64 */
 
+#if (defined SLJIT_CONFIG_ALPHA && SLJIT_CONFIG_ALPHA)
+
+/* Alpha CMPBGE-based pseudo-SIMD: 8-byte-at-a-time character scanning in GPRs.
+   Works on all Alpha CPUs; no CIX extension required. */
+
+/* Replicate a code unit to fill all positions in a 64-bit register.
+   Computed at JIT compile time and emitted as an immediate load. */
+static SLJIT_INLINE sljit_sw replicate_char_alpha(PCRE2_UCHAR c)
+{
+sljit_uw r = c;
+#if PCRE2_CODE_UNIT_WIDTH == 8
+r |= r << 8;
+r |= r << 16;
+r |= r << 32;
+#elif PCRE2_CODE_UNIT_WIDTH == 16
+r |= r << 16;
+r |= r << 32;
+#else /* 32 */
+r |= r << 32;
+#endif
+return (sljit_sw)r;
+}
+
+/* Emit CMPBGE Ra, Rb, Rc: for each byte i, bit i of Rc is set if
+   byte i of Ra >= byte i of Rb. Uses hardware register indices. */
+static SLJIT_INLINE void emit_alpha_cmpbge(struct sljit_compiler *compiler,
+  sljit_s32 ra_ind, sljit_s32 rb_ind, sljit_s32 rc_ind)
+{
+sljit_ins ins = CMPBGE | ((sljit_ins)ra_ind << 21) | ((sljit_ins)rb_ind << 16) | (sljit_ins)rc_ind;
+sljit_emit_op_custom(compiler, &ins, sizeof(ins));
+}
+
+/* Compare helper: produces CMPBGE byte-match mask in dst (a GPR).
+   For match1:  mask = cmpbge(0, data ^ cmp1)
+   For match1i: mask = cmpbge(0, (data | cmp2) ^ cmp1)
+   For match2:  mask = cmpbge(0, data ^ cmp1) | cmpbge(0, data ^ cmp2)
+
+   data may alias dst for match1/match1i.
+   For match2, data must NOT alias tmp (tmp is clobbered). */
+static void fast_forward_char_pair_alpha_compare(struct sljit_compiler *compiler,
+  vector_compare_type compare_type, sljit_s32 dst, sljit_s32 data,
+  sljit_s32 cmp1, sljit_s32 cmp2, sljit_s32 tmp)
+{
+sljit_s32 dst_ind = sljit_get_register_index(SLJIT_GP_REGISTER, dst);
+sljit_s32 tmp_ind;
+
+if (compare_type != vector_compare_match2)
+  {
+  if (compare_type == vector_compare_match1i)
+    {
+    OP2(SLJIT_OR, dst, 0, data, 0, cmp2, 0);
+    OP2(SLJIT_XOR, dst, 0, dst, 0, cmp1, 0);
+    }
+  else
+    OP2(SLJIT_XOR, dst, 0, data, 0, cmp1, 0);
+
+  /* CMPBGE $31, dst, dst — bits set where XOR byte is zero (match). */
+  emit_alpha_cmpbge(compiler, 31, dst_ind, dst_ind);
+  return;
+  }
+
+/* match2: compare against both chars, OR the masks. */
+tmp_ind = sljit_get_register_index(SLJIT_GP_REGISTER, tmp);
+
+OP2(SLJIT_XOR, tmp, 0, data, 0, cmp2, 0);
+emit_alpha_cmpbge(compiler, 31, tmp_ind, tmp_ind);
+
+OP2(SLJIT_XOR, dst, 0, data, 0, cmp1, 0);
+emit_alpha_cmpbge(compiler, 31, dst_ind, dst_ind);
+
+OP2(SLJIT_OR, dst, 0, dst, 0, tmp, 0);
+}
+
+#if PCRE2_CODE_UNIT_WIDTH != 8
+/* For 16/32-bit code units, CMPBGE produces per-byte bits but we need
+   per-code-unit matching. Compact the mask so only positions where ALL
+   bytes of a code unit matched remain set. */
+static SLJIT_INLINE void compact_cmpbge_mask(struct sljit_compiler *compiler,
+  sljit_s32 mask, sljit_s32 tmp)
+{
+#if PCRE2_CODE_UNIT_WIDTH == 16
+OP2(SLJIT_LSHR, tmp, 0, mask, 0, SLJIT_IMM, 1);
+OP2(SLJIT_AND, mask, 0, mask, 0, tmp, 0);
+OP2(SLJIT_AND, mask, 0, mask, 0, SLJIT_IMM, 0x55);
+#else /* 32 */
+OP2(SLJIT_LSHR, tmp, 0, mask, 0, SLJIT_IMM, 1);
+OP2(SLJIT_AND, mask, 0, mask, 0, tmp, 0);
+OP2(SLJIT_LSHR, tmp, 0, mask, 0, SLJIT_IMM, 2);
+OP2(SLJIT_AND, mask, 0, mask, 0, tmp, 0);
+OP2(SLJIT_AND, mask, 0, mask, 0, SLJIT_IMM, 0x11);
+#endif
+}
+#endif
+
+/* Find the position of the lowest set bit in an 8-bit CMPBGE mask held in
+   TMP1; replace TMP1 with that bit index (0-7). TMP2 and RETURN_ADDR are
+   clobbered. Uses base-ISA NEGQ+AND to isolate the bit, then a binary search
+   via three AND+SELECT pairs — identical to glibc's Alpha strchr approach. */
+static SLJIT_INLINE void emit_alpha_ctz8(struct sljit_compiler *compiler)
+{
+OP2(SLJIT_SUB, TMP2, 0, SLJIT_IMM, 0, TMP1, 0);       /* TMP2 = -TMP1          */
+OP2(SLJIT_AND, TMP1, 0, TMP1, 0, TMP2, 0);              /* TMP1 = lowest set bit */
+
+OP1(SLJIT_MOV, TMP2, 0, SLJIT_IMM, 0);
+OP2U(SLJIT_AND | SLJIT_SET_Z, TMP1, 0, SLJIT_IMM, 0xf0);
+SELECT(SLJIT_NOT_ZERO, TMP2, SLJIT_IMM, 4, TMP2);       /* TMP2 = 4 or 0         */
+
+OP1(SLJIT_MOV, RETURN_ADDR, 0, SLJIT_IMM, 0);
+OP2U(SLJIT_AND | SLJIT_SET_Z, TMP1, 0, SLJIT_IMM, 0xcc);
+SELECT(SLJIT_NOT_ZERO, RETURN_ADDR, SLJIT_IMM, 2, RETURN_ADDR);  /* RA = 2 or 0  */
+OP2(SLJIT_ADD, TMP2, 0, TMP2, 0, RETURN_ADDR, 0);
+
+OP1(SLJIT_MOV, RETURN_ADDR, 0, SLJIT_IMM, 0);
+OP2U(SLJIT_AND | SLJIT_SET_Z, TMP1, 0, SLJIT_IMM, 0xaa);
+SELECT(SLJIT_NOT_ZERO, RETURN_ADDR, SLJIT_IMM, 1, RETURN_ADDR);  /* RA = 1 or 0  */
+OP2(SLJIT_ADD, TMP1, 0, TMP2, 0, RETURN_ADDR, 0);
+}
+
+#define JIT_HAS_FAST_FORWARD_CHAR_SIMD 1
+
+static void fast_forward_char_simd(compiler_common *common, PCRE2_UCHAR char1, PCRE2_UCHAR char2, sljit_s32 offset)
+{
+DEFINE_COMPILER;
+struct sljit_label *start;
+#if defined SUPPORT_UNICODE && PCRE2_CODE_UNIT_WIDTH != 32
+struct sljit_label *restart;
+#endif
+struct sljit_jump *quit;
+struct sljit_jump *partial_quit[2];
+vector_compare_type compare_type = vector_compare_match1;
+sljit_u32 bit = 0;
+
+SLJIT_UNUSED_ARG(offset);
+
+if (char1 != char2)
+  {
+  bit = char1 ^ char2;
+  compare_type = vector_compare_match1i;
+
+  if (!is_powerof2(bit))
+    {
+    bit = 0;
+    compare_type = vector_compare_match2;
+    }
+  }
+
+partial_quit[0] = CMP(SLJIT_GREATER_EQUAL, STR_PTR, 0, STR_END, 0);
+if (common->mode == PCRE2_JIT_COMPLETE)
+  add_jump(compiler, &common->failed_match, partial_quit[0]);
+
+/* Load replicated character constants into dedicated registers. */
+OP1(SLJIT_MOV, SLJIT_R5, 0, SLJIT_IMM, replicate_char_alpha(char1 | bit));
+
+if (char1 != char2)
+  OP1(SLJIT_MOV, SLJIT_R6, 0, SLJIT_IMM, replicate_char_alpha(bit != 0 ? bit : char2));
+
+OP1(SLJIT_MOV, TMP2, 0, STR_PTR, 0);
+
+#if defined SUPPORT_UNICODE && PCRE2_CODE_UNIT_WIDTH != 32
+restart = LABEL();
+#endif
+
+/* Align STR_PTR down to 8-byte boundary; save misalignment in TMP2. */
+OP2(SLJIT_AND, TMP2, 0, TMP2, 0, SLJIT_IMM, 0x7);
+OP2(SLJIT_SUB, STR_PTR, 0, STR_PTR, 0, TMP2, 0);
+
+/* Load 8 aligned bytes, compare, produce CMPBGE mask. */
+OP1(SLJIT_MOV, TMP1, 0, SLJIT_MEM1(STR_PTR), 0);
+fast_forward_char_pair_alpha_compare(compiler, compare_type, TMP1, TMP1, SLJIT_R5, SLJIT_R6, RETURN_ADDR);
+
+#if PCRE2_CODE_UNIT_WIDTH != 8
+compact_cmpbge_mask(compiler, TMP1, RETURN_ADDR);
+#endif
+
+/* Restore STR_PTR, shift mask to ignore bytes before original position. */
+OP2(SLJIT_ADD, STR_PTR, 0, STR_PTR, 0, TMP2, 0);
+OP2(SLJIT_LSHR, TMP1, 0, TMP1, 0, TMP2, 0);
+
+quit = CMP(SLJIT_NOT_ZERO, TMP1, 0, SLJIT_IMM, 0);
+
+OP2(SLJIT_SUB, STR_PTR, 0, STR_PTR, 0, TMP2, 0);
+
+/* Main loop (aligned). */
+start = LABEL();
+
+OP2(SLJIT_ADD, STR_PTR, 0, STR_PTR, 0, SLJIT_IMM, 8);
+
+partial_quit[1] = CMP(SLJIT_GREATER_EQUAL, STR_PTR, 0, STR_END, 0);
+if (common->mode == PCRE2_JIT_COMPLETE)
+  add_jump(compiler, &common->failed_match, partial_quit[1]);
+
+OP1(SLJIT_MOV, TMP1, 0, SLJIT_MEM1(STR_PTR), 0);
+fast_forward_char_pair_alpha_compare(compiler, compare_type, TMP1, TMP1, SLJIT_R5, SLJIT_R6, RETURN_ADDR);
+
+#if PCRE2_CODE_UNIT_WIDTH != 8
+compact_cmpbge_mask(compiler, TMP1, RETURN_ADDR);
+#endif
+
+CMPTO(SLJIT_ZERO, TMP1, 0, SLJIT_IMM, 0, start);
+
+JUMPHERE(quit);
+
+/* Find position of first match. */
+emit_alpha_ctz8(compiler);
+
+OP2(SLJIT_ADD, STR_PTR, 0, STR_PTR, 0, TMP1, 0);
+
+if (common->mode != PCRE2_JIT_COMPLETE)
+  {
+  JUMPHERE(partial_quit[0]);
+  JUMPHERE(partial_quit[1]);
+  OP2U(SLJIT_SUB | SLJIT_SET_GREATER, STR_PTR, 0, STR_END, 0);
+  SELECT(SLJIT_GREATER, STR_PTR, STR_END, 0, STR_PTR);
+  }
+else
+  add_jump(compiler, &common->failed_match, CMP(SLJIT_GREATER_EQUAL, STR_PTR, 0, STR_END, 0));
+
+#if defined SUPPORT_UNICODE && PCRE2_CODE_UNIT_WIDTH != 32
+if (common->utf && offset > 0)
+  {
+  SLJIT_ASSERT(common->mode == PCRE2_JIT_COMPLETE);
+
+  OP1(MOV_UCHAR, TMP1, 0, SLJIT_MEM1(STR_PTR), IN_UCHARS(-offset));
+
+  quit = jump_if_utf_char_start(compiler, TMP1);
+
+  OP2(SLJIT_ADD, STR_PTR, 0, STR_PTR, 0, SLJIT_IMM, IN_UCHARS(1));
+  add_jump(compiler, &common->failed_match, CMP(SLJIT_GREATER_EQUAL, STR_PTR, 0, STR_END, 0));
+  OP1(SLJIT_MOV, TMP2, 0, STR_PTR, 0);
+  JUMPTO(SLJIT_JUMP, restart);
+
+  JUMPHERE(quit);
+  }
+#endif
+}
+
+#define JIT_HAS_FAST_REQUESTED_CHAR_SIMD 1
+
+static jump_list *fast_requested_char_simd(compiler_common *common, PCRE2_UCHAR char1, PCRE2_UCHAR char2)
+{
+DEFINE_COMPILER;
+struct sljit_label *start;
+struct sljit_jump *quit;
+jump_list *not_found = NULL;
+vector_compare_type compare_type = vector_compare_match1;
+sljit_u32 bit = 0;
+
+if (char1 != char2)
+  {
+  bit = char1 ^ char2;
+  compare_type = vector_compare_match1i;
+
+  if (!is_powerof2(bit))
+    {
+    bit = 0;
+    compare_type = vector_compare_match2;
+    }
+  }
+
+add_jump(compiler, ¬_found, CMP(SLJIT_GREATER_EQUAL, TMP1, 0, STR_END, 0));
+OP1(SLJIT_MOV, TMP2, 0, TMP1, 0);
+OP1(SLJIT_MOV, TMP3, 0, STR_PTR, 0);
+
+/* Load replicated character constants. */
+OP1(SLJIT_MOV, SLJIT_R5, 0, SLJIT_IMM, replicate_char_alpha(char1 | bit));
+
+if (char1 != char2)
+  OP1(SLJIT_MOV, SLJIT_R6, 0, SLJIT_IMM, replicate_char_alpha(bit != 0 ? bit : char2));
+
+OP1(SLJIT_MOV, STR_PTR, 0, TMP2, 0);
+OP2(SLJIT_AND, TMP2, 0, TMP2, 0, SLJIT_IMM, 0x7);
+OP2(SLJIT_SUB, STR_PTR, 0, STR_PTR, 0, TMP2, 0);
+
+OP1(SLJIT_MOV, TMP1, 0, SLJIT_MEM1(STR_PTR), 0);
+fast_forward_char_pair_alpha_compare(compiler, compare_type, TMP1, TMP1, SLJIT_R5, SLJIT_R6, RETURN_ADDR);
+
+#if PCRE2_CODE_UNIT_WIDTH != 8
+compact_cmpbge_mask(compiler, TMP1, RETURN_ADDR);
+#endif
+
+OP2(SLJIT_ADD, STR_PTR, 0, STR_PTR, 0, TMP2, 0);
+OP2(SLJIT_LSHR, TMP1, 0, TMP1, 0, TMP2, 0);
+
+quit = CMP(SLJIT_NOT_ZERO, TMP1, 0, SLJIT_IMM, 0);
+
+OP2(SLJIT_SUB, STR_PTR, 0, STR_PTR, 0, TMP2, 0);
+
+/* Main loop. */
+start = LABEL();
+
+OP2(SLJIT_ADD, STR_PTR, 0, STR_PTR, 0, SLJIT_IMM, 8);
+
+add_jump(compiler, ¬_found, CMP(SLJIT_GREATER_EQUAL, STR_PTR, 0, STR_END, 0));
+
+OP1(SLJIT_MOV, TMP1, 0, SLJIT_MEM1(STR_PTR), 0);
+fast_forward_char_pair_alpha_compare(compiler, compare_type, TMP1, TMP1, SLJIT_R5, SLJIT_R6, RETURN_ADDR);
+
+#if PCRE2_CODE_UNIT_WIDTH != 8
+compact_cmpbge_mask(compiler, TMP1, RETURN_ADDR);
+#endif
+
+CMPTO(SLJIT_ZERO, TMP1, 0, SLJIT_IMM, 0, start);
+
+JUMPHERE(quit);
+
+emit_alpha_ctz8(compiler);
+
+OP2(SLJIT_ADD, TMP1, 0, TMP1, 0, STR_PTR, 0);
+add_jump(compiler, ¬_found, CMP(SLJIT_GREATER_EQUAL, TMP1, 0, STR_END, 0));
+
+OP1(SLJIT_MOV, STR_PTR, 0, TMP3, 0);
+return not_found;
+}
+
+#define JIT_HAS_FAST_FORWARD_CHAR_PAIR_SIMD 1
+
+static void fast_forward_char_pair_simd(compiler_common *common, sljit_s32 offs1,
+  PCRE2_UCHAR char1a, PCRE2_UCHAR char1b, sljit_s32 offs2, PCRE2_UCHAR char2a, PCRE2_UCHAR char2b)
+{
+DEFINE_COMPILER;
+vector_compare_type compare1_type = vector_compare_match1;
+vector_compare_type compare2_type = vector_compare_match1;
+sljit_u32 bit1 = 0;
+sljit_u32 bit2 = 0;
+sljit_u32 diff = IN_UCHARS(offs1 - offs2);
+struct sljit_label *start;
+#if defined SUPPORT_UNICODE && PCRE2_CODE_UNIT_WIDTH != 32
+struct sljit_label *restart;
+#endif
+struct sljit_jump *jump[2];
+
+SLJIT_ASSERT(common->mode == PCRE2_JIT_COMPLETE && offs1 > offs2);
+SLJIT_ASSERT(diff <= (unsigned)IN_UCHARS(max_fast_forward_char_pair_offset()));
+
+/* Initialize. */
+if (common->match_end_ptr != 0)
+  {
+  OP1(SLJIT_MOV, TMP1, 0, SLJIT_MEM1(SLJIT_SP), common->match_end_ptr);
+  OP2(SLJIT_ADD, TMP1, 0, TMP1, 0, SLJIT_IMM, IN_UCHARS(offs1 + 1));
+  OP1(SLJIT_MOV, TMP3, 0, STR_END, 0);
+
+  OP2U(SLJIT_SUB | SLJIT_SET_LESS, TMP1, 0, STR_END, 0);
+  SELECT(SLJIT_LESS, STR_END, TMP1, 0, STR_END);
+  }
+
+OP2(SLJIT_ADD, STR_PTR, 0, STR_PTR, 0, SLJIT_IMM, IN_UCHARS(offs1));
+add_jump(compiler, &common->failed_match, CMP(SLJIT_GREATER_EQUAL, STR_PTR, 0, STR_END, 0));
+
+/* Set up replicated character constants. */
+if (char1a == char1b)
+  OP1(SLJIT_MOV, SLJIT_R5, 0, SLJIT_IMM, replicate_char_alpha(char1a));
+else
+  {
+  bit1 = char1a ^ char1b;
+  if (is_powerof2(bit1))
+    {
+    compare1_type = vector_compare_match1i;
+    OP1(SLJIT_MOV, SLJIT_R5, 0, SLJIT_IMM, replicate_char_alpha(char1a | bit1));
+    OP1(SLJIT_MOV, SLJIT_R6, 0, SLJIT_IMM, replicate_char_alpha(bit1));
+    }
+  else
+    {
+    compare1_type = vector_compare_match2;
+    bit1 = 0;
+    OP1(SLJIT_MOV, SLJIT_R5, 0, SLJIT_IMM, replicate_char_alpha(char1a));
+    OP1(SLJIT_MOV, SLJIT_R6, 0, SLJIT_IMM, replicate_char_alpha(char1b));
+    }
+  }
+
+if (char2a == char2b)
+  OP1(SLJIT_MOV, SLJIT_R7, 0, SLJIT_IMM, replicate_char_alpha(char2a));
+else
+  {
+  bit2 = char2a ^ char2b;
+  if (is_powerof2(bit2))
+    {
+    compare2_type = vector_compare_match1i;
+    OP1(SLJIT_MOV, SLJIT_R7, 0, SLJIT_IMM, replicate_char_alpha(char2a | bit2));
+    OP1(SLJIT_MOV, SLJIT_R8, 0, SLJIT_IMM, replicate_char_alpha(bit2));
+    }
+  else
+    {
+    compare2_type = vector_compare_match2;
+    bit2 = 0;
+    OP1(SLJIT_MOV, SLJIT_R7, 0, SLJIT_IMM, replicate_char_alpha(char2a));
+    OP1(SLJIT_MOV, SLJIT_R8, 0, SLJIT_IMM, replicate_char_alpha(char2b));
+    }
+  }
+
+#if defined SUPPORT_UNICODE && PCRE2_CODE_UNIT_WIDTH != 32
+restart = LABEL();
+#endif
+
+OP2(SLJIT_SUB, TMP1, 0, STR_PTR, 0, SLJIT_IMM, diff);
+OP1(SLJIT_MOV, TMP2, 0, STR_PTR, 0);
+OP2(SLJIT_AND, TMP2, 0, TMP2, 0, SLJIT_IMM, 0x7);
+OP2(SLJIT_SUB, STR_PTR, 0, STR_PTR, 0, TMP2, 0);
+
+/* Load data1 (for char1 comparison). */
+OP1(SLJIT_MOV, SLJIT_R9, 0, SLJIT_MEM1(STR_PTR), 0);
+
+/* Construct data2 (for char2 comparison). If the second load address
+   falls before our aligned load, synthesize data2 by shifting data1
+   left, filling the low bytes with zeros (they will be masked off by
+   the alignment shift and never produce false matches). */
+jump[0] = CMP(SLJIT_GREATER_EQUAL, TMP1, 0, STR_PTR, 0);
+
+OP1(SLJIT_MOV, TMP1, 0, SLJIT_MEM1(STR_PTR), -8);
+OP2(SLJIT_LSHR, TMP1, 0, TMP1, 0, SLJIT_IMM, (8 - diff) * 8);
+OP2(SLJIT_SHL, RETURN_ADDR, 0, SLJIT_R9, 0, SLJIT_IMM, diff * 8);
+OP2(SLJIT_OR, TMP1, 0, TMP1, 0, RETURN_ADDR, 0);
+jump[1] = JUMP(SLJIT_JUMP);
+
+JUMPHERE(jump[0]);
+
+OP2(SLJIT_SHL, TMP1, 0, SLJIT_R9, 0, SLJIT_IMM, diff * 8);
+
+JUMPHERE(jump[1]);
+
+/* Compare both data words. */
+fast_forward_char_pair_alpha_compare(compiler, compare2_type, TMP1, TMP1, SLJIT_R7, SLJIT_R8, RETURN_ADDR);
+fast_forward_char_pair_alpha_compare(compiler, compare1_type, SLJIT_R9, SLJIT_R9, SLJIT_R5, SLJIT_R6, RETURN_ADDR);
+
+OP2(SLJIT_AND, TMP1, 0, TMP1, 0, SLJIT_R9, 0);
+
+#if PCRE2_CODE_UNIT_WIDTH != 8
+compact_cmpbge_mask(compiler, TMP1, RETURN_ADDR);
+#endif
+
+/* Ignore matches before the original STR_PTR. */
+OP2(SLJIT_ADD, STR_PTR, 0, STR_PTR, 0, TMP2, 0);
+OP2(SLJIT_LSHR, TMP1, 0, TMP1, 0, TMP2, 0);
+
+jump[0] = CMP(SLJIT_NOT_ZERO, TMP1, 0, SLJIT_IMM, 0);
+
+OP2(SLJIT_SUB, STR_PTR, 0, STR_PTR, 0, TMP2, 0);
+
+/* Main loop. */
+start = LABEL();
+
+OP2(SLJIT_ADD, STR_PTR, 0, STR_PTR, 0, SLJIT_IMM, 8);
+add_jump(compiler, &common->failed_match, CMP(SLJIT_GREATER_EQUAL, STR_PTR, 0, STR_END, 0));
+
+/* Load data1 and construct unaligned data2. */
+OP1(SLJIT_MOV, SLJIT_R9, 0, SLJIT_MEM1(STR_PTR), 0);
+OP1(SLJIT_MOV, TMP1, 0, SLJIT_MEM1(STR_PTR), -8);
+OP2(SLJIT_LSHR, TMP1, 0, TMP1, 0, SLJIT_IMM, (8 - diff) * 8);
+OP2(SLJIT_SHL, RETURN_ADDR, 0, SLJIT_R9, 0, SLJIT_IMM, diff * 8);
+OP2(SLJIT_OR, TMP1, 0, TMP1, 0, RETURN_ADDR, 0);
+
+fast_forward_char_pair_alpha_compare(compiler, compare1_type, SLJIT_R9, SLJIT_R9, SLJIT_R5, SLJIT_R6, RETURN_ADDR);
+fast_forward_char_pair_alpha_compare(compiler, compare2_type, TMP1, TMP1, SLJIT_R7, SLJIT_R8, RETURN_ADDR);
+
+OP2(SLJIT_AND, TMP1, 0, TMP1, 0, SLJIT_R9, 0);
+
+#if PCRE2_CODE_UNIT_WIDTH != 8
+compact_cmpbge_mask(compiler, TMP1, RETURN_ADDR);
+#endif
+
+CMPTO(SLJIT_ZERO, TMP1, 0, SLJIT_IMM, 0, start);
+
+JUMPHERE(jump[0]);
+
+emit_alpha_ctz8(compiler);
+
+OP2(SLJIT_ADD, STR_PTR, 0, STR_PTR, 0, TMP1, 0);
+
+add_jump(compiler, &common->failed_match, CMP(SLJIT_GREATER_EQUAL, STR_PTR, 0, STR_END, 0));
+
+#if defined SUPPORT_UNICODE && PCRE2_CODE_UNIT_WIDTH != 32
+if (common->utf)
+  {
+  OP1(MOV_UCHAR, TMP1, 0, SLJIT_MEM1(STR_PTR), IN_UCHARS(-offs1));
+
+  jump[0] = jump_if_utf_char_start(compiler, TMP1);
+
+  OP2(SLJIT_ADD, STR_PTR, 0, STR_PTR, 0, SLJIT_IMM, IN_UCHARS(1));
+  CMPTO(SLJIT_LESS, STR_PTR, 0, STR_END, 0, restart);
+
+  add_jump(compiler, &common->failed_match, JUMP(SLJIT_JUMP));
+
+  JUMPHERE(jump[0]);
+  }
+#endif
+
+OP2(SLJIT_SUB, STR_PTR, 0, STR_PTR, 0, SLJIT_IMM, IN_UCHARS(offs1));
+
+if (common->match_end_ptr != 0)
+  OP1(SLJIT_MOV, STR_END, 0, TMP3, 0);
+}
+
+#endif /* SLJIT_CONFIG_ALPHA */
+
 #endif /* !SUPPORT_VALGRIND */

From 52f31d58423e7d89cd5d52207b074a32ac75fa18 Mon Sep 17 00:00:00 2001
From: Matt Turner 
Date: Tue, 14 Jul 2026 12:52:47 -0400
Subject: [PATCH 4/7] alpha: add guarded prefetch hints to CMPBGE SIMD scanning
 loops

Emit PREFETCH_L1 (LDL R31) 192 bytes ahead of the current position
in all three fast-forward scanning loops: fast_forward_char_simd,
fast_requested_char_simd, and fast_forward_char_pair_simd.

The 192-byte (3 cache line) prefetch distance matches what glibc's
Alpha memchr uses. Without this, the JIT's CMPBGE loop loses to the
interpreter's memchr() call on full-scan no-match patterns because
memchr has prefetching and software pipelining.

The prefetch is guarded by a comparison against STR_END - 192 to
avoid reading past the end of the buffer on short haystacks, where
unconditional prefetching caused a ~15% regression.
---
 src/pcre2_jit_simd_inc.h | 24 ++++++++++++++++++++++++
 1 file changed, 24 insertions(+)

diff --git a/src/pcre2_jit_simd_inc.h b/src/pcre2_jit_simd_inc.h
index 7b4036d90..fd271ca4e 100644
--- a/src/pcre2_jit_simd_inc.h
+++ b/src/pcre2_jit_simd_inc.h
@@ -2646,6 +2646,7 @@ struct sljit_label *restart;
 #endif
 struct sljit_jump *quit;
 struct sljit_jump *partial_quit[2];
+struct sljit_jump *no_prefetch;
 vector_compare_type compare_type = vector_compare_match1;
 sljit_u32 bit = 0;
 
@@ -2699,6 +2700,9 @@ quit = CMP(SLJIT_NOT_ZERO, TMP1, 0, SLJIT_IMM, 0);
 
 OP2(SLJIT_SUB, STR_PTR, 0, STR_PTR, 0, TMP2, 0);
 
+/* Prefetch threshold: only prefetch when more than 192 bytes remain. */
+OP2(SLJIT_SUB, TMP2, 0, STR_END, 0, SLJIT_IMM, 192);
+
 /* Main loop (aligned). */
 start = LABEL();
 
@@ -2708,6 +2712,10 @@ partial_quit[1] = CMP(SLJIT_GREATER_EQUAL, STR_PTR, 0, STR_END, 0);
 if (common->mode == PCRE2_JIT_COMPLETE)
   add_jump(compiler, &common->failed_match, partial_quit[1]);
 
+no_prefetch = CMP(SLJIT_GREATER_EQUAL, STR_PTR, 0, TMP2, 0);
+OP_SRC(SLJIT_PREFETCH_L1, SLJIT_MEM1(STR_PTR), 192);
+JUMPHERE(no_prefetch);
+
 OP1(SLJIT_MOV, TMP1, 0, SLJIT_MEM1(STR_PTR), 0);
 fast_forward_char_pair_alpha_compare(compiler, compare_type, TMP1, TMP1, SLJIT_R5, SLJIT_R6, RETURN_ADDR);
 
@@ -2760,6 +2768,7 @@ static jump_list *fast_requested_char_simd(compiler_common *common, PCRE2_UCHAR
 DEFINE_COMPILER;
 struct sljit_label *start;
 struct sljit_jump *quit;
+struct sljit_jump *no_prefetch;
 jump_list *not_found = NULL;
 vector_compare_type compare_type = vector_compare_match1;
 sljit_u32 bit = 0;
@@ -2804,6 +2813,9 @@ quit = CMP(SLJIT_NOT_ZERO, TMP1, 0, SLJIT_IMM, 0);
 
 OP2(SLJIT_SUB, STR_PTR, 0, STR_PTR, 0, TMP2, 0);
 
+/* Prefetch threshold: only prefetch when more than 192 bytes remain. */
+OP2(SLJIT_SUB, TMP2, 0, STR_END, 0, SLJIT_IMM, 192);
+
 /* Main loop. */
 start = LABEL();
 
@@ -2811,6 +2823,10 @@ OP2(SLJIT_ADD, STR_PTR, 0, STR_PTR, 0, SLJIT_IMM, 8);
 
 add_jump(compiler, ¬_found, CMP(SLJIT_GREATER_EQUAL, STR_PTR, 0, STR_END, 0));
 
+no_prefetch = CMP(SLJIT_GREATER_EQUAL, STR_PTR, 0, TMP2, 0);
+OP_SRC(SLJIT_PREFETCH_L1, SLJIT_MEM1(STR_PTR), 192);
+JUMPHERE(no_prefetch);
+
 OP1(SLJIT_MOV, TMP1, 0, SLJIT_MEM1(STR_PTR), 0);
 fast_forward_char_pair_alpha_compare(compiler, compare_type, TMP1, TMP1, SLJIT_R5, SLJIT_R6, RETURN_ADDR);
 
@@ -2847,6 +2863,7 @@ struct sljit_label *start;
 struct sljit_label *restart;
 #endif
 struct sljit_jump *jump[2];
+struct sljit_jump *no_prefetch;
 
 SLJIT_ASSERT(common->mode == PCRE2_JIT_COMPLETE && offs1 > offs2);
 SLJIT_ASSERT(diff <= (unsigned)IN_UCHARS(max_fast_forward_char_pair_offset()));
@@ -2954,12 +2971,19 @@ jump[0] = CMP(SLJIT_NOT_ZERO, TMP1, 0, SLJIT_IMM, 0);
 
 OP2(SLJIT_SUB, STR_PTR, 0, STR_PTR, 0, TMP2, 0);
 
+/* Prefetch threshold: only prefetch when more than 192 bytes remain. */
+OP2(SLJIT_SUB, TMP2, 0, STR_END, 0, SLJIT_IMM, 192);
+
 /* Main loop. */
 start = LABEL();
 
 OP2(SLJIT_ADD, STR_PTR, 0, STR_PTR, 0, SLJIT_IMM, 8);
 add_jump(compiler, &common->failed_match, CMP(SLJIT_GREATER_EQUAL, STR_PTR, 0, STR_END, 0));
 
+no_prefetch = CMP(SLJIT_GREATER_EQUAL, STR_PTR, 0, TMP2, 0);
+OP_SRC(SLJIT_PREFETCH_L1, SLJIT_MEM1(STR_PTR), 192);
+JUMPHERE(no_prefetch);
+
 /* Load data1 and construct unaligned data2. */
 OP1(SLJIT_MOV, SLJIT_R9, 0, SLJIT_MEM1(STR_PTR), 0);
 OP1(SLJIT_MOV, TMP1, 0, SLJIT_MEM1(STR_PTR), -8);

From bd4986fcc6dfacae936a5a9f8c4490b1c9d7cfa9 Mon Sep 17 00:00:00 2001
From: Matt Turner 
Date: Wed, 15 Jul 2026 09:46:45 -0400
Subject: [PATCH 5/7] alpha: use memchr() for exact single-character JIT
 scanning

For 8-bit mode with an exact single-character match, call glibc's
memchr() instead of the inline CMPBGE scanning loop. glibc's Alpha
memchr uses CMPBGE with 3-cache-line-ahead prefetching, cacheline
alignment, and software pipelining (overlapping the next load with
the current compare to hide L1 latency).

The CMPBGE loop is retained for case-insensitive (match1i) and
two-character (match2) scanning where memchr does not apply.

Both fast_forward_char_simd and fast_requested_char_simd gain the
memchr path; fast_forward_char_pair_simd is unchanged since it
searches for two characters at different offsets simultaneously.
---
 src/pcre2_jit_simd_inc.h | 76 ++++++++++++++++++++++++++++++++++++++++
 1 file changed, 76 insertions(+)

diff --git a/src/pcre2_jit_simd_inc.h b/src/pcre2_jit_simd_inc.h
index fd271ca4e..1866891f6 100644
--- a/src/pcre2_jit_simd_inc.h
+++ b/src/pcre2_jit_simd_inc.h
@@ -2668,6 +2668,63 @@ partial_quit[0] = CMP(SLJIT_GREATER_EQUAL, STR_PTR, 0, STR_END, 0);
 if (common->mode == PCRE2_JIT_COMPLETE)
   add_jump(compiler, &common->failed_match, partial_quit[0]);
 
+#if PCRE2_CODE_UNIT_WIDTH == 8
+if (compare_type == vector_compare_match1)
+  {
+  /* Use memchr() for exact single-character scanning. glibc's Alpha memchr
+     uses CMPBGE with prefetching and software pipelining. */
+
+  /* The call clobbers every scratch register, so TMP3 must be preserved: the
+     callers keep the unclamped STR_END there while match_end_ptr is set. */
+  OP1(SLJIT_MOV, SLJIT_MEM1(SLJIT_SP), LOCAL0, TMP3, 0);
+
+#if defined SUPPORT_UNICODE
+  restart = LABEL();
+#endif
+
+  /* STR_PTR is SLJIT_R1, which is also the second argument register, so the
+     length and the start pointer must be read before it is overwritten. */
+  OP2(SLJIT_SUB, SLJIT_R2, 0, STR_END, 0, STR_PTR, 0);
+  OP1(SLJIT_MOV, SLJIT_R0, 0, STR_PTR, 0);
+  OP1(SLJIT_MOV, SLJIT_R1, 0, SLJIT_IMM, char1);
+  sljit_emit_icall(compiler, SLJIT_CALL, SLJIT_ARGS3(W, W, 32, W),
+    SLJIT_IMM, SLJIT_FUNC_ADDR(memchr));
+  OP1(SLJIT_MOV, TMP3, 0, SLJIT_MEM1(SLJIT_SP), LOCAL0);
+
+  if (common->mode == PCRE2_JIT_COMPLETE)
+    {
+    add_jump(compiler, &common->failed_match,
+      CMP(SLJIT_EQUAL, SLJIT_RETURN_REG, 0, SLJIT_IMM, 0));
+    OP1(SLJIT_MOV, STR_PTR, 0, SLJIT_RETURN_REG, 0);
+
+#if defined SUPPORT_UNICODE
+    if (common->utf && offset > 0)
+      {
+      OP1(MOV_UCHAR, TMP1, 0, SLJIT_MEM1(STR_PTR), IN_UCHARS(-offset));
+      quit = jump_if_utf_char_start(compiler, TMP1);
+      OP2(SLJIT_ADD, STR_PTR, 0, STR_PTR, 0, SLJIT_IMM, IN_UCHARS(1));
+      add_jump(compiler, &common->failed_match,
+        CMP(SLJIT_GREATER_EQUAL, STR_PTR, 0, STR_END, 0));
+      JUMPTO(SLJIT_JUMP, restart);
+      JUMPHERE(quit);
+      }
+#endif
+    }
+  else
+    {
+    /* Partial mode: found → STR_PTR = result, not found → STR_PTR = STR_END. */
+    quit = CMP(SLJIT_EQUAL, SLJIT_RETURN_REG, 0, SLJIT_IMM, 0);
+    OP1(SLJIT_MOV, STR_PTR, 0, SLJIT_RETURN_REG, 0);
+    partial_quit[1] = JUMP(SLJIT_JUMP);
+    JUMPHERE(quit);
+    JUMPHERE(partial_quit[0]);
+    OP1(SLJIT_MOV, STR_PTR, 0, STR_END, 0);
+    JUMPHERE(partial_quit[1]);
+    }
+  return;
+  }
+#endif
+
 /* Load replicated character constants into dedicated registers. */
 OP1(SLJIT_MOV, SLJIT_R5, 0, SLJIT_IMM, replicate_char_alpha(char1 | bit));
 
@@ -2786,6 +2843,25 @@ if (char1 != char2)
   }
 
 add_jump(compiler, ¬_found, CMP(SLJIT_GREATER_EQUAL, TMP1, 0, STR_END, 0));
+
+#if PCRE2_CODE_UNIT_WIDTH == 8
+if (compare_type == vector_compare_match1)
+  {
+  /* Use memchr() to check if the required character exists.
+     Save STR_PTR across the icall since it clobbers all scratch registers. */
+  OP1(SLJIT_MOV, SLJIT_MEM1(SLJIT_SP), LOCAL0, STR_PTR, 0);
+  OP1(SLJIT_MOV, SLJIT_R0, 0, TMP1, 0);
+  OP1(SLJIT_MOV, SLJIT_R1, 0, SLJIT_IMM, char1);
+  OP2(SLJIT_SUB, SLJIT_R2, 0, STR_END, 0, TMP1, 0);
+  sljit_emit_icall(compiler, SLJIT_CALL, SLJIT_ARGS3(W, W, 32, W),
+    SLJIT_IMM, SLJIT_FUNC_ADDR(memchr));
+  OP1(SLJIT_MOV, STR_PTR, 0, SLJIT_MEM1(SLJIT_SP), LOCAL0);
+  add_jump(compiler, ¬_found,
+    CMP(SLJIT_EQUAL, SLJIT_RETURN_REG, 0, SLJIT_IMM, 0));
+  return not_found;
+  }
+#endif
+
 OP1(SLJIT_MOV, TMP2, 0, TMP1, 0);
 OP1(SLJIT_MOV, TMP3, 0, STR_PTR, 0);
 

From 6e62316a77e2c26969f2bf52861245585d9476d4 Mon Sep 17 00:00:00 2001
From: Matt Turner 
Date: Sat, 8 Aug 2026 17:54:21 -0400
Subject: [PATCH 6/7] Test the remaining JIT backends under QEMU

We have hand-written code for every architecture SLJIT supports, but CI
only exercises a few of them. The existing "ptarmigan" job covers s390x,
ppc64le, armv7, aarch64 and riscv64, which leaves the 32-bit ARM, PowerPC,
MIPS and LoongArch backends untested. The ARM one is easy to miss:
Ubuntu's armhf compiler defaults to Thumb-2, so armv7 exercises
sljitNativeARM_T2_32.c and never sljitNativeARM_32.c.

Add a job which cross-compiles for each of them and runs the test suite
under qemu-user. This is a different mechanism to ptarmigan, which boots
a container of the target architecture: run-on-arch-action supports only
armv6, armv7, aarch64, riscv64, s390x and ppc64le, so none of these
targets can be added to it. Cross-compiling is also faster, since only
the test programs are emulated rather than the compiler as well.

RunTest already accepts a "-sim" prefix for exactly this purpose, so the
test scripts need no changes. Shared libraries are disabled so that the
test programs are real executables rather than libtool wrapper scripts,
which qemu-user cannot run.

Both MIPS release 6 and the earlier releases are covered, since
SLJIT_MIPS_REV selects different code for each.
---
 .github/workflows/dev.yml | 95 +++++++++++++++++++++++++++++++++++++++
 1 file changed, 95 insertions(+)

diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml
index ba9e2cf24..5d48b0b04 100644
--- a/.github/workflows/dev.yml
+++ b/.github/workflows/dev.yml
@@ -23,6 +23,7 @@ on:
         - chaffinch
         - fruitbat
         - ptarmigan
+        - coelacanth
         - zebrilus
         - bee
   push:
@@ -626,6 +627,100 @@ jobs:
             ninja
             ctest -j3 --output-on-failure && (cat ./Testing/Temporary/LastTest.log || true)
 
+  coelacanth:
+    # Tests with: cross-compilation and qemu-user, for the JIT backends that no
+    # other job covers. A coelacanth is a fish that was thought to be extinct.
+    name: QEMU ${{ matrix.config.name }}
+    runs-on: ubuntu-latest
+    container:
+      image: ${{ matrix.config.container }}
+    timeout-minutes: 20
+    strategy:
+      fail-fast: false
+      matrix:
+        config:
+          # "libc" is the Debian architecture name. It is needed because the
+          # cross compilers do not depend on the target's libc headers, and it
+          # cannot always be derived from the triple.
+          #
+          # Ubuntu armhf defaults to Thumb-2, so ARM mode needs asking for.
+          - { name: arm,         triple: arm-linux-gnueabihf,         qemu: arm,         libc: armhf,      container: "ubuntu:26.04", cflags: -marm }
+          - { name: arm-thumb2,  triple: arm-linux-gnueabihf,         qemu: arm,         libc: armhf,      container: "ubuntu:26.04", cflags: -mthumb }
+          - { name: loongarch64, triple: loongarch64-linux-gnu,       qemu: loongarch64, libc: loong64,    container: "ubuntu:26.04" }
+          # PowerPC big-endian; ptarmigan covers ppc64le, which is a different
+          # ABI to ppc64.
+          - { name: powerpc,     triple: powerpc-linux-gnu,           qemu: ppc,         libc: powerpc,    container: "ubuntu:26.04" }
+          - { name: powerpc64,   triple: powerpc64-linux-gnu,         qemu: ppc64,       libc: ppc64,      container: "ubuntu:26.04" }
+          # The MIPS cross compilers are no longer built for Ubuntu 26.04,
+          # hence the older container.
+          - { name: mips,        triple: mips-linux-gnu,              qemu: mips,        libc: mips,       container: "ubuntu:24.04" }
+          - { name: mipsel,      triple: mipsel-linux-gnu,            qemu: mipsel,      libc: mipsel,     container: "ubuntu:24.04" }
+          - { name: mips64,      triple: mips64-linux-gnuabi64,       qemu: mips64,      libc: mips64,     container: "ubuntu:24.04" }
+          - { name: mips64el,    triple: mips64el-linux-gnuabi64,     qemu: mips64el,    libc: mips64el,   container: "ubuntu:24.04" }
+          # MIPS release 6 is a different instruction set to the earlier
+          # releases, and SLJIT_MIPS_REV >= 6 selects different code. There is
+          # no separate qemu-user binary for it; the emulated CPU has to be
+          # named instead, because the defaults predate release 6.
+          - { name: mips32r6el,  triple: mipsisa32r6el-linux-gnu,     qemu: mipsel,      libc: mipsr6el,   container: "ubuntu:24.04", qemu_cpu: mips32r6-generic }
+          - { name: mips64r6el,  triple: mipsisa64r6el-linux-gnuabi64, qemu: mips64el,   libc: mips64r6el, container: "ubuntu:24.04", qemu_cpu: I6400 }
+    if: |
+      (github.event_name == 'workflow_dispatch' && (inputs.job_id == 'all' || inputs.job_id == 'coelacanth')) ||
+      github.event_name == 'push'
+    env:
+      # Where qemu-user looks for the target's shared libraries.
+      QEMU_LD_PREFIX: /usr/${{ matrix.config.triple }}
+    steps:
+      - name: Setup
+        run: |
+          apt-get -qq update
+          apt-get -qq install -y git autoconf automake libtool make \
+                                 gcc-14-${{ matrix.config.triple }} \
+                                 libc6-dev-${{ matrix.config.libc }}-cross \
+                                 qemu-user
+
+          # Not set unconditionally: an empty QEMU_CPU makes qemu fail to find
+          # a CPU definition.
+          if [ -n "${{ matrix.config.qemu_cpu }}" ]; then
+            echo "QEMU_CPU=${{ matrix.config.qemu_cpu }}" >> "$GITHUB_ENV"
+          fi
+        env:
+          DEBIAN_FRONTEND: noninteractive
+
+      - name: Checkout
+        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+        with:
+          submodules: true
+
+      - name: Prepare
+        run: ./autogen.sh
+
+      - name: Configure
+        # Shared libraries are disabled so that the test programs are real
+        # executables rather than libtool wrapper scripts, which qemu-user
+        # cannot run.
+        run: |
+          ./configure \
+            --build=`./config.guess` \
+            --host=${{ matrix.config.triple }} \
+            CC=${{ matrix.config.triple }}-gcc-14 \
+            CFLAGS="${{ matrix.config.cflags }} $CFLAGS_GCC_STYLE" \
+            --disable-shared \
+            --enable-jit \
+            --enable-pcre2-16 \
+            --enable-pcre2-32
+
+      - name: Build
+        run: make -j$(nproc)
+
+      - name: Test (main test script)
+        run: ./RunTest -sim qemu-${{ matrix.config.qemu }}
+
+      - name: Test (JIT test program)
+        run: qemu-${{ matrix.config.qemu }} ./pcre2_jit_test
+
+      - name: Test (pcre2posix program)
+        run: qemu-${{ matrix.config.qemu }} ./pcre2posix_test -v
+
   zebrilus:
     # Tests with: Zig compiler. A "zebrilus" is known as a "zigzag heron".
     name: Zig

From 98814f98e4be347b6bae2d06549d990d44e8aaab Mon Sep 17 00:00:00 2001
From: Matt Turner 
Date: Sun, 9 Aug 2026 15:01:19 -0400
Subject: [PATCH 7/7] Test the Alpha JIT backend under QEMU

Add alpha to the coelacanth matrix, so the new sljitNativeALPHA_64.c
backend is exercised by CI like the others.

Alpha is not a release architecture, but the cross toolchain and the
emulator are both packaged: gcc-14-alpha-linux-gnu and qemu-alpha are
available in Ubuntu 26.04, and no extra apt sources are needed. The
libc package is named libc6.1-dev-alpha-cross, because Alpha uses
libc6.1 rather than libc6, and it provides the libc6-dev-alpha-cross
name that the job's "libc" matrix field builds, so the existing install
line needs no change.

Verified by cross-compiling with gcc 15 and running the full suite under
qemu-alpha: RunTest passes with and without JIT, and pcre2_jit_test and
pcre2posix_test both pass, in 8-, 16- and 32-bit modes.
---
 .github/workflows/dev.yml | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml
index 5d48b0b04..e50f4bc1c 100644
--- a/.github/workflows/dev.yml
+++ b/.github/workflows/dev.yml
@@ -643,6 +643,9 @@ jobs:
           # cross compilers do not depend on the target's libc headers, and it
           # cannot always be derived from the triple.
           #
+          # Alpha's libc package is libc6.1-dev-alpha-cross, which provides the
+          # libc6-dev-alpha-cross name used here.
+          - { name: alpha,       triple: alpha-linux-gnu,             qemu: alpha,       libc: alpha,      container: "ubuntu:26.04" }
           # Ubuntu armhf defaults to Thumb-2, so ARM mode needs asking for.
           - { name: arm,         triple: arm-linux-gnueabihf,         qemu: arm,         libc: armhf,      container: "ubuntu:26.04", cflags: -marm }
           - { name: arm-thumb2,  triple: arm-linux-gnueabihf,         qemu: arm,         libc: armhf,      container: "ubuntu:26.04", cflags: -mthumb }