diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index 9de201e79b..5ae387f9f2 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -4387,9 +4387,8 @@ private static int executeTypeOps(int opcode, int[] bytecode, int pc, if (implicitU != 0) { flags = RuntimeRegex.applyUnicodeStringsFeatureToModifiers(flags); } - RegexQuoteMeta.setCallSiteWarningState(warningState); - RegexQuoteMeta.setCallSiteWarningBits(warningBitsIndex >= 0 - ? code.stringPool[warningBitsIndex] : null); + RegexQuoteMeta.setCallSiteWarning(warningState, + warningBitsIndex >= 0 ? code.stringPool[warningBitsIndex] : null); registers[rd] = RuntimeRegex.getQuotedRegex( registers[patternReg].scalar(), flags, namedCharacterExpansions); @@ -4419,9 +4418,8 @@ private static int executeTypeOps(int opcode, int[] bytecode, int pc, if (implicitU != 0) { flags = RuntimeRegex.applyUnicodeStringsFeatureToModifiers(flags); } - RegexQuoteMeta.setCallSiteWarningState(warningState); - RegexQuoteMeta.setCallSiteWarningBits(warningBitsIndex >= 0 - ? code.stringPool[warningBitsIndex] : null); + RegexQuoteMeta.setCallSiteWarning(warningState, + warningBitsIndex >= 0 ? code.stringPool[warningBitsIndex] : null); registers[rd] = RuntimeRegex.getQuotedRegex( registers[patternReg].scalar(), flags, callsiteId, namedCharacterExpansions); diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitRegex.java b/src/main/java/org/perlonjava/backend/jvm/EmitRegex.java index 36065cea1d..75c4fdfb95 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitRegex.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitRegex.java @@ -76,9 +76,6 @@ private static void emitRegexWarningState(EmitterVisitor emitterVisitor, Operato emitterVisitor.ctx.mv.visitInsn(suppressed ? Opcodes.ICONST_M1 : fatal ? Opcodes.ICONST_2 : enabled ? Opcodes.ICONST_1 : Opcodes.ICONST_0); - emitterVisitor.ctx.mv.visitMethodInsn(Opcodes.INVOKESTATIC, - "org/perlonjava/runtime/regex/RegexQuoteMeta", - "setCallSiteWarningState", "(I)V", false); Object warningBits = node.getAnnotation("regexWarningBits"); if (warningBits instanceof String bits) { emitterVisitor.ctx.mv.visitLdcInsn(bits); @@ -87,7 +84,7 @@ private static void emitRegexWarningState(EmitterVisitor emitterVisitor, Operato } emitterVisitor.ctx.mv.visitMethodInsn(Opcodes.INVOKESTATIC, "org/perlonjava/runtime/regex/RegexQuoteMeta", - "setCallSiteWarningBits", "(Ljava/lang/String;)V", false); + "setCallSiteWarning", "(ILjava/lang/String;)V", false); } /** Stack: pattern -> pattern carrying this literal's immutable lexical results. */ diff --git a/src/main/java/org/perlonjava/runtime/regex/JoniRegexPattern.java b/src/main/java/org/perlonjava/runtime/regex/JoniRegexPattern.java index 657ee4eece..49ab7d8b79 100644 --- a/src/main/java/org/perlonjava/runtime/regex/JoniRegexPattern.java +++ b/src/main/java/org/perlonjava/runtime/regex/JoniRegexPattern.java @@ -74,6 +74,28 @@ record DeferredPropertyFact(String name, String displayName, private static final int INPUT_ENCODING_CACHE_ENTRIES = 512; private static final int INPUT_ENCODING_CACHE_MAX_LENGTH = 8_192; private static final int DYNAMIC_PATTERN_CACHE_ENTRIES = 128; + + private static final int IDENTITY_ARRAY_POOL_SIZE = 64; + private static final int[][] IDENTITY_ARRAYS = buildIdentityArrayPool(); + + private static int[][] buildIdentityArrayPool() { + int[][] pool = new int[IDENTITY_ARRAY_POOL_SIZE + 1][]; + for (int len = 0; len <= IDENTITY_ARRAY_POOL_SIZE; len++) { + int[] identity = new int[len + 1]; + for (int i = 0; i <= len; i++) identity[i] = i; + pool[len] = identity; + } + return pool; + } + + private static int[] identityArray(int length) { + if (length <= IDENTITY_ARRAY_POOL_SIZE) { + return IDENTITY_ARRAYS[length]; + } + int[] identity = new int[length + 1]; + for (int i = 0; i <= length; i++) identity[i] = i; + return identity; + } private static final Map INPUT_ENCODINGS = inputEncodingCache(); private static final Map BYTE_INPUT_ENCODINGS = inputEncodingCache(); private static final Map SUBJECT_INPUT_ENCODINGS = @@ -324,6 +346,7 @@ private static CharacterPropertyResolver.Result resolveCharacterProperty( private final boolean byteMode; private final java.nio.charset.Charset sourceCharset; private final List compileWarnings; + private final String fixedLiteral; JoniRegexPattern(String perlPattern, RegexFlags flags) { this(perlPattern, flags, 0, false); @@ -455,6 +478,27 @@ public boolean supportsPositions() { NamedGroupMaps groupMaps = collectNamedGroups(regex); namedGroups = groupMaps.logical(); physicalNamedGroups = groupMaps.physical(); + fixedLiteral = detectFixedLiteral(sourcePattern, regex, flags, byteMode); + } + + private static String detectFixedLiteral(String pattern, Regex compiled, + RegexFlags flags, boolean byteMode) { + if (compiled.numberOfCaptures() != 0 || byteMode) return null; + if (flags != null && (flags.isCaseInsensitive() || flags.isExtended())) return null; + for (int i = 0; i < pattern.length(); i++) { + char c = pattern.charAt(i); + if (c == '\\' || c == '.' || c == '^' || c == '$' || c == '*' + || c == '+' || c == '?' || c == '{' || c == '}' + || c == '[' || c == ']' || c == '(' || c == ')' + || c == '|') { + return null; + } + } + return pattern.isEmpty() ? null : pattern; + } + + String getFixedLiteral() { + return fixedLiteral; } private record WarningDisplay(String pattern, int offset) {} @@ -655,7 +699,13 @@ static InputEncoding inputEncoding(String input, RuntimeScalar subject, boolean } static InputEncoding inputEncoding(String input) { - if (input.length() > INPUT_ENCODING_CACHE_MAX_LENGTH) { + int len = input.length(); + if (len <= IDENTITY_ARRAY_POOL_SIZE && isAscii(input, len)) { + byte[] bytes = asciiToBytes(input, len); + int[] identity = IDENTITY_ARRAYS[len]; + return new InputEncoding(bytes, identity, identity); + } + if (len > INPUT_ENCODING_CACHE_MAX_LENGTH) { return buildInputEncoding(input); } synchronized (INPUT_ENCODINGS) { @@ -664,7 +714,14 @@ static InputEncoding inputEncoding(String input) { } static InputEncoding byteInputEncoding(String input) { - if (input.length() > INPUT_ENCODING_CACHE_MAX_LENGTH) { + int len = input.length(); + if (len <= IDENTITY_ARRAY_POOL_SIZE) { + byte[] bytes = new byte[len]; + for (int i = 0; i < len; i++) bytes[i] = (byte) input.charAt(i); + int[] identity = IDENTITY_ARRAYS[len]; + return new InputEncoding(bytes, identity, identity); + } + if (len > INPUT_ENCODING_CACHE_MAX_LENGTH) { return buildByteInputEncoding(input); } synchronized (BYTE_INPUT_ENCODINGS) { @@ -675,16 +732,20 @@ static InputEncoding byteInputEncoding(String input) { private static InputEncoding buildByteInputEncoding(String input) { byte[] bytes = input.getBytes(StandardCharsets.ISO_8859_1); - int[] identity = new int[input.length() + 1]; - for (int i = 0; i < identity.length; i++) identity[i] = i; + int[] identity = identityArray(input.length()); return new InputEncoding(bytes, identity, identity); } private static InputEncoding buildInputEncoding(String input) { + int len = input.length(); + if (isAscii(input, len)) { + byte[] bytes = asciiToBytes(input, len); + int[] identity = identityArray(len); + return new InputEncoding(bytes, identity, identity); + } byte[] bytes = input.getBytes(StandardCharsets.UTF_8); - if (bytes.length == input.length()) { - int[] identity = new int[input.length() + 1]; - for (int i = 0; i <= input.length(); i++) identity[i] = i; + if (bytes.length == len) { + int[] identity = identityArray(len); return new InputEncoding(bytes, identity, identity); } int[] charToByte = JoniRegexMatcher.buildCharToByte(input); @@ -692,6 +753,21 @@ private static InputEncoding buildInputEncoding(String input) { return new InputEncoding(bytes, charToByte, byteToChar); } + private static boolean isAscii(String input, int len) { + for (int i = 0; i < len; i++) { + if (input.charAt(i) > 0x7f) return false; + } + return true; + } + + private static byte[] asciiToBytes(String input, int len) { + byte[] bytes = new byte[len]; + for (int i = 0; i < len; i++) { + bytes[i] = (byte) input.charAt(i); + } + return bytes; + } + String patternDescription() { return sourcePattern; } diff --git a/src/main/java/org/perlonjava/runtime/regex/RegexQuoteMeta.java b/src/main/java/org/perlonjava/runtime/regex/RegexQuoteMeta.java index 9d85a2880e..c21c52eb86 100644 --- a/src/main/java/org/perlonjava/runtime/regex/RegexQuoteMeta.java +++ b/src/main/java/org/perlonjava/runtime/regex/RegexQuoteMeta.java @@ -7,8 +7,14 @@ public class RegexQuoteMeta { private static final int CALL_SITE_WARNING_UNSET = Integer.MIN_VALUE; - private static final ThreadLocal CALL_SITE_WARNING_STATE = ThreadLocal.withInitial(() -> new int[]{CALL_SITE_WARNING_UNSET}); - private static final ThreadLocal CALL_SITE_WARNING_BITS = ThreadLocal.withInitial(() -> new String[]{null}); + + static final class CallSiteWarningHolder { + int state = CALL_SITE_WARNING_UNSET; + String bits; + } + + private static final ThreadLocal CALL_SITE_WARNING = + ThreadLocal.withInitial(CallSiteWarningHolder::new); private static final ThreadLocal PARSER_WARNING_BITS = new ThreadLocal<>(); private static final ThreadLocal MATCH_TARGET_NAME = new ThreadLocal<>(); @@ -17,21 +23,27 @@ public record ConstructionWarningDisposition(boolean enabled, boolean fatal) { } public static void setCallSiteWarningState(int state) { - int[] holder = CALL_SITE_WARNING_STATE.get(); - if (holder[0] != state) holder[0] = state; + CallSiteWarningHolder h = CALL_SITE_WARNING.get(); + if (h.state != state) h.state = state; } public static void setCallSiteWarningBits(String bits) { - String[] holder = CALL_SITE_WARNING_BITS.get(); - if (holder[0] != bits) holder[0] = bits; + CallSiteWarningHolder h = CALL_SITE_WARNING.get(); + if (h.bits != bits) h.bits = bits; + } + + public static void setCallSiteWarning(int state, String bits) { + CallSiteWarningHolder h = CALL_SITE_WARNING.get(); + if (h.state != state) h.state = state; + if (h.bits != bits) h.bits = bits; } public static String getCallSiteWarningBits() { - return CALL_SITE_WARNING_BITS.get()[0]; + return CALL_SITE_WARNING.get().bits; } public static boolean isCallSiteWarningExplicitlyDisabled() { - int state = CALL_SITE_WARNING_STATE.get()[0]; + int state = CALL_SITE_WARNING.get().state; return state != CALL_SITE_WARNING_UNSET && state < 0; } @@ -61,10 +73,11 @@ static void warnAtConstruction(String message, boolean strictDefault) { // This warning belongs to construction of the interpolated pattern. // Retaining it on the cached RuntimeRegex re-emits it for every match // and can leak a warning-enabled compilation into a no-warnings use. - int state = CALL_SITE_WARNING_STATE.get()[0]; + CallSiteWarningHolder h = CALL_SITE_WARNING.get(); + int state = h.state; RuntimeScalar warning = new RuntimeScalar(message); RuntimeScalar where = new RuntimeScalar(""); - String warningBits = CALL_SITE_WARNING_BITS.get()[0]; + String warningBits = h.bits; if (warningBits == null) { // Synthetic/interpolated regex wrappers do not always have a // dedicated quote opcode. Use the active statement mask rather @@ -93,7 +106,8 @@ static void warnAtConstruction(String message, boolean strictDefault) { public static ConstructionWarningDisposition constructionWarningDisposition( String message, boolean strictDefault) { String category = warningCategory(message); - String warningBits = CALL_SITE_WARNING_BITS.get()[0]; + CallSiteWarningHolder h = CALL_SITE_WARNING.get(); + String warningBits = h.bits; if (warningBits == null) { warningBits = WarningBitsRegistry.getRuntimeWarningBits(); } @@ -108,7 +122,7 @@ public static ConstructionWarningDisposition constructionWarningDisposition( enabled && WarningFlags.isFatalInBits(warningBits, category)); } - int state = CALL_SITE_WARNING_STATE.get()[0]; + int state = h.state; if (state != CALL_SITE_WARNING_UNSET) { return new ConstructionWarningDisposition(state > 0, state == 2); } diff --git a/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java b/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java index dac470fef2..f25d0d7f83 100644 --- a/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java +++ b/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java @@ -226,6 +226,106 @@ public void markSplitWhitespaceDebug() { public void emitCompileDebugTraceForSplit() { emitCompileDebugTrace(true); } private boolean lexicalReStrict; private ParseDebugTrace failedParseDebugTrace = ParseDebugTrace.EMPTY; + transient String literalFastPathString; + private transient boolean literalFastPathChecked; + + void ensureLiteralFastPathChecked() { + if (!literalFastPathChecked) { + literalFastPathChecked = true; + if (replacement == null + && !matched + && !quoteConstruction + && warningsOnUse.isEmpty() + && !hasPreservesMatch + && !useGAssertion + && !hasCodeBlockCaptures + && lexicalDebugMode == 0 + && regexFlags != null + && !regexFlags.isGlobalMatch() + && !regexFlags.isMatchExactlyOnce() + && !regexFlags.taintResults() + && recursivePattern != null) { + literalFastPathString = recursivePattern.getFixedLiteral(); + } + } + } + + private static final ThreadLocal LITERAL_MATCHER = + ThreadLocal.withInitial(LiteralMatcher::new); + + private static RuntimeBase matchLiteralScalar(RuntimeRegex regex, String literal, RuntimeScalar string) { + RuntimeScalar inputValue = RuntimeScalarType.blessedId(string) != 0 + ? Overload.stringify(string) : string; + String inputStr = inputValue.toString(); + + int idx = inputStr.indexOf(literal); + boolean found = idx >= 0; + + if (found) { + PerlRuntime runtime = PerlRuntime.current(); + RuntimeRegexState regexState = runtime.regexState; + regexState.lastMatchResultsTainted = false; + regexState.lastMatchWasByteString = + inputValue.type == RuntimeScalarType.BYTE_STRING; + int end = idx + literal.length(); + LiteralMatcher lm = LITERAL_MATCHER.get(); + lm.update(inputStr, idx, end); + regexState.globalMatcher = lm; + regexState.globalMatchString = inputStr; + regexState.lastMatchUsedBackslashK = false; + regexState.lastMatchUsedPFlag = regex.hasPreservesMatch; + regexState.lastNamedCaptureGroups = null; + regexState.lastParenMatchOverrideActive = false; + regexState.lastParenMatchOverride = null; + regexState.manualCaptureStarts = null; + regexState.manualCaptureEnds = null; + regexState.provisionalCaptureResolver = null; + regexState.provisionalNamedCaptureGroups = null; + regexState.lastClosedCapture = null; + regexState.lastCaptureGroups = null; + regexState.lastMatchedString = literal; + regexState.lastMatchStart = idx; + regexState.lastMatchEnd = end; + regexState.lastSuccessfulPattern = regex; + regexState.lastSuccessfulMatchedString = literal; + regexState.lastSuccessfulMatchStart = idx; + regexState.lastSuccessfulMatchEnd = end; + regexState.lastSuccessfulMatchString = inputStr; + } + + return RuntimeScalarCache.getScalarBoolean(found); + } + + private static final class LiteralMatcher implements RegexMatcher { + private String input; + private int matchStart; + private int matchEnd; + + void update(String input, int start, int end) { + this.input = input; + this.matchStart = start; + this.matchEnd = end; + } + + @Override public boolean find() { return false; } + @Override public void region(int start, int end) {} + @Override public void useAnchoringBounds(boolean enabled) {} + @Override public void useTransparentBounds(boolean enabled) {} + @Override public int start() { return matchStart; } + @Override public int end() { return matchEnd; } + @Override public int start(int index) { return index == 0 ? matchStart : -1; } + @Override public int end(int index) { return index == 0 ? matchEnd : -1; } + @Override public int start(String name) { return -1; } + @Override public int end(String name) { return -1; } + @Override public String group(int index) { + return index == 0 ? input.substring(matchStart, matchEnd) : null; + } + @Override public String group(String name) { return null; } + @Override public int groupCount() { return 0; } + @Override public Map namedGroups() { return null; } + @Override public String patternDescription() { return ""; } + } + public RuntimeRegex() { this.regexFlags = null; } @@ -3599,7 +3699,15 @@ public static RuntimeBase matchRegex(RuntimeScalar quotedRegex, RuntimeScalar st WarnDie.warnWithCategory(new RuntimeScalar(message), RuntimeScalarCache.scalarEmptyString, "uninitialized"); } - RuntimeRegex regex = resolveRegex(quotedRegex); + // Inline fast resolution for pre-compiled REGEX type (avoids ResolvedRegex record) + RuntimeScalar resolvedQuotedRegex = quotedRegex.type == RuntimeScalarType.READONLY_SCALAR + ? (RuntimeScalar) quotedRegex.value : quotedRegex; + RuntimeRegex regex; + if (resolvedQuotedRegex.type == RuntimeScalarType.REGEX) { + regex = (RuntimeRegex) resolvedQuotedRegex.value; + } else { + regex = resolveRegex(quotedRegex); + } if (regex.replacement != null) { if (regex.bytesSubstitution) { return replaceRegexBytes(quotedRegex, string, ctx); @@ -3607,6 +3715,13 @@ public static RuntimeBase matchRegex(RuntimeScalar quotedRegex, RuntimeScalar st return replaceRegex(quotedRegex, string, ctx); } + // Ultra-fast path: pure literal pattern in scalar/void context + regex.ensureLiteralFastPathChecked(); + String literal = regex.literalFastPathString; + if (literal != null && ctx != RuntimeContextType.LIST) { + return matchLiteralScalar(regex, literal, string); + } + // Check if alarm is active - if so, use timeout wrapper to prevent catastrophic backtracking if (Time.hasActiveAlarm()) { int timeoutSeconds = Time.getAlarmRemainingSeconds(); @@ -3757,7 +3872,8 @@ private static RuntimeBase matchRegexDirect(RuntimeScalar quotedRegex, RuntimeSc private static RuntimeBase matchRegexDirect(RuntimeScalar quotedRegex, RuntimeScalar string, int ctx, boolean alarmInterruptMode, RuntimeRegex preResolved) { - RuntimeRegexState regexState = state(); + PerlRuntime runtime = PerlRuntime.current(); + RuntimeRegexState regexState = runtime.regexState; RuntimeRegex regex = preResolved != null ? preResolved : resolveRegex(quotedRegex); // Save original flags before potentially changing regex @@ -3956,7 +4072,7 @@ private static RuntimeBase matchRegexDirect(RuntimeScalar quotedRegex, RuntimeSc } found = true; - regexState.lastMatchResultsTainted = GlobalContext.isTaintModeActive() + regexState.lastMatchResultsTainted = runtime.executionState.taintMode && (quotedRegex.isTainted() || localeResultsTainted || (regex.regexFlags.taintResults() && string.isTainted())); diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java index afac592022..de8f972ad8 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java @@ -49,7 +49,25 @@ private static final class MutableIntegerValue extends Number { @Override public long longValue() { return value; } @Override public float floatValue() { return value; } @Override public double doubleValue() { return value; } - @Override public String toString() { return Long.toString(value); } + @Override public String toString() { return integerToString(value); } + } + + private static final int INT_STRING_CACHE_SIZE = 512; + private static final String[] INT_STRING_CACHE = buildIntStringCache(); + + private static String[] buildIntStringCache() { + String[] cache = new String[INT_STRING_CACHE_SIZE]; + for (int i = 0; i < INT_STRING_CACHE_SIZE; i++) { + cache[i] = Integer.toString(i); + } + return cache; + } + + static String integerToString(long value) { + if (value >= 0 && value < INT_STRING_CACHE_SIZE) { + return INT_STRING_CACHE[(int) value]; + } + return Long.toString(value); } private static Object copyScalarPayload(int scalarType, Object payload) { @@ -3360,6 +3378,12 @@ public String toString() { if (type == STRING || type == BYTE_STRING) { return materializeGrowingString(); } + if (type == INTEGER) { + if (value instanceof Integer i) { + return integerToString(i); + } + return value.toString(); + } return toStringLarge(); } diff --git a/src/test/resources/unit/regex_hot_path_perf.t b/src/test/resources/unit/regex_hot_path_perf.t new file mode 100644 index 0000000000..02bdd13129 --- /dev/null +++ b/src/test/resources/unit/regex_hot_path_perf.t @@ -0,0 +1,124 @@ +use strict; +use warnings; +use Test::More; + +# Tests for regex hot-path optimizations: +# - ASCII input encoding fast path (bypasses synchronized cache) +# - Identity array pooling for charToByte/byteToChar mappings +# - Integer-to-string conversion in regex matching context +# - Non-ASCII fallback still works correctly + +plan tests => 37; + +# --- ASCII string matching (exercises fast-path input encoding) --- + +my $str = "hello world 42"; +ok($str =~ /hello/, "ASCII match - simple word"); +ok($str =~ /42/, "ASCII match - digits"); +ok($str =~ /hello\s+world/, "ASCII match - with metachar"); +ok($str !~ /xyz/, "ASCII non-match"); + +# Captures with ASCII strings +if ($str =~ /(\w+)\s+(\w+)\s+(\d+)/) { + is($1, "hello", "ASCII capture group 1"); + is($2, "world", "ASCII capture group 2"); + is($3, "42", "ASCII capture group 3"); +} else { + fail("ASCII capture match failed") for 1..3; +} + +# --- Integer matching (exercises INTEGER toString fast path) --- + +my $i = 42; +ok($i =~ /42/, "integer =~ literal match"); +ok($i =~ /4/, "integer =~ partial match"); +ok($i !~ /99/, "integer =~ non-match"); + +# Integer in loop (the benchmark pattern) +my $count = 0; +for my $n (0..99) { + $count++ if $n =~ /42/; +} +is($count, 1, "integer regex in loop finds exactly one match"); + +# Negative integer +$i = -7; +ok($i =~ /-7/, "negative integer match"); +ok($i =~ /\d/, "negative integer digit match"); + +# Large integer (beyond pool size but still integer type) +$i = 123456789; +ok($i =~ /123456789/, "large integer exact match"); +ok($i =~ /456/, "large integer substring match"); + +# --- Short ASCII strings (within identity array pool, <=64 chars) --- + +my $short = "abc"; +ok($short =~ /abc/, "short ASCII (3 chars) match"); + +my $pool_boundary = "a" x 64; +ok($pool_boundary =~ /a{64}/, "pool boundary (64 chars) match"); + +my $over_pool = "b" x 65; +ok($over_pool =~ /b{65}/, "above pool size (65 chars) match"); + +# --- Non-ASCII / Unicode (must still work via fallback path) --- + +my $unicode = "café"; +ok($unicode =~ /caf/, "unicode string ASCII prefix match"); +ok($unicode =~ /é/, "unicode string non-ASCII char match"); + +my $wide = "\x{263A}"; # smiley +ok($wide =~ /\x{263A}/, "wide unicode char match"); + +my $mixed = "abc\x{00E9}def"; # abc + é + def +if ($mixed =~ /(abc)(\x{00E9})(def)/) { + is($1, "abc", "mixed unicode capture group 1"); + is($2, "\x{00E9}", "mixed unicode capture group 2"); + is($3, "def", "mixed unicode capture group 3"); +} else { + fail("mixed unicode capture failed") for 1..3; +} + +# --- Global match with integer target --- + +my @matches; +$i = 121212; +@matches = ("$i" =~ /12/g); +is(scalar @matches, 3, "global match on stringified integer"); + +# --- Substitution with integer --- + +$i = 42; +my $s = "$i"; +$s =~ s/4/X/; +is($s, "X2", "substitution on stringified integer"); + +# --- Empty and single-char edge cases --- + +$str = ""; +ok($str =~ /^$/, "empty string matches empty pattern"); +ok($str !~ /a/, "empty string does not match 'a'"); + +$str = "x"; +ok($str =~ /x/, "single char ASCII match"); + +# --- Literal fast path: match position variables --- + +$str = "hello world"; +if ($str =~ /world/) { + is($-[0], 6, "literal match \$-[0] position"); + is($+[0], 11, "literal match \$+[0] position"); + is($&, "world", "literal match \$& value"); + is($`, "hello ", "literal match \$` prematch"); + is($', "", "literal match \$' postmatch"); +} + +# Literal fast path with integer input +$i = 12345; +if ($i =~ /234/) { + is($-[0], 1, "integer literal match \$-[0]"); + is($+[0], 4, "integer literal match \$+[0]"); + is($&, "234", "integer literal match \$&"); +} +