diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index 2194b93c3..61d97c3a2 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -1427,8 +1427,8 @@ public void visit(StringNode node) { : RuntimeScalarCache.getOrCreateStringIndex(node.value); if (cacheIdx >= 0) { RuntimeScalar cached = (opcode == Opcodes.LOAD_BYTE_STRING) - ? RuntimeScalarCache.getScalarByteString(cacheIdx) - : RuntimeScalarCache.getScalarString(cacheIdx); + ? RuntimeScalarCache.materializeByteStringLiteral(cacheIdx) + : RuntimeScalarCache.materializeStringLiteral(cacheIdx); int constIdx = addToConstantPool(cached); emit(Opcodes.LOAD_CONST); emitReg(rd); diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperator.java b/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperator.java index 5cedbc636..4d65644ae 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperator.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperator.java @@ -473,8 +473,9 @@ else if (node.right instanceof BinaryOperatorNode rightCall) { bytecodeCompiler.emitReg(rd); bytecodeCompiler.emitInt(0); - int rightCtx = bytecodeCompiler.currentCallContext; - bytecodeCompiler.compileNode(node.right, rd, rightCtx); + // LHS is scalar for the boolean test; RHS uses this operator's context (LIST/SCALAR/ + // VOID/RUNTIME) — perlop: context propagates to the right operand (//: EXPR2 in //). + bytecodeCompiler.compileNode(node.right, rd, bytecodeCompiler.currentCallContext); int rs2 = bytecodeCompiler.lastResultReg; if (rs2 >= 0) { bytecodeCompiler.emitAliasWithTarget(rd, rs2); @@ -499,8 +500,8 @@ else if (node.right instanceof BinaryOperatorNode rightCall) { bytecodeCompiler.emitReg(rd); bytecodeCompiler.emitInt(0); - int rightCtx = bytecodeCompiler.currentCallContext; - bytecodeCompiler.compileNode(node.right, rd, rightCtx); + // LHS scalar for truth test; RHS in this operator's context — see && branch above. + bytecodeCompiler.compileNode(node.right, rd, bytecodeCompiler.currentCallContext); int rs2 = bytecodeCompiler.lastResultReg; if (rs2 >= 0) { bytecodeCompiler.emitAliasWithTarget(rd, rs2); @@ -530,8 +531,8 @@ else if (node.right instanceof BinaryOperatorNode rightCall) { bytecodeCompiler.emitReg(definedReg); bytecodeCompiler.emitInt(0); - int rightCtx = bytecodeCompiler.currentCallContext; - bytecodeCompiler.compileNode(node.right, rd, rightCtx); + // LHS scalar for definedness; RHS in this operator's context — see && branch above. + bytecodeCompiler.compileNode(node.right, rd, bytecodeCompiler.currentCallContext); int rs2 = bytecodeCompiler.lastResultReg; if (rs2 >= 0) { bytecodeCompiler.emitAliasWithTarget(rd, rs2); diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitLiteral.java b/src/main/java/org/perlonjava/backend/jvm/EmitLiteral.java index 8fc5c06ed..6a1538e1d 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitLiteral.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitLiteral.java @@ -276,8 +276,8 @@ public static void emitString(EmitterContext ctx, StringNode node) { mv.visitMethodInsn( Opcodes.INVOKESTATIC, "org/perlonjava/runtime/runtimetypes/RuntimeScalarCache", - "getScalarByteString", - "(I)Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;", + "materializeByteStringLiteral", + "(I)Lorg/perlonjava/runtime/runtimetypes/RuntimeScalarReadOnly;", false); return; } else { @@ -311,8 +311,8 @@ public static void emitString(EmitterContext ctx, StringNode node) { mv.visitMethodInsn( Opcodes.INVOKESTATIC, "org/perlonjava/runtime/runtimetypes/RuntimeScalarCache", - "getScalarString", - "(I)Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;", + "materializeStringLiteral", + "(I)Lorg/perlonjava/runtime/runtimetypes/RuntimeScalarReadOnly;", false); } else { // String is too long for cache or null, create new object diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitLogicalOperator.java b/src/main/java/org/perlonjava/backend/jvm/EmitLogicalOperator.java index 9ea901ab8..cffb4a762 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitLogicalOperator.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitLogicalOperator.java @@ -246,10 +246,12 @@ static void emitLogicalOperator(EmitterVisitor emitterVisitor, BinaryOperatorNod // If true, jump to convert label mv.visitJumpInsn(compareOpcode, convertLabel); - // LHS is false: evaluate RHS in LIST context + // LHS is false: RHS is evaluated in the same context as this &&/||/// + // (perlop: context propagates to the right operand). For //, EXPR2 is explicitly + // in the context // itself; LHS stays scalar for the test above. mv.visitInsn(Opcodes.POP); // Remove LHS node.right.accept(emitterVisitor.with(RuntimeContextType.LIST)); - // Stack: [RuntimeList] + // Stack: [RuntimeList] — LIST context emission matches flattening for aggregates mv.visitJumpInsn(Opcodes.GOTO, endLabel); // LHS is true: convert scalar to list diff --git a/src/main/java/org/perlonjava/runtime/regex/RegexFlags.java b/src/main/java/org/perlonjava/runtime/regex/RegexFlags.java index 91b17768f..42106e3dc 100644 --- a/src/main/java/org/perlonjava/runtime/regex/RegexFlags.java +++ b/src/main/java/org/perlonjava/runtime/regex/RegexFlags.java @@ -28,11 +28,15 @@ public record RegexFlags(boolean isGlobalMatch, boolean keepCurrentPosition, boo boolean isAscii) { public static RegexFlags fromModifiers(String modifiers, String patternString) { + // m?PAT? is encoded by StringParser as an extra trailing '?' on the modifier string + // (see parseRegexMatch). Do NOT use modifiers.contains("?"): '?' appears inside many + // ordinary patterns (e.g. (?:...) or ...?) and must not enable match-once mode for those. + boolean matchOnce = modifiers != null && modifiers.endsWith("?"); return new RegexFlags( modifiers.contains("g"), modifiers.contains("c"), modifiers.contains("r"), - modifiers.contains("?"), + matchOnce, patternString != null && patternString.contains("\\G"), modifiers.contains("xx"), modifiers.contains("n"), diff --git a/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java b/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java index 8cdd0fd00..baec8ecb5 100644 --- a/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java +++ b/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java @@ -990,7 +990,9 @@ private static RuntimeBase matchRegexDirect(RuntimeScalar quotedRegex, RuntimeSc if (found) { fixPerl16894AlternateCaptureInLookahead(regex, inputStr); - regex.matched = true; // Counter for m?PAT? + if (regex.regexFlags.isMatchExactlyOnce()) { + regex.matched = true; // m?PAT? — remember we consumed the one allowed match + } lastMatchUsedPFlag = regex.hasPreservesMatch; lastSuccessfulPattern = regex; // Store last successful match information (persists across failed matches) diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimePosLvalue.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimePosLvalue.java index 4474af17c..d2a797e3d 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimePosLvalue.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimePosLvalue.java @@ -70,8 +70,21 @@ public static void invalidatePos(RuntimeScalar perlVariable) { if (perlVariable == null) { return; } - // Remove the cache entry entirely so pos() returns undef - positionCache.remove(perlVariable); + // Reset the canonical pos lvalue in place. Removing the cache entry orphans the + // PosLvalueScalar that matchRegexDirect may already hold (local posScalar), breaking + // /g and \\G after (?{ }) or other mid-match assignments to the target scalar. + CacheEntry cachedEntry = positionCache.get(perlVariable); + if (cachedEntry != null) { + int code = perlVariable.value == null ? 0 : perlVariable.value.hashCode(); + cachedEntry.valueHash = code; + RuntimeScalar pos = cachedEntry.regexPosition; + pos.type = RuntimeScalarType.UNDEF; + pos.value = null; + cachedEntry.lastMatchWasZeroLength = false; + cachedEntry.lastMatchPosition = -1; + cachedEntry.lastMatchPattern = null; + cachedEntry.hasUnicodeChars = null; + } } private static void clearZeroLengthMatchTracking(RuntimeScalar perlVariable) { diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java index f407b2087..6e8afe1d8 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java @@ -1040,11 +1040,27 @@ public void releaseOwnedScalarReferenceContents() { // Types < TIED_SCALAR (0-8) never have REFERENCE_BIT (0x8000), so no // reference check is needed here — all reference types route to setLarge(). public RuntimeScalar set(RuntimeScalar value) { - if (this.type < TIED_SCALAR & value.type < TIED_SCALAR) { - this.type = value.type; - this.value = value.value; + // Perl clears pos() when assigning from another SV ($x = $y), but preserves it for + // self-assignment ($x = $x). Hash/array element slots reuse one RuntimeScalar per key; + // $h{k} = $str must reset pos on that slot (Data::SExpression set_input / lexer \G). + // Invalidate only after the new value is stored so the pos cache records the correct + // string hash, and refresh in place so existing PosLvalueScalar handles stay valid. + if (value != null && this.type < TIED_SCALAR & value.type < TIED_SCALAR) { + if (this != value) { + this.type = value.type; + this.value = value.value; + RuntimePosLvalue.invalidatePos(this); + } else { + this.type = value.type; + this.value = value.value; + } return this; } + if (this != value) { + RuntimeScalar r = setLarge(value); + RuntimePosLvalue.invalidatePos(this); + return r; + } return setLarge(value); } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalarCache.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalarCache.java index 8301d2ec4..2cb816c18 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalarCache.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalarCache.java @@ -201,6 +201,29 @@ public static RuntimeScalar getScalarString(int index) { return scalarString[index]; } + /** + * Materialize a fresh read-only string scalar from a short-string cache entry. + *

Perl allocates a distinct SV for each string literal occurrence. Reusing the + * singleton from {@link #getScalarString(int)} breaks operations keyed by scalar identity, + * notably {@code pos()} / {@code \\G} regex state (see Data::SExpression folding tests). + */ + public static RuntimeScalarReadOnly materializeStringLiteral(int index) { + RuntimeScalarReadOnly template = scalarString[index]; + RuntimeScalarReadOnly copy = new RuntimeScalarReadOnly(template.s); + copy.type = template.type; + return copy; + } + + /** + * Same as {@link #materializeStringLiteral(int)} for octet-string literals. + */ + public static RuntimeScalarReadOnly materializeByteStringLiteral(int index) { + RuntimeScalarReadOnly template = scalarByteString[index]; + RuntimeScalarReadOnly copy = new RuntimeScalarReadOnly(template.s); + copy.type = RuntimeScalarType.BYTE_STRING; + return copy; + } + /** * Looks up an existing cache index for the specified byte string without creating a new entry. *