Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
9e1114c
feat: Add negated regex match (!~) operator to interpreter
fglock Feb 18, 2026
80fab8b
fix: Context propagation for logical operators and regex matches
fglock Feb 18, 2026
6d4f8fc
feat: Implement last/next/redo operators in interpreter with label su…
fglock Feb 18, 2026
67c1143
fix: Prevent last/next/redo in do-while loops in interpreter
fglock Feb 18, 2026
08856dc
fix: Preserve RUNTIME context for RHS of logical operators
fglock Feb 18, 2026
42bcde5
feat: Escape invalid quantifier braces in regex patterns (JVM compile…
fglock Feb 18, 2026
00518a9
docs: Document interpreter mode regex brace issue
fglock Feb 18, 2026
a2a4da9
fix: Evaluate while/if conditions in SCALAR context in interpreter mode
fglock Feb 18, 2026
583578a
fix: Handle ScalarSpecialVariable in interpreter using addToScalar
fglock Feb 18, 2026
306c8c2
fix: Use STRING bitwise operators by default in interpreter
fglock Feb 18, 2026
e050511
fix: Return undef for non-participating regex captures in LIST context
fglock Feb 18, 2026
badeb61
fix: Disable escapeInvalidQuantifierBraces to restore test compatibility
fglock Feb 18, 2026
fe6ee44
remove: Delete unescaped_braces.t test (feature disabled)
fglock Feb 18, 2026
a078172
Revert "fix: Return undef for non-participating regex captures in LIS…
fglock Feb 18, 2026
1a0346e
Reapply "fix: Return undef for non-participating regex captures in LI…
fglock Feb 18, 2026
5a0c340
Revert "Reapply "fix: Return undef for non-participating regex captur…
fglock Feb 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion dev/interpreter/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# PerlOnJava Interpreter Developer Guide

- name all test files /tmp/test.pl
- name all test files /tmp/test.pl - this makes it easier to authorize to run the tests

## Quick Reference

Expand Down
194 changes: 194 additions & 0 deletions dev/prompts/20260218_context_propagation_fixes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
# Context Propagation Fixes for Logical Operators

**Date:** 2026-02-18
**Status:** ✓ Complete

## Problem

Logical operators and control flow constructs were not properly evaluating their operands in SCALAR context, causing incorrect behavior in postfix `if/unless` statements and other boolean contexts.

### Example Issues

```perl
# Postfix if with regex - was not working in interpreter mode
say "ok" if $x =~ /pattern/;

# Logical NOT with regex - was not working
say "ok" if !($x =~ /pattern/);

# Ternary operator - was not working correctly
say ($x =~ /pattern/) ? "match" : "no match";
```

## Root Cause

### Interpreter Path (BytecodeCompiler)
The BytecodeCompiler was evaluating operands of logical operators without setting the context to SCALAR. This meant:

1. In postfix `if`, the condition was evaluated in VOID context (ctx=0) instead of SCALAR context (ctx=1)
2. The regex match was not being evaluated for its boolean value
3. This only affected the interpreter bytecode path

### JVM Path (EmitterVisitor)
The EmitLogicalOperator was preserving RUNTIME context for operands when the outer context was RUNTIME. When RUNTIME context is used, the actual wantarray value is loaded at runtime, which can be VOID (0). This caused:

1. Logical operators to pass RUNTIME context to their operands
2. When the final statement is a postfix `if`, the wantarray is 0 (VOID)
3. Regex matches were being called with ctx=0 instead of ctx=1

## Solution

### 1. Fixed Logical AND/OR Operators (BytecodeCompiler.java)

Added context save/restore for `&&`, `||`, `//` operators:

```java
// Compile left operand in scalar context (need boolean value)
int savedContext = currentCallContext;
currentCallContext = RuntimeContextType.SCALAR;
node.left.accept(this);
int rs1 = lastResultReg;
currentCallContext = savedContext;
```

**Files changed:**
- Line 3363-3370: `&&` and `and` operators
- Line 3404-3411: `||` and `or` operators
- Line 3443-3450: `//` operator

### 2. Fixed Logical NOT Operators (BytecodeCompiler.java)

Added context save/restore for `!` and `not` operators:

```java
// Evaluate operand in scalar context (need boolean value)
int savedContext = currentCallContext;
currentCallContext = RuntimeContextType.SCALAR;
node.operand.accept(this);
int rs = lastResultReg;
currentCallContext = savedContext;
```

**Files changed:**
- Line 4168-4176: `!` and `not` operators

### 3. Fixed Ternary Operator (BytecodeCompiler.java)

Added context save/restore for condition evaluation:

```java
// Compile condition in scalar context (need boolean value)
int savedContext = currentCallContext;
currentCallContext = RuntimeContextType.SCALAR;
node.condition.accept(this);
int condReg = lastResultReg;
currentCallContext = savedContext;
```

**Files changed:**
- Line 6498-6505: Ternary operator `? :`

### 4. Fixed Regex Match in LIST Context (RuntimeRegex.java)

In Perl, a successful regex match with no captures returns (1) in LIST context, not an empty list:

```java
if (ctx == RuntimeContextType.LIST) {
// In LIST context: return captured groups, or (1) for success with no captures (non-global)
if (found && result.elements.isEmpty() && !regex.regexFlags.isGlobalMatch()) {
// Non-global match with no captures in LIST context returns (1)
result.elements.add(RuntimeScalarCache.getScalarInt(1));
}
return result;
}
```

**Files changed:**
- Line 543-549: matchRegexDirect return logic

### 5. Fixed EmitterVisitor RUNTIME Context (EmitLogicalOperator.java)

The logical operators were preserving RUNTIME context for their operands, which caused the actual wantarray value (often VOID=0) to be used instead of SCALAR context:

```java
// OLD: Preserved RUNTIME context
int operandContext = emitterVisitor.ctx.contextType == RuntimeContextType.RUNTIME
? RuntimeContextType.RUNTIME
: RuntimeContextType.SCALAR;

// NEW: Always use SCALAR context for boolean evaluation
int operandContext = RuntimeContextType.SCALAR;
```

**Files changed:**
- Line 315-317: Removed RUNTIME context preservation in emitLogicalOperatorSimple

This fix ensures that even when the outer context is RUNTIME, logical operators evaluate their operands in SCALAR context to get boolean values.

## Testing

### Before Fix
```perl
# Interpreter mode (eval STRING)
eval q{
my $x = "test";
say "ok" if $x =~ /test/; # No output (WRONG)
};
```

### After Fix
```perl
# Both JVM and interpreter modes now work
my $x = "test";
say "ok" if $x =~ /test/; # Prints "ok" ✓
say "ok" if !($x =~ /fail/); # Prints "ok" ✓
say "ok" if not ($x =~ /fail/); # Prints "ok" ✓
say ($x =~ /test/) ? "yes" : "no"; # Prints "yes" ✓

# LIST context now returns (1) for matches with no captures
say "Match: ", ($x =~ /test/); # Prints "Match: 1" ✓
```

### Test Results
- All unit tests passing ✓
- Postfix if/unless working ✓
- Logical operators working ✓
- Ternary operator working ✓
- Regex LIST context working ✓

## Files Modified

1. `src/main/java/org/perlonjava/interpreter/BytecodeCompiler.java`
- Fixed `&&`, `||`, `//` operators to evaluate operands in SCALAR context
- Fixed `!`, `not` operators to evaluate operands in SCALAR context
- Fixed ternary `? :` operator to evaluate condition in SCALAR context

2. `src/main/java/org/perlonjava/codegen/EmitLogicalOperator.java`
- Fixed logical operators to always use SCALAR context for operands, even when outer context is RUNTIME

3. `src/main/java/org/perlonjava/regex/RuntimeRegex.java`
- Fixed regex match to return (1) in LIST context for non-global matches with no captures

## Key Lessons

1. **Context matters**: Logical operators must evaluate their operands in SCALAR context to get boolean values
2. **Two code paths**: Changes need to be made in both EmitterVisitor (JVM bytecode) and BytecodeCompiler (interpreter bytecode)
3. **RUNTIME context trap**: When outer context is RUNTIME, the actual wantarray value is loaded at runtime, which can be VOID. Logical operators must explicitly use SCALAR context, not preserve RUNTIME.
4. **Perl semantics**: Regex matches in LIST context return (1) for success when there are no captures (non-global)
5. **Pattern**: The context save/restore pattern is:
```java
int savedContext = currentCallContext;
currentCallContext = RuntimeContextType.SCALAR;
node.operand.accept(this);
currentCallContext = savedContext;
```
6. **Last statement issue**: When the postfix if is the last statement in a program, the outer context is RUNTIME (not VOID), which exposed the RUNTIME context bug in EmitterVisitor

## Known Issues

There appears to be a separate, very specific issue with certain regex patterns containing octal escapes after 4 or more repeated characters (e.g., `"bbbb\337e" =~ /bbbb\337e/` fails). This is unrelated to the context propagation fixes and requires separate investigation.

## References

- Previous work: `dev/prompts/20260218_interpreter_negated_regex_match.md`
- Skill guide: `dev/interpreter/SKILL.md`
117 changes: 117 additions & 0 deletions dev/prompts/20260218_interpreter_negated_regex_match.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# Interpreter: Added Support for Negated Regex Match (!~) Operator

**Date:** 2026-02-18
**Status:** ✓ Complete

## Problem

The test `perl5_t/t/re/charset.t` was failing when run with `JPERL_EVAL_USE_INTERPRETER=1` due to missing support for the `!~` (negated regex match) operator in the interpreter's bytecode compiler.

### Discovery Process

The error was hidden inside eval blocks. Added temporary debug output to expose the actual exception:

```
Unsupported operator: !~ at (eval 345) line 1, near ") /x"
```

The error occurred in `BytecodeCompiler.compileBinaryOperatorSwitch` at line 2998 (the default case that throws for unsupported operators).

## Solution

Implemented the `!~` operator following the SKILL.md guide for adding new operators:

### 1. Added Opcode Definition (Opcodes.java)
```java
public static final short MATCH_REGEX_NOT = 217;
```

### 2. Added Compiler Support (BytecodeCompiler.java)
Added case for `!~` in `compileBinaryOperatorSwitch`:
```java
case "!~" -> {
// $string !~ /pattern/ - negated regex match
emit(Opcodes.MATCH_REGEX_NOT);
emitReg(rd);
emitReg(rs1);
emitReg(rs2);
emit(currentCallContext);
}
```

### 3. Added Runtime Implementation (BytecodeInterpreter.java)
```java
case Opcodes.MATCH_REGEX_NOT: {
// Negated regex match: rd = !RuntimeRegex.matchRegex(...)
int rd = bytecode[pc++];
int stringReg = bytecode[pc++];
int regexReg = bytecode[pc++];
int ctx = bytecode[pc++];
RuntimeBase matchResult = org.perlonjava.regex.RuntimeRegex.matchRegex(
(RuntimeScalar) registers[regexReg],
(RuntimeScalar) registers[stringReg],
ctx
);
// Negate the boolean result
registers[rd] = new RuntimeScalar(matchResult.scalar().getBoolean() ? 0 : 1);
break;
}
```

### 4. Added Disassembly Support (InterpretedCode.java)
```java
case Opcodes.MATCH_REGEX_NOT:
rd = bytecode[pc++];
strReg = bytecode[pc++];
regReg = bytecode[pc++];
matchCtx = bytecode[pc++];
sb.append("MATCH_REGEX_NOT r").append(rd).append(" = r").append(strReg)
.append(" !~ r").append(regReg).append(" (ctx=").append(matchCtx).append(")\n");
break;
```

## Testing

### Before Fix
Tests 3, 4, 7, 8, 11, 12, 15, 16, etc. were failing with:
```
not ok 3 - my $a = "\t"; $a !~ qr/ (?a: \S ) /x; "\t" is not a \S under /a
```

### After Fix
All tests using `!~` operator now pass:
```
ok 3 - my $a = "\t"; $a !~ qr/ (?a: \S ) /x; "\t" is not a \S under /a
ok 4 - my $a = "\t" x 10; $a !~ qr/ (?a: \S{10} ) /x; "\t" is not a \S under /a
...
```

### Test Results
- Total tests: 5552
- Remaining failures: 270 (unrelated to `!~` operator)
- Unit tests: ✓ All passing

The remaining 270 failures are related to other issues (word boundaries, Unicode character classes) and not the `!~` operator implementation.

## Files Modified

1. `src/main/java/org/perlonjava/interpreter/Opcodes.java` - Added MATCH_REGEX_NOT = 217
2. `src/main/java/org/perlonjava/interpreter/BytecodeCompiler.java` - Added compiler case
3. `src/main/java/org/perlonjava/interpreter/BytecodeInterpreter.java` - Added runtime handler
4. `src/main/java/org/perlonjava/interpreter/InterpretedCode.java` - Added disassembly case

## Key Lessons

1. **Error Hiding in Eval**: Errors inside `eval` blocks are caught and set to `$@`, making debugging difficult without temporarily adding logging.

2. **Follow SKILL.md**: The guide in `dev/interpreter/SKILL.md` provides excellent step-by-step instructions for adding operators.

3. **Disassembly is Critical**: Missing disassembly cases cause PC misalignment and corrupt subsequent bytecode instructions.

4. **Type Conversion**: `RuntimeRegex.matchRegex()` returns `RuntimeBase`, not `RuntimeScalar`, so `.scalar()` must be called before `.getBoolean()`.

## References

- SKILL.md: `dev/interpreter/SKILL.md`
- Test file: `perl5_t/t/re/charset.t`
- Related opcodes: MATCH_REGEX (167), MATCH_REGEX_NOT (217)
2 changes: 2 additions & 0 deletions dev/prompts/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

This document captures key knowledge about PerlOnJava internals learned during debugging sessions.

- name all test files /tmp/test.pl - this makes it easier to authorize to run the tests

## Variable Storage and Scoping

### Three Types of Variable Declarations
Expand Down
Loading