From 2e4d10d3aabce349ba3de8a89036aab81e968dac Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Tue, 6 Jan 2026 17:31:39 +0100 Subject: [PATCH 01/51] WIP: stack-safe spills for control-flow propagation --- dev/design/CONTROL_FLOW_FIX_RESULTS.md | 162 ++++++++++++ dev/prompts/CONTROL_FLOW_FIX_INSTRUCTIONS.md | 197 +++++++++++++++ skip_control_flow.t | 185 ++++++++++++++ .../org/perlonjava/codegen/Dereference.java | 130 +++++++++- .../codegen/EmitBinaryOperator.java | 157 ++++++++++-- .../perlonjava/codegen/EmitControlFlow.java | 25 +- .../org/perlonjava/codegen/EmitOperator.java | 23 +- .../perlonjava/codegen/EmitSubroutine.java | 236 +++++++++++------- .../org/perlonjava/codegen/EmitVariable.java | 25 ++ .../perlonjava/codegen/EmitterContext.java | 4 + .../codegen/EmitterMethodCreator.java | 149 ++++++++++- .../org/perlonjava/codegen/JavaClassInfo.java | 18 ++ 12 files changed, 1181 insertions(+), 130 deletions(-) create mode 100644 dev/design/CONTROL_FLOW_FIX_RESULTS.md create mode 100644 dev/prompts/CONTROL_FLOW_FIX_INSTRUCTIONS.md create mode 100644 skip_control_flow.t diff --git a/dev/design/CONTROL_FLOW_FIX_RESULTS.md b/dev/design/CONTROL_FLOW_FIX_RESULTS.md new file mode 100644 index 000000000..036a5da74 --- /dev/null +++ b/dev/design/CONTROL_FLOW_FIX_RESULTS.md @@ -0,0 +1,162 @@ +# Control Flow Fix: Implementation Results and Findings + +**Date:** January 6, 2026 +**Related:** `CONTROL_FLOW_FINAL_STATUS.md`, `CONTROL_FLOW_FINAL_STEPS.md` + +## Summary + +This document captures the results of implementing the control-flow fix for tagged returns (`last LABEL`, `next LABEL`, `redo LABEL`, `goto LABEL`) and the subsequent work to resolve ASM `Frame.merge` crashes caused by non-empty JVM operand stacks at merge points. + +## Phase 1: Tagged Returns (Completed ✓) + +**Goal:** Enable `last LABEL`, `next LABEL`, `redo LABEL`, `goto LABEL` to work across subroutine boundaries. + +**Implementation:** +- Introduced `RuntimeControlFlowList` with `ControlFlowMarker` to carry control-flow metadata through the call stack +- Modified `EmitSubroutine` to check every subroutine return for control-flow markers and dispatch to appropriate loop labels or propagate upward +- Updated `EmitControlFlow` to emit markers for non-local jumps (when target label is not in current scope) + +**Result:** ✓ **SUCCESS** +- All 11 tests in `skip_control_flow.t` pass +- Tagged returns work correctly across nested subroutines and loops + +## Phase 2: ASM Frame.merge Crashes (In Progress) + +**Problem:** After implementing tagged returns, `pack.t` (which loads `Data::Dumper` and `Test2::API`) started failing with: +``` +java.lang.ArrayIndexOutOfBoundsException: Index 1 out of bounds for length 1 + at org.objectweb.asm.Frame.merge(Frame.java:1280) +``` + +**Root Cause:** +The JVM operand stack was non-empty at certain `GOTO` instructions that jump to merge points (labels with multiple incoming edges). ASM's frame computation requires all incoming edges to a merge point to have compatible stack heights. + +### Approach 1: Spill-to-Local with Preallocated Pool (Current) + +**Strategy:** Before evaluating subexpressions that can produce non-local control flow (subroutine calls, operators), spill intermediate operands to local variables, keeping the JVM operand stack empty. + +**Implementation:** +1. **Preallocated Spill-Slot Pool:** + - Added `spillSlots[]` array to `JavaClassInfo`, preallocated in `EmitterMethodCreator` + - Default: 16 slots (configurable via `JPERL_SPILL_SLOTS` env var) + - `acquireSpillSlot()` / `releaseSpillSlot()` manage the pool + - Fallback to `allocateLocalVariable()` if pool exhausted + +2. **Applied Spills:** + - **String concatenation** (`EmitOperator.emitConcatenation`): spill LHS before evaluating RHS + - **Binary operators** (`EmitBinaryOperator`): spill LHS before evaluating RHS (for operators that can trigger control flow) + - **Scalar assignment** (`EmitVariable.handleAssignOperator`): spill RHS value before evaluating LHS + +3. **Control Switch:** + - `JPERL_NO_SPILL_BINARY_LHS=1` disables spills (for A/B testing) + - Default: spills enabled + +**Advantages:** +- ✓ By-construction invariant: operand stack is empty when we evaluate potentially-escaping subexpressions +- ✓ No dependency on `TempLocalCountVisitor` sizing (uses fixed preallocated pool) +- ✓ Deterministic and predictable +- ✓ Works with ASM's existing frame computation + +**Limitations:** +- ⚠ May not cover all edge cases (still finding failure sites in `pack.t`) +- ⚠ Could theoretically exhaust the spill-slot pool on very deeply nested expressions (though 16 slots should be sufficient for typical code) + +**Status:** In progress. Most common patterns covered, but `pack.t` still fails on some edge cases. + +### Approach 2: AnalyzerAdapter-Based Stack Cleanup (Abandoned) + +**Strategy:** Wrap the generated `apply()` method's `MethodVisitor` with ASM's `AnalyzerAdapter`, which tracks the operand stack linearly. Before each non-local `GOTO`, emit `POP/POP2` instructions to empty the stack based on `AnalyzerAdapter.stack`. + +**Implementation Attempted:** +1. Added `asm-commons` dependency +2. Wrapped `apply()` method visitor with `AnalyzerAdapter` in `EmitterMethodCreator` +3. Added `JavaClassInfo.emitPopOperandStackToEmpty(mv)` to emit POPs based on adapter's stack +4. Called `emitPopOperandStackToEmpty()` before all `GOTO returnLabel` and loop label jumps + +**Why It Failed:** +- ❌ `AnalyzerAdapter` tracks the stack **linearly** during emission, not across control-flow merges +- ❌ At a `GOTO L`, the adapter only knows the stack state on the **current linear path** +- ❌ It cannot know what stack state other predecessor paths will have when they reach `L` +- ❌ Result: we can "pop to empty" on one path, but another path might still arrive at the same label with items on the stack → incompatible stack heights at merge + +**Fundamental Limitation:** +`AnalyzerAdapter` is not a full control-flow dataflow analyzer. It cannot guarantee the invariant "all incoming edges to a merge point have the same stack height" because it doesn't compute merged states during emission. + +**Conclusion:** This approach cannot work without a full two-pass compiler (emit bytecode, analyze with `Analyzer`, rewrite to insert POPs on all incoming edges). + +### Approach 3: Full Control-Flow Analysis + Rewrite (Not Attempted) + +**Strategy:** +1. Generate bytecode normally +2. Run ASM's `Analyzer` to compute stack heights at every instruction (including merges) +3. Rewrite the method to insert `POP/POP2` on all incoming edges to merge points as needed + +**Advantages:** +- ✓ Would be truly systematic and handle all cases +- ✓ No need for manual spilling or stack tracking during emission + +**Disadvantages:** +- ❌ Requires a full additional compiler pass +- ❌ Complex rewriting logic +- ❌ May be overkill for this problem + +**Status:** Not pursued. The spill-slot pool approach is simpler and should be sufficient. + +## Current Status + +**Working:** +- ✓ Tagged returns (`last LABEL`, `next LABEL`, `redo LABEL`, `goto LABEL`) across subroutine boundaries +- ✓ `skip_control_flow.t` (11/11 tests pass) +- ✓ Spill-slot pool infrastructure in place +- ✓ Spills applied to concat, binary operators, scalar assignment + +**In Progress:** +- ⚠ `pack.t` still fails with ASM frame merge errors at `anon453` instructions 104/108 +- Root cause: When a subroutine call (inside an expression) returns a control-flow marker, the propagation logic tries to jump to `returnLabel`, but there are values on the JVM operand stack from the outer expression context +- Current `stackLevelManager` doesn't track the actual JVM operand stack during expression evaluation - it only tracks "logical" stack levels (loop nesting, etc.) + +**Two Possible Solutions:** + +### Solution A: Comprehensive Stack Tracking (Preferred) +Track every JVM operand stack operation throughout expression evaluation: +- Add `increment(1)` after every operation that pushes to stack +- Add `decrement(1)` after every operation that pops from stack +- This would make `stackLevelManager.getStackLevel()` accurately reflect the JVM operand stack depth +- Then `stackLevelManager.emitPopInstructions(mv, 0)` would correctly clean the stack before control-flow propagation + +**Requires changes to:** +- All binary operators +- All method calls +- All local variable stores/loads +- All expression evaluation sites + +### Solution B: Targeted Spills (Current Approach) +Continue applying spills to ensure stack is empty before subroutine calls: +- Already applied to: concat, binary ops, scalar assignment +- Still need to identify remaining patterns where subroutine calls happen with non-empty stack + +**Next Steps:** +1. Decide between Solution A (comprehensive tracking) vs Solution B (targeted spills) +2. If Solution A: Implement stack tracking in binary operators and expression evaluation +3. If Solution B: Continue identifying and fixing specific failure patterns +4. Re-test `pack.t` until it passes +5. Run full regression suite +6. Prepare PR + +## Lessons Learned + +1. **By-construction invariants are more reliable than runtime tracking** when dealing with ASM frame computation. + +2. **`AnalyzerAdapter` is not sufficient for merge-point analysis** — it only tracks linear paths during emission. + +3. **Preallocated resource pools** (spill slots) are better than dynamic allocation (`TempLocalCountVisitor` + buffer) for avoiding VerifyError and frame computation issues. + +4. **The spill approach is not "whack-a-mole"** if we apply it systematically to all expression evaluation sites that can trigger non-local control flow (subroutine calls, operators that can return marked lists). + +5. **Environment variable switches** (`JPERL_NO_SPILL_BINARY_LHS`) are valuable for A/B testing and debugging. + +## References + +- `CONTROL_FLOW_FINAL_STATUS.md` - Original design for tagged returns +- `CONTROL_FLOW_FINAL_STEPS.md` - Implementation steps +- `ASM_FRAME_COMPUTATION_BLOCKER.md` - Earlier notes on frame computation issues diff --git a/dev/prompts/CONTROL_FLOW_FIX_INSTRUCTIONS.md b/dev/prompts/CONTROL_FLOW_FIX_INSTRUCTIONS.md new file mode 100644 index 000000000..2bf9bf759 --- /dev/null +++ b/dev/prompts/CONTROL_FLOW_FIX_INSTRUCTIONS.md @@ -0,0 +1,197 @@ +# Control Flow Fix Instructions + +## Problem Statement + +The Perl control flow for labeled blocks with `last LABEL` is broken in scalar context when called through multiple stack frames. This affects Test::More's `skip()` function and other labeled block control flow. + +## Current Status + +### What Works +- Single frame control flow: `last LABEL` works when called directly in the same frame +- Void context: `last LABEL` works through multiple frames in void context +- Test::More has been updated to use `last SKIP` directly (no TestMoreHelper workaround) + +### What's Broken +- **Scalar context control flow**: `last LABEL` fails when called through 2+ frames in scalar context +- Tests 2, 5, 8 in `skip_control_flow.t` demonstrate this failure + +## Baseline Expectations + +Based on logs from 2026-01-06 09:27: +- **op/pack.t**: 14579/14726 tests passing +- **uni/variables.t**: 66683/66880 tests passing +- **op/lc.t**: 2710/2716 tests passing + +Current state shows regressions: +- **op/pack.t**: 245/14726 (regression of -14334 tests) +- **uni/variables.t**: 56/66880 (regression of -66627 tests) + +## Test Files + +### Unit Test +`src/test/resources/unit/skip_control_flow.t` - 11 tests demonstrating control flow issues + +**Backup location**: `/tmp/skip_control_flow.t.backup` + +Run with: +```bash +./jperl src/test/resources/unit/skip_control_flow.t +``` + +Expected results: +- Tests 1, 3, 4, 6, 7, 9, 10, 11: PASS +- **Tests 2, 5, 8: FAIL** (scalar context issue - these demonstrate the problem) + +### Integration Tests + +Test `uni/variables.t`: +```bash +cd perl5_t/t && ../../jperl uni/variables.t 2>&1 | grep "Looks like" +# Should show: planned 66880, ran 66880 (or close to it) +# Currently shows: planned 66880, ran 56 +``` + +Test `op/pack.t`: +```bash +cd perl5_t/t && ../../jperl op/pack.t 2>&1 | grep "Looks like" +# Should show: planned 14726, ran ~14579 +# Currently shows: planned 14726, ran 249 +``` + +Use test runner for batch testing: +```bash +perl dev/tools/perl_test_runner.pl perl5_t/t/op/pack.t perl5_t/t/uni/variables.t +``` + +## Build Process + +**CRITICAL**: Always build with `make` and wait for completion: + +```bash +make +# Wait for "BUILD SUCCESSFUL" message +# Do NOT interrupt the build +``` + +The `make` command runs: +- `./gradlew classes testUnitParallel --parallel shadowJar` +- Compiles Java code +- Runs unit tests +- Builds the JAR file + +**Do NOT use**: +- `./gradlew shadowJar` alone (skips tests) +- `./gradlew clean shadowJar` (unless specifically needed) + +## Development Workflow + +### 1. BEFORE Making Changes + +Add failing tests to `skip_control_flow.t` that demonstrate the problem: + +```perl +# Example: Add a test that shows the issue +# Test X) Description of what should work but doesn't +{ + my $out = ''; + sub test_func { last SOMELABEL } + SOMELABEL: { + $out .= 'A'; + my $result = test_func(); # Scalar context + $out .= 'B'; + } + $out .= 'C'; + ok_tap($out eq 'AC', 'description of expected behavior'); +} +``` + +Run the test to confirm it fails: +```bash +./jperl src/test/resources/unit/skip_control_flow.t +``` + +### 2. Make Your Changes + +Focus areas: +- `src/main/java/org/perlonjava/codegen/EmitBlock.java` - Block code generation +- `src/main/java/org/perlonjava/runtime/RuntimeControlFlowRegistry.java` - Control flow marker registry +- Loop constructs in `EmitForeach.java`, `EmitStatement.java` + +### 3. Build and Test + +```bash +# Build (wait for completion) +make + +# Test unit tests +./jperl src/test/resources/unit/skip_control_flow.t + +# Test integration +cd perl5_t/t && ../../jperl uni/variables.t 2>&1 | grep "Looks like" +cd perl5_t/t && ../../jperl op/pack.t 2>&1 | grep "Looks like" +``` + +### 4. Verify No Regressions + +Compare test counts to baseline: +- `uni/variables.t`: Should run ~66683/66880 tests (not stop at 56) +- `op/pack.t`: Should run ~14579/14726 tests (not stop at 245) +- `skip_control_flow.t`: All 11 tests should pass + +## Key Technical Details + +### Control Flow Registry + +`RuntimeControlFlowRegistry` manages non-local control flow markers: +- `register(marker)` - Sets a control flow marker +- `hasMarker()` - Checks if marker exists +- `checkLoopAndGetAction(label)` - Checks and clears matching marker +- `markerMatchesLabel(label)` - Checks if marker matches without clearing +- `clear()` - Clears the marker + +### The Problem + +When `last LABEL` is called in scalar context through multiple frames: +1. The marker is registered correctly +2. The registry check happens +3. But the control flow doesn't properly exit the labeled block +4. This causes tests to continue executing when they should stop + +### Previous Attempts + +Several approaches were tried and caused regressions: +1. Adding registry checks in `EmitBlock.java` after each statement + - Caused `op/pack.t` to stop at test 245 +2. Unconditional registry clearing at block exit + - Caused `op/pack.t` to stop at test 245 +3. Conditional registry clearing (only if marker matches) + - Still caused `op/pack.t` to stop at test 245 + +The issue is that bare labeled blocks (not actual loops) need special handling. + +## Success Criteria + +1. All 11 tests in `skip_control_flow.t` pass (including tests 2, 5, 8) +2. `uni/variables.t` runs to completion (~66683/66880 tests) +3. `op/pack.t` runs to completion (~14579/14726 tests) +4. No new test failures introduced + +## Files to Review + +- `src/main/java/org/perlonjava/codegen/EmitBlock.java` +- `src/main/java/org/perlonjava/runtime/RuntimeControlFlowRegistry.java` +- `src/main/java/org/perlonjava/codegen/EmitForeach.java` +- `src/main/java/org/perlonjava/codegen/EmitStatement.java` +- `src/main/perl/lib/Test/More.pm` +- `src/test/resources/unit/skip_control_flow.t` + +## Notes + +- Work in a branch +- The JAR builds correctly - test results are real, not build issues +- The baseline (14579/14726 for op/pack.t) may be from a specific branch/commit +- Focus on making the unit tests pass first, then verify integration tests +- Real loops (for/while/foreach) have their own registry checks that work correctly +- The issue is specific to bare labeled blocks in scalar context + +Good luck! diff --git a/skip_control_flow.t b/skip_control_flow.t new file mode 100644 index 000000000..c5ddf0ee3 --- /dev/null +++ b/skip_control_flow.t @@ -0,0 +1,185 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +# Minimal TAP without Test::More (we need this to work even when skip()/TODO are broken) +my $t = 0; +sub ok_tap { + my ($cond, $name) = @_; + $t++; + print(($cond ? "ok" : "not ok"), " $t - $name\n"); +} + +# 1) Single frame - MYLABEL +{ + my $out = ''; + sub test_once { last MYLABEL } + MYLABEL: { + $out .= 'A'; + test_once(); + $out .= 'B'; + } + $out .= 'C'; + ok_tap($out eq 'AC', 'last MYLABEL exits MYLABEL block (single frame)'); +} + +# 2) Two frames, scalar context - MYLABEL +{ + my $out = ''; + sub inner2 { last MYLABEL } + sub outer2 { my $x = inner2(); return $x; } + MYLABEL: { + $out .= 'A'; + my $r = outer2(); + $out .= 'B'; + } + $out .= 'C'; + ok_tap($out eq 'AC', 'last MYLABEL exits MYLABEL block (2 frames, scalar context)'); +} + +# 3) Two frames, void context - MYLABEL +{ + my $out = ''; + sub innerv { last MYLABEL } + sub outerv { innerv(); } + MYLABEL: { + $out .= 'A'; + outerv(); + $out .= 'B'; + } + $out .= 'C'; + ok_tap($out eq 'AC', 'last MYLABEL exits MYLABEL block (2 frames, void context)'); +} + +# 4) Single frame - LABEL2 +{ + my $out = ''; + sub test2_once { last LABEL2 } + LABEL2: { + $out .= 'A'; + test2_once(); + $out .= 'B'; + } + $out .= 'C'; + ok_tap($out eq 'AC', 'last LABEL2 exits LABEL2 block (single frame)'); +} + +# 5) Two frames, scalar context - LABEL2 +{ + my $out = ''; + sub inner_label2 { last LABEL2 } + sub outer_label2 { my $x = inner_label2(); return $x; } + LABEL2: { + $out .= 'A'; + my $r = outer_label2(); + $out .= 'B'; + } + $out .= 'C'; + ok_tap($out eq 'AC', 'last LABEL2 exits LABEL2 block (2 frames, scalar context)'); +} + +# 6) Two frames, void context - LABEL2 +{ + my $out = ''; + sub innerv_label2 { last LABEL2 } + sub outerv_label2 { innerv_label2(); } + LABEL2: { + $out .= 'A'; + outerv_label2(); + $out .= 'B'; + } + $out .= 'C'; + ok_tap($out eq 'AC', 'last LABEL2 exits LABEL2 block (2 frames, void context)'); +} + +# 7) Single frame - LABEL3 +{ + my $out = ''; + sub test3_once { last LABEL3 } + LABEL3: { + $out .= 'A'; + test3_once(); + $out .= 'B'; + } + $out .= 'C'; + ok_tap($out eq 'AC', 'last LABEL3 exits LABEL3 block (single frame)'); +} + +# 8) Two frames, scalar context - LABEL3 +{ + my $out = ''; + sub inner_label3 { last LABEL3 } + sub outer_label3 { my $x = inner_label3(); return $x; } + LABEL3: { + $out .= 'A'; + my $r = outer_label3(); + $out .= 'B'; + } + $out .= 'C'; + ok_tap($out eq 'AC', 'last LABEL3 exits LABEL3 block (2 frames, scalar context)'); +} + +# 9) Two frames, void context - LABEL3 +{ + my $out = ''; + sub innerv_label3 { last LABEL3 } + sub outerv_label3 { innerv_label3(); } + LABEL3: { + $out .= 'A'; + outerv_label3(); + $out .= 'B'; + } + $out .= 'C'; + ok_tap($out eq 'AC', 'last LABEL3 exits LABEL3 block (2 frames, void context)'); +} + +# 10) Stale marker bug - labeled block in eval leaves marker +{ + my $out = ''; + # This eval creates a labeled block that might leave a stale marker + eval "\${\x{30cd}single:\x{30cd}colon} = 'test'"; + $out .= 'A'; + + # This SKIP block should work normally, not be affected by stale marker + MYLABEL: { + $out .= 'B'; + $out .= 'C'; + } + $out .= 'D'; + ok_tap($out eq 'ABCD', 'labeled block in eval does not leave stale marker'); +} + +# 11) Registry clearing bug - large SKIP block (>3 statements) with skip() +{ + my $out = ''; + my $count = 0; + + # SKIP block with >3 statements (so registry check won't run inside) + # But registry clearing at exit WILL run + SKIP: { + my $a = 1; # statement 1 + my $b = 2; # statement 2 + my $c = 3; # statement 3 + my $d = 4; # statement 4 + $out .= 'S'; + last SKIP; # This sets a marker, but block has >3 statements so no check + $out .= 'X'; + } + # When SKIP exits, registry is cleared unconditionally + # This removes the marker that was correctly set by last SKIP + + $out .= 'A'; + + # This loop should run 3 times + for my $i (1..3) { + INNER: { + $out .= 'L'; + $count++; + } + } + + $out .= 'B'; + ok_tap($out eq 'SALLLB' && $count == 3, 'large SKIP block does not break subsequent loops'); +} + +print "1..$t\n"; diff --git a/src/main/java/org/perlonjava/codegen/Dereference.java b/src/main/java/org/perlonjava/codegen/Dereference.java index 7a8171e8f..3133dd29f 100644 --- a/src/main/java/org/perlonjava/codegen/Dereference.java +++ b/src/main/java/org/perlonjava/codegen/Dereference.java @@ -436,10 +436,37 @@ static void handleArrowOperator(EmitterVisitor emitterVisitor, BinaryOperatorNod // Push __SUB__ handleSelfCallOperator(emitterVisitor.with(RuntimeContextType.SCALAR), null); + int objectSlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot(); + boolean pooledObject = objectSlot >= 0; + if (!pooledObject) { + objectSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); + } + mv.visitVarInsn(Opcodes.ASTORE, objectSlot); + + int methodSlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot(); + boolean pooledMethod = methodSlot >= 0; + if (!pooledMethod) { + methodSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); + } + mv.visitVarInsn(Opcodes.ASTORE, methodSlot); + + int subSlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot(); + boolean pooledSub = subSlot >= 0; + if (!pooledSub) { + subSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); + } + mv.visitVarInsn(Opcodes.ASTORE, subSlot); + // Generate native RuntimeBase[] array for parameters instead of RuntimeList ListNode paramList = ListNode.makeList(arguments); int argCount = paramList.elements.size(); + int argsArraySlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot(); + boolean pooledArgsArray = argsArraySlot >= 0; + if (!pooledArgsArray) { + argsArraySlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); + } + // Create array of RuntimeBase with size equal to number of arguments if (argCount <= 5) { mv.visitInsn(Opcodes.ICONST_0 + argCount); @@ -450,10 +477,21 @@ static void handleArrowOperator(EmitterVisitor emitterVisitor, BinaryOperatorNod } mv.visitTypeInsn(Opcodes.ANEWARRAY, "org/perlonjava/runtime/RuntimeBase"); + mv.visitVarInsn(Opcodes.ASTORE, argsArraySlot); + // Populate the array with arguments EmitterVisitor listVisitor = emitterVisitor.with(RuntimeContextType.LIST); for (int index = 0; index < argCount; index++) { - mv.visitInsn(Opcodes.DUP); // Duplicate array reference + int argSlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot(); + boolean pooledArg = argSlot >= 0; + if (!pooledArg) { + argSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); + } + + paramList.elements.get(index).accept(listVisitor); + mv.visitVarInsn(Opcodes.ASTORE, argSlot); + + mv.visitVarInsn(Opcodes.ALOAD, argsArraySlot); if (index <= 5) { mv.visitInsn(Opcodes.ICONST_0 + index); } else if (index <= 127) { @@ -461,13 +499,18 @@ static void handleArrowOperator(EmitterVisitor emitterVisitor, BinaryOperatorNod } else { mv.visitIntInsn(Opcodes.SIPUSH, index); } + mv.visitVarInsn(Opcodes.ALOAD, argSlot); + mv.visitInsn(Opcodes.AASTORE); - // Generate code for argument in LIST context - paramList.elements.get(index).accept(listVisitor); - - mv.visitInsn(Opcodes.AASTORE); // Store in array + if (pooledArg) { + emitterVisitor.ctx.javaClassInfo.releaseSpillSlot(); + } } + mv.visitVarInsn(Opcodes.ALOAD, objectSlot); + mv.visitVarInsn(Opcodes.ALOAD, methodSlot); + mv.visitVarInsn(Opcodes.ALOAD, subSlot); + mv.visitVarInsn(Opcodes.ALOAD, argsArraySlot); mv.visitLdcInsn(emitterVisitor.ctx.contextType); // push call context to stack mv.visitMethodInsn( Opcodes.INVOKESTATIC, @@ -475,6 +518,19 @@ static void handleArrowOperator(EmitterVisitor emitterVisitor, BinaryOperatorNod "call", "(Lorg/perlonjava/runtime/RuntimeScalar;Lorg/perlonjava/runtime/RuntimeScalar;Lorg/perlonjava/runtime/RuntimeScalar;[Lorg/perlonjava/runtime/RuntimeBase;I)Lorg/perlonjava/runtime/RuntimeList;", false); // generate an .call() + + if (pooledArgsArray) { + emitterVisitor.ctx.javaClassInfo.releaseSpillSlot(); + } + if (pooledSub) { + emitterVisitor.ctx.javaClassInfo.releaseSpillSlot(); + } + if (pooledMethod) { + emitterVisitor.ctx.javaClassInfo.releaseSpillSlot(); + } + if (pooledObject) { + emitterVisitor.ctx.javaClassInfo.releaseSpillSlot(); + } if (emitterVisitor.ctx.contextType == RuntimeContextType.SCALAR) { // Transform the value in the stack to RuntimeScalar emitterVisitor.ctx.mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/perlonjava/runtime/RuntimeList", "scalar", "()Lorg/perlonjava/runtime/RuntimeScalar;", false); @@ -492,12 +548,29 @@ public static void handleArrowArrayDeref(EmitterVisitor emitterVisitor, BinaryOp node.left.accept(scalarVisitor); // target - left parameter + int leftSlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot(); + boolean pooledLeft = leftSlot >= 0; + if (!pooledLeft) { + leftSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); + } + emitterVisitor.ctx.mv.visitVarInsn(Opcodes.ASTORE, leftSlot); + ArrayLiteralNode right = (ArrayLiteralNode) node.right; if (right.elements.size() == 1) { // Single index: use get/delete/exists methods Node elem = right.elements.getFirst(); elem.accept(emitterVisitor.with(RuntimeContextType.SCALAR)); + int indexSlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot(); + boolean pooledIndex = indexSlot >= 0; + if (!pooledIndex) { + indexSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); + } + emitterVisitor.ctx.mv.visitVarInsn(Opcodes.ASTORE, indexSlot); + + emitterVisitor.ctx.mv.visitVarInsn(Opcodes.ALOAD, leftSlot); + emitterVisitor.ctx.mv.visitVarInsn(Opcodes.ALOAD, indexSlot); + // Check if strict refs is enabled at compile time if (emitterVisitor.ctx.symbolTable.isStrictOptionEnabled(Strict.HINT_STRICT_REFS)) { // Use strict version (throws error on symbolic references) @@ -524,6 +597,10 @@ public static void handleArrowArrayDeref(EmitterVisitor emitterVisitor, BinaryOp emitterVisitor.ctx.mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/perlonjava/runtime/RuntimeScalar", methodName, "(Lorg/perlonjava/runtime/RuntimeScalar;Ljava/lang/String;)Lorg/perlonjava/runtime/RuntimeScalar;", false); } + + if (pooledIndex) { + emitterVisitor.ctx.javaClassInfo.releaseSpillSlot(); + } } else { // Multiple indices: use slice method (only for get operation) if (!arrayOperation.equals("get")) { @@ -534,9 +611,23 @@ public static void handleArrowArrayDeref(EmitterVisitor emitterVisitor, BinaryOp ListNode nodeRight = right.asListNode(); nodeRight.accept(emitterVisitor.with(RuntimeContextType.LIST)); + int indexListSlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot(); + boolean pooledIndexList = indexListSlot >= 0; + if (!pooledIndexList) { + indexListSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); + } + emitterVisitor.ctx.mv.visitVarInsn(Opcodes.ASTORE, indexListSlot); + + emitterVisitor.ctx.mv.visitVarInsn(Opcodes.ALOAD, leftSlot); + emitterVisitor.ctx.mv.visitVarInsn(Opcodes.ALOAD, indexListSlot); + emitterVisitor.ctx.mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/perlonjava/runtime/RuntimeScalar", "arrayDerefGetSlice", "(Lorg/perlonjava/runtime/RuntimeList;)Lorg/perlonjava/runtime/RuntimeList;", false); + if (pooledIndexList) { + emitterVisitor.ctx.javaClassInfo.releaseSpillSlot(); + } + // Context conversion: list slice in scalar/void contexts if (emitterVisitor.ctx.contextType == RuntimeContextType.SCALAR) { // Convert RuntimeList to RuntimeScalar (Perl scalar slice semantics) @@ -547,6 +638,10 @@ public static void handleArrowArrayDeref(EmitterVisitor emitterVisitor, BinaryOp } } + if (pooledLeft) { + emitterVisitor.ctx.javaClassInfo.releaseSpillSlot(); + } + EmitOperator.handleVoidContext(emitterVisitor); } @@ -557,6 +652,13 @@ public static void handleArrowHashDeref(EmitterVisitor emitterVisitor, BinaryOpe node.left.accept(scalarVisitor); // target - left parameter + int leftSlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot(); + boolean pooledLeft = leftSlot >= 0; + if (!pooledLeft) { + leftSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); + } + emitterVisitor.ctx.mv.visitVarInsn(Opcodes.ASTORE, leftSlot); + // emit the {0} as a RuntimeList ListNode nodeRight = ((HashLiteralNode) node.right).asListNode(); @@ -571,6 +673,16 @@ public static void handleArrowHashDeref(EmitterVisitor emitterVisitor, BinaryOpe emitterVisitor.ctx.logDebug("visit -> (HashLiteralNode) autoquote " + node.right); nodeRight.accept(emitterVisitor.with(RuntimeContextType.SCALAR)); + int keySlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot(); + boolean pooledKey = keySlot >= 0; + if (!pooledKey) { + keySlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); + } + emitterVisitor.ctx.mv.visitVarInsn(Opcodes.ASTORE, keySlot); + + emitterVisitor.ctx.mv.visitVarInsn(Opcodes.ALOAD, leftSlot); + emitterVisitor.ctx.mv.visitVarInsn(Opcodes.ALOAD, keySlot); + // Check if strict refs is enabled at compile time if (emitterVisitor.ctx.symbolTable.isStrictOptionEnabled(Strict.HINT_STRICT_REFS)) { // Use strict version (throws error on symbolic references) @@ -597,6 +709,14 @@ public static void handleArrowHashDeref(EmitterVisitor emitterVisitor, BinaryOpe emitterVisitor.ctx.mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/perlonjava/runtime/RuntimeScalar", methodName, "(Lorg/perlonjava/runtime/RuntimeScalar;Ljava/lang/String;)Lorg/perlonjava/runtime/RuntimeScalar;", false); } + + if (pooledKey) { + emitterVisitor.ctx.javaClassInfo.releaseSpillSlot(); + } + + if (pooledLeft) { + emitterVisitor.ctx.javaClassInfo.releaseSpillSlot(); + } EmitOperator.handleVoidContext(emitterVisitor); } } diff --git a/src/main/java/org/perlonjava/codegen/EmitBinaryOperator.java b/src/main/java/org/perlonjava/codegen/EmitBinaryOperator.java index 6a486f79d..bcbf4eab5 100644 --- a/src/main/java/org/perlonjava/codegen/EmitBinaryOperator.java +++ b/src/main/java/org/perlonjava/codegen/EmitBinaryOperator.java @@ -1,5 +1,6 @@ package org.perlonjava.codegen; +import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; import org.perlonjava.astnode.BinaryOperatorNode; import org.perlonjava.astnode.IdentifierNode; @@ -14,6 +15,8 @@ import static org.perlonjava.codegen.EmitOperator.emitOperator; public class EmitBinaryOperator { + static final boolean ENABLE_SPILL_BINARY_LHS = System.getenv("JPERL_NO_SPILL_BINARY_LHS") == null; + static void handleBinaryOperator(EmitterVisitor emitterVisitor, BinaryOperatorNode node, OperatorHandler operatorHandler) { EmitterVisitor scalarVisitor = emitterVisitor.with(RuntimeContextType.SCALAR); // execute operands in scalar context @@ -52,8 +55,27 @@ static void handleBinaryOperator(EmitterVisitor emitterVisitor, BinaryOperatorNo if (emitterVisitor.ctx.symbolTable.isStrictOptionEnabled(Strict.HINT_INTEGER)) { if (node.operator.equals("%")) { // Use integer modulus when "use integer" is in effect - node.left.accept(scalarVisitor); // left parameter - right.accept(scalarVisitor); // right parameter + MethodVisitor mv = emitterVisitor.ctx.mv; + if (ENABLE_SPILL_BINARY_LHS) { + node.left.accept(scalarVisitor); + int leftSlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot(); + boolean pooled = leftSlot >= 0; + if (!pooled) { + leftSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); + } + mv.visitVarInsn(Opcodes.ASTORE, leftSlot); + + right.accept(scalarVisitor); + + mv.visitVarInsn(Opcodes.ALOAD, leftSlot); + mv.visitInsn(Opcodes.SWAP); + if (pooled) { + emitterVisitor.ctx.javaClassInfo.releaseSpillSlot(); + } + } else { + node.left.accept(scalarVisitor); // left parameter + right.accept(scalarVisitor); // right parameter + } emitterVisitor.ctx.mv.visitMethodInsn( Opcodes.INVOKESTATIC, "org/perlonjava/operators/MathOperators", @@ -64,8 +86,27 @@ static void handleBinaryOperator(EmitterVisitor emitterVisitor, BinaryOperatorNo return; } else if (node.operator.equals("/")) { // Use integer division when "use integer" is in effect - node.left.accept(scalarVisitor); // left parameter - right.accept(scalarVisitor); // right parameter + MethodVisitor mv = emitterVisitor.ctx.mv; + if (ENABLE_SPILL_BINARY_LHS) { + node.left.accept(scalarVisitor); + int leftSlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot(); + boolean pooled = leftSlot >= 0; + if (!pooled) { + leftSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); + } + mv.visitVarInsn(Opcodes.ASTORE, leftSlot); + + right.accept(scalarVisitor); + + mv.visitVarInsn(Opcodes.ALOAD, leftSlot); + mv.visitInsn(Opcodes.SWAP); + if (pooled) { + emitterVisitor.ctx.javaClassInfo.releaseSpillSlot(); + } + } else { + node.left.accept(scalarVisitor); // left parameter + right.accept(scalarVisitor); // right parameter + } emitterVisitor.ctx.mv.visitMethodInsn( Opcodes.INVOKESTATIC, "org/perlonjava/operators/MathOperators", @@ -76,8 +117,27 @@ static void handleBinaryOperator(EmitterVisitor emitterVisitor, BinaryOperatorNo return; } else if (node.operator.equals("<<")) { // Use integer left shift when "use integer" is in effect - node.left.accept(scalarVisitor); // left parameter - right.accept(scalarVisitor); // right parameter + MethodVisitor mv = emitterVisitor.ctx.mv; + if (ENABLE_SPILL_BINARY_LHS) { + node.left.accept(scalarVisitor); + int leftSlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot(); + boolean pooled = leftSlot >= 0; + if (!pooled) { + leftSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); + } + mv.visitVarInsn(Opcodes.ASTORE, leftSlot); + + right.accept(scalarVisitor); + + mv.visitVarInsn(Opcodes.ALOAD, leftSlot); + mv.visitInsn(Opcodes.SWAP); + if (pooled) { + emitterVisitor.ctx.javaClassInfo.releaseSpillSlot(); + } + } else { + node.left.accept(scalarVisitor); // left parameter + right.accept(scalarVisitor); // right parameter + } emitterVisitor.ctx.mv.visitMethodInsn( Opcodes.INVOKESTATIC, "org/perlonjava/operators/BitwiseOperators", @@ -88,8 +148,27 @@ static void handleBinaryOperator(EmitterVisitor emitterVisitor, BinaryOperatorNo return; } else if (node.operator.equals(">>")) { // Use integer right shift when "use integer" is in effect - node.left.accept(scalarVisitor); // left parameter - right.accept(scalarVisitor); // right parameter + MethodVisitor mv = emitterVisitor.ctx.mv; + if (ENABLE_SPILL_BINARY_LHS) { + node.left.accept(scalarVisitor); + int leftSlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot(); + boolean pooled = leftSlot >= 0; + if (!pooled) { + leftSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); + } + mv.visitVarInsn(Opcodes.ASTORE, leftSlot); + + right.accept(scalarVisitor); + + mv.visitVarInsn(Opcodes.ALOAD, leftSlot); + mv.visitInsn(Opcodes.SWAP); + if (pooled) { + emitterVisitor.ctx.javaClassInfo.releaseSpillSlot(); + } + } else { + node.left.accept(scalarVisitor); // left parameter + right.accept(scalarVisitor); // right parameter + } emitterVisitor.ctx.mv.visitMethodInsn( Opcodes.INVOKESTATIC, "org/perlonjava/operators/BitwiseOperators", @@ -101,8 +180,27 @@ static void handleBinaryOperator(EmitterVisitor emitterVisitor, BinaryOperatorNo } } - node.left.accept(scalarVisitor); // left parameter - right.accept(scalarVisitor); // right parameter + MethodVisitor mv = emitterVisitor.ctx.mv; + if (ENABLE_SPILL_BINARY_LHS) { + node.left.accept(scalarVisitor); // left parameter + int leftSlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot(); + boolean pooled = leftSlot >= 0; + if (!pooled) { + leftSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); + } + mv.visitVarInsn(Opcodes.ASTORE, leftSlot); + + right.accept(scalarVisitor); // right parameter + + mv.visitVarInsn(Opcodes.ALOAD, leftSlot); + mv.visitInsn(Opcodes.SWAP); + if (pooled) { + emitterVisitor.ctx.javaClassInfo.releaseSpillSlot(); + } + } else { + node.left.accept(scalarVisitor); // left parameter + right.accept(scalarVisitor); // right parameter + } // stack: [left, right] emitOperator(node, emitterVisitor); } @@ -111,10 +209,39 @@ static void handleCompoundAssignment(EmitterVisitor emitterVisitor, BinaryOperat // compound assignment operators like `+=` EmitterVisitor scalarVisitor = emitterVisitor.with(RuntimeContextType.SCALAR); // execute operands in scalar context - node.left.accept(scalarVisitor); // target - left parameter - emitterVisitor.ctx.mv.visitInsn(Opcodes.DUP); - node.right.accept(scalarVisitor); // right parameter - // stack: [left, left, right] + MethodVisitor mv = emitterVisitor.ctx.mv; + if (ENABLE_SPILL_BINARY_LHS) { + node.left.accept(scalarVisitor); // target - left parameter + int leftSlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot(); + boolean pooledLeft = leftSlot >= 0; + if (!pooledLeft) { + leftSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); + } + mv.visitVarInsn(Opcodes.ASTORE, leftSlot); + + node.right.accept(scalarVisitor); // right parameter + int rightSlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot(); + boolean pooledRight = rightSlot >= 0; + if (!pooledRight) { + rightSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); + } + mv.visitVarInsn(Opcodes.ASTORE, rightSlot); + + mv.visitVarInsn(Opcodes.ALOAD, leftSlot); + mv.visitInsn(Opcodes.DUP); + mv.visitVarInsn(Opcodes.ALOAD, rightSlot); + + if (pooledRight) { + emitterVisitor.ctx.javaClassInfo.releaseSpillSlot(); + } + if (pooledLeft) { + emitterVisitor.ctx.javaClassInfo.releaseSpillSlot(); + } + } else { + node.left.accept(scalarVisitor); // target - left parameter + mv.visitInsn(Opcodes.DUP); + node.right.accept(scalarVisitor); // right parameter + } // perform the operation String baseOperator = node.operator.substring(0, node.operator.length() - 1); // Create a BinaryOperatorNode for the base operation @@ -126,7 +253,7 @@ static void handleCompoundAssignment(EmitterVisitor emitterVisitor, BinaryOperat ); EmitOperator.emitOperator(baseOpNode, scalarVisitor); // assign to the Lvalue - emitterVisitor.ctx.mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/perlonjava/runtime/RuntimeScalar", "set", "(Lorg/perlonjava/runtime/RuntimeScalar;)Lorg/perlonjava/runtime/RuntimeScalar;", false); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/perlonjava/runtime/RuntimeScalar", "set", "(Lorg/perlonjava/runtime/RuntimeScalar;)Lorg/perlonjava/runtime/RuntimeScalar;", false); EmitOperator.handleVoidContext(emitterVisitor); } diff --git a/src/main/java/org/perlonjava/codegen/EmitControlFlow.java b/src/main/java/org/perlonjava/codegen/EmitControlFlow.java index 807f67350..48a96570d 100644 --- a/src/main/java/org/perlonjava/codegen/EmitControlFlow.java +++ b/src/main/java/org/perlonjava/codegen/EmitControlFlow.java @@ -63,7 +63,7 @@ static void handleNextOperator(EmitterContext ctx, OperatorNode node) { } if (loopLabels == null) { - // Non-local control flow: register in RuntimeControlFlowRegistry and return normally + // Non-local control flow: return tagged RuntimeControlFlowList ctx.logDebug("visit(next): Non-local control flow for " + operator + " " + labelStr); // Determine control flow type @@ -71,8 +71,8 @@ static void handleNextOperator(EmitterContext ctx, OperatorNode node) { : operator.equals("last") ? ControlFlowType.LAST : ControlFlowType.REDO; - // Create ControlFlowMarker: new ControlFlowMarker(type, label, fileName, lineNumber) - ctx.mv.visitTypeInsn(Opcodes.NEW, "org/perlonjava/runtime/ControlFlowMarker"); + // Create RuntimeControlFlowList: new RuntimeControlFlowList(type, label, fileName, lineNumber) + ctx.mv.visitTypeInsn(Opcodes.NEW, "org/perlonjava/runtime/RuntimeControlFlowList"); ctx.mv.visitInsn(Opcodes.DUP); ctx.mv.visitFieldInsn(Opcodes.GETSTATIC, "org/perlonjava/runtime/ControlFlowType", @@ -89,27 +89,12 @@ static void handleNextOperator(EmitterContext ctx, OperatorNode node) { int lineNumber = ctx.errorUtil != null ? ctx.errorUtil.getLineNumber(node.tokenIndex) : 0; ctx.mv.visitLdcInsn(lineNumber); ctx.mv.visitMethodInsn(Opcodes.INVOKESPECIAL, - "org/perlonjava/runtime/ControlFlowMarker", + "org/perlonjava/runtime/RuntimeControlFlowList", "", "(Lorg/perlonjava/runtime/ControlFlowType;Ljava/lang/String;Ljava/lang/String;I)V", false); - // Register the marker: RuntimeControlFlowRegistry.register(marker) - ctx.mv.visitMethodInsn(Opcodes.INVOKESTATIC, - "org/perlonjava/runtime/RuntimeControlFlowRegistry", - "register", - "(Lorg/perlonjava/runtime/ControlFlowMarker;)V", - false); - - // Return empty list (marker is in registry, will be checked by loop) - // We MUST NOT jump to returnLabel as it breaks ASM frame computation - ctx.mv.visitTypeInsn(Opcodes.NEW, "org/perlonjava/runtime/RuntimeList"); - ctx.mv.visitInsn(Opcodes.DUP); - ctx.mv.visitMethodInsn(Opcodes.INVOKESPECIAL, - "org/perlonjava/runtime/RuntimeList", - "", - "()V", - false); + // Return the tagged list (will be detected at subroutine return boundary) ctx.mv.visitInsn(Opcodes.ARETURN); return; } diff --git a/src/main/java/org/perlonjava/codegen/EmitOperator.java b/src/main/java/org/perlonjava/codegen/EmitOperator.java index 16a36e4b2..dcee1748e 100644 --- a/src/main/java/org/perlonjava/codegen/EmitOperator.java +++ b/src/main/java/org/perlonjava/codegen/EmitOperator.java @@ -19,6 +19,8 @@ */ public class EmitOperator { + private static final boolean ENABLE_SPILL_BINARY_LHS = System.getenv("JPERL_NO_SPILL_BINARY_LHS") == null; + static void emitOperator(Node node, EmitterVisitor emitterVisitor) { // Extract operator string from the node String operator = null; @@ -417,8 +419,25 @@ static void handleConcatOperator(EmitterVisitor emitterVisitor, BinaryOperatorNo EmitterVisitor scalarVisitor = emitterVisitor.with(RuntimeContextType.SCALAR); // execute operands in scalar context // Accept both left and right operands in SCALAR context. - node.left.accept(scalarVisitor); // target - left parameter - node.right.accept(scalarVisitor); // right parameter + if (ENABLE_SPILL_BINARY_LHS) { + MethodVisitor mv = emitterVisitor.ctx.mv; + node.left.accept(scalarVisitor); // target - left parameter + int leftSlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot(); + boolean pooled = leftSlot >= 0; + if (!pooled) { + leftSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); + } + mv.visitVarInsn(Opcodes.ASTORE, leftSlot); + node.right.accept(scalarVisitor); // right parameter + mv.visitVarInsn(Opcodes.ALOAD, leftSlot); + mv.visitInsn(Opcodes.SWAP); + if (pooled) { + emitterVisitor.ctx.javaClassInfo.releaseSpillSlot(); + } + } else { + node.left.accept(scalarVisitor); // target - left parameter + node.right.accept(scalarVisitor); // right parameter + } emitOperator(node, emitterVisitor); } diff --git a/src/main/java/org/perlonjava/codegen/EmitSubroutine.java b/src/main/java/org/perlonjava/codegen/EmitSubroutine.java index c23e974e3..fe1b48a51 100644 --- a/src/main/java/org/perlonjava/codegen/EmitSubroutine.java +++ b/src/main/java/org/perlonjava/codegen/EmitSubroutine.java @@ -259,14 +259,32 @@ static void handleApplyOperator(EmitterVisitor emitterVisitor, BinaryOperatorNod false); } } - + + int codeRefSlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot(); + boolean pooledCodeRef = codeRefSlot >= 0; + if (!pooledCodeRef) { + codeRefSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); + } + mv.visitVarInsn(Opcodes.ASTORE, codeRefSlot); + + int nameSlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot(); + boolean pooledName = nameSlot >= 0; + if (!pooledName) { + nameSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); + } mv.visitLdcInsn(subroutineName); + mv.visitVarInsn(Opcodes.ASTORE, nameSlot); // Generate native RuntimeBase[] array for parameters instead of RuntimeList ListNode paramList = ListNode.makeList(node.right); int argCount = paramList.elements.size(); - // Create array of RuntimeBase with size equal to number of arguments + int argsArraySlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot(); + boolean pooledArgsArray = argsArraySlot >= 0; + if (!pooledArgsArray) { + argsArraySlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); + } + if (argCount <= 5) { mv.visitInsn(Opcodes.ICONST_0 + argCount); } else if (argCount <= 127) { @@ -275,11 +293,20 @@ static void handleApplyOperator(EmitterVisitor emitterVisitor, BinaryOperatorNod mv.visitIntInsn(Opcodes.SIPUSH, argCount); } mv.visitTypeInsn(Opcodes.ANEWARRAY, "org/perlonjava/runtime/RuntimeBase"); + mv.visitVarInsn(Opcodes.ASTORE, argsArraySlot); - // Populate the array with arguments EmitterVisitor listVisitor = emitterVisitor.with(RuntimeContextType.LIST); for (int index = 0; index < argCount; index++) { - mv.visitInsn(Opcodes.DUP); // Duplicate array reference + int argSlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot(); + boolean pooledArg = argSlot >= 0; + if (!pooledArg) { + argSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); + } + + paramList.elements.get(index).accept(listVisitor); + mv.visitVarInsn(Opcodes.ASTORE, argSlot); + + mv.visitVarInsn(Opcodes.ALOAD, argsArraySlot); if (index <= 5) { mv.visitInsn(Opcodes.ICONST_0 + index); } else if (index <= 127) { @@ -287,13 +314,17 @@ static void handleApplyOperator(EmitterVisitor emitterVisitor, BinaryOperatorNod } else { mv.visitIntInsn(Opcodes.SIPUSH, index); } + mv.visitVarInsn(Opcodes.ALOAD, argSlot); + mv.visitInsn(Opcodes.AASTORE); - // Generate code for argument in LIST context - paramList.elements.get(index).accept(listVisitor); - - mv.visitInsn(Opcodes.AASTORE); // Store in array + if (pooledArg) { + emitterVisitor.ctx.javaClassInfo.releaseSpillSlot(); + } } + mv.visitVarInsn(Opcodes.ALOAD, codeRefSlot); + mv.visitVarInsn(Opcodes.ALOAD, nameSlot); + mv.visitVarInsn(Opcodes.ALOAD, argsArraySlot); emitterVisitor.pushCallContext(); // Push call context to stack mv.visitMethodInsn( Opcodes.INVOKESTATIC, @@ -301,87 +332,122 @@ static void handleApplyOperator(EmitterVisitor emitterVisitor, BinaryOperatorNod "apply", "(Lorg/perlonjava/runtime/RuntimeScalar;Ljava/lang/String;[Lorg/perlonjava/runtime/RuntimeBase;I)Lorg/perlonjava/runtime/RuntimeList;", false); // Generate an .apply() call - - // Check for control flow (last/next/redo/goto/tail calls) - // NOTE: Call-site control flow is handled in VOID context below (after the call result is on stack). - // Do not call emitControlFlowCheck here, as it can clear the registry and/or require returning. - + + if (pooledArgsArray) { + emitterVisitor.ctx.javaClassInfo.releaseSpillSlot(); + } + if (pooledName) { + emitterVisitor.ctx.javaClassInfo.releaseSpillSlot(); + } + if (pooledCodeRef) { + emitterVisitor.ctx.javaClassInfo.releaseSpillSlot(); + } + + // Tagged returns control-flow handling: + // If RuntimeCode.apply() returned a RuntimeControlFlowList marker, handle it here. + if (emitterVisitor.ctx.javaClassInfo.returnLabel != null && + emitterVisitor.ctx.javaClassInfo.controlFlowTempSlot >= 0) { + + Label notControlFlow = new Label(); + Label propagateToCaller = new Label(); + Label checkLoopLabels = new Label(); + + // Store result in temp slot + mv.visitVarInsn(Opcodes.ASTORE, emitterVisitor.ctx.javaClassInfo.controlFlowTempSlot); + + // Load and check if it's a control flow marker + mv.visitVarInsn(Opcodes.ALOAD, emitterVisitor.ctx.javaClassInfo.controlFlowTempSlot); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, + "org/perlonjava/runtime/RuntimeList", + "isNonLocalGoto", + "()Z", + false); + mv.visitJumpInsn(Opcodes.IFEQ, notControlFlow); + + // Marked: load control flow type ordinal into controlFlowActionSlot + mv.visitVarInsn(Opcodes.ALOAD, emitterVisitor.ctx.javaClassInfo.controlFlowTempSlot); + mv.visitTypeInsn(Opcodes.CHECKCAST, "org/perlonjava/runtime/RuntimeControlFlowList"); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, + "org/perlonjava/runtime/RuntimeControlFlowList", + "getControlFlowType", + "()Lorg/perlonjava/runtime/ControlFlowType;", + false); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, + "org/perlonjava/runtime/ControlFlowType", + "ordinal", + "()I", + false); + mv.visitVarInsn(Opcodes.ISTORE, emitterVisitor.ctx.javaClassInfo.controlFlowActionSlot); + + // Only handle LAST/NEXT/REDO locally (ordinals 0/1/2). Others propagate. + mv.visitVarInsn(Opcodes.ILOAD, emitterVisitor.ctx.javaClassInfo.controlFlowActionSlot); + mv.visitInsn(Opcodes.ICONST_2); + mv.visitJumpInsn(Opcodes.IF_ICMPGT, propagateToCaller); + + mv.visitLabel(checkLoopLabels); + for (LoopLabels loopLabels : emitterVisitor.ctx.javaClassInfo.loopLabelStack) { + Label nextLoopCheck = new Label(); + + // if (!marked.matchesLabel(loopLabels.labelName)) continue; + mv.visitVarInsn(Opcodes.ALOAD, emitterVisitor.ctx.javaClassInfo.controlFlowTempSlot); + mv.visitTypeInsn(Opcodes.CHECKCAST, "org/perlonjava/runtime/RuntimeControlFlowList"); + if (loopLabels.labelName != null) { + mv.visitLdcInsn(loopLabels.labelName); + } else { + mv.visitInsn(Opcodes.ACONST_NULL); + } + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, + "org/perlonjava/runtime/RuntimeControlFlowList", + "matchesLabel", + "(Ljava/lang/String;)Z", + false); + mv.visitJumpInsn(Opcodes.IFEQ, nextLoopCheck); + + // Match found: jump based on type + Label checkNext = new Label(); + Label checkRedo = new Label(); + + // if (type == LAST (0)) goto lastLabel + mv.visitVarInsn(Opcodes.ILOAD, emitterVisitor.ctx.javaClassInfo.controlFlowActionSlot); + mv.visitInsn(Opcodes.ICONST_0); + mv.visitJumpInsn(Opcodes.IF_ICMPNE, checkNext); + emitterVisitor.ctx.javaClassInfo.stackLevelManager.emitPopInstructions(mv, loopLabels.asmStackLevel); + mv.visitJumpInsn(Opcodes.GOTO, loopLabels.lastLabel); + + // if (type == NEXT (1)) goto nextLabel + mv.visitLabel(checkNext); + mv.visitVarInsn(Opcodes.ILOAD, emitterVisitor.ctx.javaClassInfo.controlFlowActionSlot); + mv.visitInsn(Opcodes.ICONST_1); + mv.visitJumpInsn(Opcodes.IF_ICMPNE, checkRedo); + emitterVisitor.ctx.javaClassInfo.stackLevelManager.emitPopInstructions(mv, loopLabels.asmStackLevel); + mv.visitJumpInsn(Opcodes.GOTO, loopLabels.nextLabel); + + // if (type == REDO (2)) goto redoLabel + mv.visitLabel(checkRedo); + emitterVisitor.ctx.javaClassInfo.stackLevelManager.emitPopInstructions(mv, loopLabels.asmStackLevel); + mv.visitJumpInsn(Opcodes.GOTO, loopLabels.redoLabel); + + mv.visitLabel(nextLoopCheck); + } + + // No loop match; propagate + mv.visitJumpInsn(Opcodes.GOTO, propagateToCaller); + + // Propagate: jump to returnLabel with the marked list + mv.visitLabel(propagateToCaller); + emitterVisitor.ctx.javaClassInfo.stackLevelManager.emitPopInstructions(mv, 0); + mv.visitVarInsn(Opcodes.ALOAD, emitterVisitor.ctx.javaClassInfo.controlFlowTempSlot); + mv.visitJumpInsn(Opcodes.GOTO, emitterVisitor.ctx.javaClassInfo.returnLabel); + + // Not a control flow marker - load it back and continue + mv.visitLabel(notControlFlow); + mv.visitVarInsn(Opcodes.ALOAD, emitterVisitor.ctx.javaClassInfo.controlFlowTempSlot); + } + if (emitterVisitor.ctx.contextType == RuntimeContextType.SCALAR) { // Transform the value in the stack to RuntimeScalar mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/perlonjava/runtime/RuntimeList", "scalar", "()Lorg/perlonjava/runtime/RuntimeScalar;", false); } else if (emitterVisitor.ctx.contextType == RuntimeContextType.VOID) { - if (ENABLE_CONTROL_FLOW_CHECKS) { - LoopLabels innermostLoop = null; - for (LoopLabels loopLabels : emitterVisitor.ctx.javaClassInfo.loopLabelStack) { - if (loopLabels.isTrueLoop && loopLabels.context == RuntimeContextType.VOID) { - innermostLoop = loopLabels; - break; - } - } - if (innermostLoop != null) { - Label noAction = new Label(); - Label noMarker = new Label(); - Label checkNext = new Label(); - Label checkRedo = new Label(); - - // action = checkLoopAndGetAction(loopLabel) - if (innermostLoop.labelName != null) { - mv.visitLdcInsn(innermostLoop.labelName); - } else { - mv.visitInsn(Opcodes.ACONST_NULL); - } - mv.visitMethodInsn(Opcodes.INVOKESTATIC, - "org/perlonjava/runtime/RuntimeControlFlowRegistry", - "checkLoopAndGetAction", - "(Ljava/lang/String;)I", - false); - mv.visitVarInsn(Opcodes.ISTORE, emitterVisitor.ctx.javaClassInfo.controlFlowActionSlot); - - // if (action == 0) goto noAction - mv.visitVarInsn(Opcodes.ILOAD, emitterVisitor.ctx.javaClassInfo.controlFlowActionSlot); - mv.visitJumpInsn(Opcodes.IFEQ, noAction); - - // action != 0: pop call result, clean stack, jump to next/last/redo - mv.visitInsn(Opcodes.POP); - - // if (action == 1) last - mv.visitVarInsn(Opcodes.ILOAD, emitterVisitor.ctx.javaClassInfo.controlFlowActionSlot); - mv.visitInsn(Opcodes.ICONST_1); - mv.visitJumpInsn(Opcodes.IF_ICMPNE, checkNext); - mv.visitJumpInsn(Opcodes.GOTO, innermostLoop.lastLabel); - - // if (action == 2) next - mv.visitLabel(checkNext); - mv.visitVarInsn(Opcodes.ILOAD, emitterVisitor.ctx.javaClassInfo.controlFlowActionSlot); - mv.visitInsn(Opcodes.ICONST_2); - mv.visitJumpInsn(Opcodes.IF_ICMPNE, checkRedo); - mv.visitJumpInsn(Opcodes.GOTO, innermostLoop.nextLabel); - - // if (action == 3) redo - mv.visitLabel(checkRedo); - mv.visitVarInsn(Opcodes.ILOAD, emitterVisitor.ctx.javaClassInfo.controlFlowActionSlot); - mv.visitInsn(Opcodes.ICONST_3); - mv.visitJumpInsn(Opcodes.IF_ICMPEQ, innermostLoop.redoLabel); - - // Unknown action: unwind this loop (do NOT fall through to noMarker) - mv.visitJumpInsn(Opcodes.GOTO, innermostLoop.lastLabel); - - // action == 0: if marker still present, unwind this loop (label targets outer) - mv.visitLabel(noAction); - mv.visitMethodInsn(Opcodes.INVOKESTATIC, - "org/perlonjava/runtime/RuntimeControlFlowRegistry", - "hasMarker", - "()Z", - false); - mv.visitJumpInsn(Opcodes.IFEQ, noMarker); - - mv.visitInsn(Opcodes.POP); - mv.visitJumpInsn(Opcodes.GOTO, innermostLoop.lastLabel); - - mv.visitLabel(noMarker); - } - } - mv.visitInsn(Opcodes.POP); } } diff --git a/src/main/java/org/perlonjava/codegen/EmitVariable.java b/src/main/java/org/perlonjava/codegen/EmitVariable.java index fe99c98b1..2d1d046b6 100644 --- a/src/main/java/org/perlonjava/codegen/EmitVariable.java +++ b/src/main/java/org/perlonjava/codegen/EmitVariable.java @@ -441,6 +441,10 @@ static void handleAssignOperator(EmitterVisitor emitterVisitor, BinaryOperatorNo node.right.accept(emitterVisitor.with(RuntimeContextType.SCALAR)); // emit the value + boolean spillRhs = !(left instanceof OperatorNode op && op.operator.equals("keys")); + int rhsSlot = -1; + boolean pooledRhs = false; + if (isLocalAssignment) { // Clone the scalar before calling local() if (right instanceof OperatorNode operatorNode && operatorNode.operator.equals("*")) { @@ -456,8 +460,22 @@ static void handleAssignOperator(EmitterVisitor emitterVisitor, BinaryOperatorNo } } + if (spillRhs) { + rhsSlot = ctx.javaClassInfo.acquireSpillSlot(); + pooledRhs = rhsSlot >= 0; + if (!pooledRhs) { + rhsSlot = ctx.symbolTable.allocateLocalVariable(); + } + mv.visitVarInsn(Opcodes.ASTORE, rhsSlot); + } + node.left.accept(emitterVisitor.with(RuntimeContextType.SCALAR)); // emit the variable + if (spillRhs) { + mv.visitVarInsn(Opcodes.ALOAD, rhsSlot); + mv.visitInsn(Opcodes.SWAP); + } + OperatorNode nodeLeft = null; if (node.left instanceof OperatorNode operatorNode) { nodeLeft = operatorNode; @@ -486,6 +504,9 @@ static void handleAssignOperator(EmitterVisitor emitterVisitor, BinaryOperatorNo mv.visitInsn(Opcodes.SWAP); // Stack: [value, hash] mv.visitInsn(Opcodes.POP); // Stack: [value] // value is left on stack as the result of the assignment + if (pooledRhs) { + ctx.javaClassInfo.releaseSpillSlot(); + } return; // Skip normal assignment processing } @@ -518,6 +539,10 @@ static void handleAssignOperator(EmitterVisitor emitterVisitor, BinaryOperatorNo } else { mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/perlonjava/runtime/RuntimeBase", "addToScalar", "(Lorg/perlonjava/runtime/RuntimeScalar;)Lorg/perlonjava/runtime/RuntimeScalar;", false); } + + if (pooledRhs) { + ctx.javaClassInfo.releaseSpillSlot(); + } break; case RuntimeContextType.LIST: emitterVisitor.ctx.logDebug("SET right side list"); diff --git a/src/main/java/org/perlonjava/codegen/EmitterContext.java b/src/main/java/org/perlonjava/codegen/EmitterContext.java index ea2cb53f1..82a0f2327 100644 --- a/src/main/java/org/perlonjava/codegen/EmitterContext.java +++ b/src/main/java/org/perlonjava/codegen/EmitterContext.java @@ -150,6 +150,10 @@ public void logDebug(String message) { } } + public void clearContextCache() { + contextCache.clear(); + } + @Override public String toString() { return "EmitterContext{\n" + diff --git a/src/main/java/org/perlonjava/codegen/EmitterMethodCreator.java b/src/main/java/org/perlonjava/codegen/EmitterMethodCreator.java index 418c78ce0..f8f8e3bd0 100644 --- a/src/main/java/org/perlonjava/codegen/EmitterMethodCreator.java +++ b/src/main/java/org/perlonjava/codegen/EmitterMethodCreator.java @@ -1,6 +1,13 @@ package org.perlonjava.codegen; import org.objectweb.asm.*; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.analysis.Analyzer; +import org.objectweb.asm.tree.analysis.AnalyzerException; +import org.objectweb.asm.tree.analysis.BasicValue; +import org.objectweb.asm.tree.analysis.BasicInterpreter; +import org.objectweb.asm.util.CheckClassAdapter; import org.objectweb.asm.util.TraceClassVisitor; import org.perlonjava.astnode.Node; import org.perlonjava.astvisitor.EmitterVisitor; @@ -38,6 +45,27 @@ public static String generateClassName() { return "org/perlonjava/anon" + classCounter++; } + private static void debugAnalyzeWithBasicInterpreter(ClassReader cr, PrintWriter out) { + try { + ClassNode cn = new ClassNode(); + cr.accept(cn, ClassReader.EXPAND_FRAMES); + for (Object m : cn.methods) { + MethodNode mn = (MethodNode) m; + try { + Analyzer analyzer = new Analyzer<>(new BasicInterpreter()); + analyzer.analyze(cn.name, mn); + } catch (AnalyzerException ae) { + int insnIndex = (ae.node != null) ? mn.instructions.indexOf(ae.node) : -1; + out.println("BasicInterpreter failure in " + cn.name + "." + mn.name + mn.desc + " at instruction " + insnIndex); + ae.printStackTrace(out); + return; + } + } + } catch (Throwable t) { + t.printStackTrace(out); + } + } + /** * Generates a descriptor string based on the prefix of a Perl variable name. * @@ -101,16 +129,58 @@ public static Class createClassWithMethod(EmitterContext ctx, Node ast, boole } public static byte[] getBytecode(EmitterContext ctx, Node ast, boolean useTryCatch) { + boolean asmDebug = System.getenv("JPERL_ASM_DEBUG") != null; + try { + return getBytecodeInternal(ctx, ast, useTryCatch, false); + } catch (ArrayIndexOutOfBoundsException frameComputeCrash) { + // In normal operation we MUST NOT fall back to no-frames output, as that will fail + // verification on modern JVMs ("Expecting a stackmap frame..."). + // + // When JPERL_ASM_DEBUG is enabled, do a diagnostic pass without COMPUTE_FRAMES so we can + // disassemble + analyze the produced bytecode. + frameComputeCrash.printStackTrace(); + if (asmDebug) { + try { + // Reset JavaClassInfo to avoid reusing partially-resolved Labels. + if (ctx != null && ctx.javaClassInfo != null) { + String previousName = ctx.javaClassInfo.javaClassName; + ctx.javaClassInfo = new JavaClassInfo(); + ctx.javaClassInfo.javaClassName = previousName; + ctx.clearContextCache(); + } + getBytecodeInternal(ctx, ast, useTryCatch, true); + } catch (Throwable diagErr) { + diagErr.printStackTrace(); + } + } + throw new PerlCompilerException( + ast.getIndex(), + "Internal compiler error: ASM frame computation failed. " + + "Re-run with JPERL_ASM_DEBUG=1 to print disassembly and analysis. " + + "Original error: " + frameComputeCrash.getMessage(), + ctx.errorUtil, + frameComputeCrash); + } + } + + private static byte[] getBytecodeInternal(EmitterContext ctx, Node ast, boolean useTryCatch, boolean disableFrames) { String className = ctx.javaClassInfo.javaClassName; String methodName = "apply"; byte[] classData = null; + boolean asmDebug = System.getenv("JPERL_ASM_DEBUG") != null; try { String[] env = ctx.symbolTable.getVariableNames(); // Create a ClassWriter with COMPUTE_FRAMES and COMPUTE_MAXS options for automatic frame and max // stack size calculation - ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); + // Only disable COMPUTE_FRAMES for the explicit diagnostic pass (disableFrames=true). + // In normal operation (even when JPERL_ASM_DEBUG is enabled) we want COMPUTE_FRAMES, + // otherwise the generated class may fail verification on modern JVMs. + int cwFlags = disableFrames + ? ClassWriter.COMPUTE_MAXS + : (ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); + ClassWriter cw = new ClassWriter(cwFlags); ctx.cw = cw; // The context type is determined by the caller. @@ -231,14 +301,34 @@ public static byte[] getBytecode(EmitterContext ctx, Node ast, boolean useTryCat int tailCallArgsSlot = ctx.symbolTable.allocateLocalVariable(); ctx.javaClassInfo.tailCallCodeRefSlot = tailCallCodeRefSlot; ctx.javaClassInfo.tailCallArgsSlot = tailCallArgsSlot; + mv.visitInsn(Opcodes.ACONST_NULL); + mv.visitVarInsn(Opcodes.ASTORE, tailCallCodeRefSlot); + mv.visitInsn(Opcodes.ACONST_NULL); + mv.visitVarInsn(Opcodes.ASTORE, tailCallArgsSlot); // Allocate slot for control flow check temp storage // This is used at call sites to temporarily store marked RuntimeControlFlowList int controlFlowTempSlot = ctx.symbolTable.allocateLocalVariable(); ctx.javaClassInfo.controlFlowTempSlot = controlFlowTempSlot; + mv.visitInsn(Opcodes.ACONST_NULL); + mv.visitVarInsn(Opcodes.ASTORE, controlFlowTempSlot); int controlFlowActionSlot = ctx.symbolTable.allocateLocalVariable(); ctx.javaClassInfo.controlFlowActionSlot = controlFlowActionSlot; + mv.visitInsn(Opcodes.ICONST_0); + mv.visitVarInsn(Opcodes.ISTORE, controlFlowActionSlot); + + int spillSlotCount = System.getenv("JPERL_SPILL_SLOTS") != null + ? Integer.parseInt(System.getenv("JPERL_SPILL_SLOTS")) + : 16; + ctx.javaClassInfo.spillSlots = new int[spillSlotCount]; + ctx.javaClassInfo.spillTop = 0; + for (int i = 0; i < spillSlotCount; i++) { + int slot = ctx.symbolTable.allocateLocalVariable(); + ctx.javaClassInfo.spillSlots[i] = slot; + mv.visitInsn(Opcodes.ACONST_NULL); + mv.visitVarInsn(Opcodes.ASTORE, slot); + } // Create a label for the return point ctx.javaClassInfo.returnLabel = new Label(); @@ -510,13 +600,66 @@ public static byte[] getBytecode(EmitterContext ctx, Node ast, boolean useTryCat StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); TraceClassVisitor tcv = new TraceClassVisitor(pw); - cr.accept(tcv, 0); + cr.accept(tcv, ClassReader.EXPAND_FRAMES); System.out.println(sw); } + + if (asmDebug) { + try { + ClassReader cr = new ClassReader(classData); + + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + TraceClassVisitor tcv = new TraceClassVisitor(pw); + cr.accept(tcv, ClassReader.EXPAND_FRAMES); + pw.flush(); + System.err.println(sw); + + PrintWriter verifyPw = new PrintWriter(System.err); + String thisClassNameDot = className.replace('/', '.'); + final byte[] verifyClassData = classData; + ClassLoader verifyLoader = new ClassLoader(GlobalVariable.globalClassLoader) { + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + if (name.equals(thisClassNameDot)) { + synchronized (getClassLoadingLock(name)) { + Class loaded = findLoadedClass(name); + if (loaded == null) { + loaded = defineClass(name, verifyClassData, 0, verifyClassData.length); + } + if (resolve) { + resolveClass(loaded); + } + return loaded; + } + } + return super.loadClass(name, resolve); + } + }; + + boolean verified = false; + try { + CheckClassAdapter.verify(cr, verifyLoader, true, verifyPw); + verified = true; + } catch (Throwable verifyErr) { + verifyErr.printStackTrace(verifyPw); + } + verifyPw.flush(); + + // Always run a classloader-free verifier pass to get a concrete method/instruction index. + // CheckClassAdapter.verify() can fail or be noisy when it cannot load generated anon classes. + debugAnalyzeWithBasicInterpreter(cr, verifyPw); + verifyPw.flush(); + } catch (Throwable t) { + t.printStackTrace(); + } + } return classData; } catch (ArrayIndexOutOfBoundsException e) { - // Print full stack trace for debugging + if (!disableFrames) { + throw e; + } e.printStackTrace(); throw new PerlCompilerException( ast.getIndex(), diff --git a/src/main/java/org/perlonjava/codegen/JavaClassInfo.java b/src/main/java/org/perlonjava/codegen/JavaClassInfo.java index 63132ee4e..08cfdd0c7 100644 --- a/src/main/java/org/perlonjava/codegen/JavaClassInfo.java +++ b/src/main/java/org/perlonjava/codegen/JavaClassInfo.java @@ -39,6 +39,9 @@ public class JavaClassInfo { public int controlFlowActionSlot; + public int[] spillSlots; + public int spillTop; + /** * Manages the stack level for the class. */ @@ -61,6 +64,21 @@ public JavaClassInfo() { this.stackLevelManager = new StackLevelManager(); this.loopLabelStack = new ArrayDeque<>(); this.gotoLabelStack = new ArrayDeque<>(); + this.spillSlots = new int[0]; + this.spillTop = 0; + } + + public int acquireSpillSlot() { + if (spillTop >= spillSlots.length) { + return -1; + } + return spillSlots[spillTop++]; + } + + public void releaseSpillSlot() { + if (spillTop > 0) { + spillTop--; + } } /** From 8401ca2c123db41dbfbf5e1dcb4d2287db445102 Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Tue, 6 Jan 2026 17:34:47 +0100 Subject: [PATCH 02/51] chore: untrack local control-flow scratch files --- .gitignore | 4 + dev/design/CONTROL_FLOW_FIX_RESULTS.md | 162 --------------- dev/prompts/CONTROL_FLOW_FIX_INSTRUCTIONS.md | 197 ------------------- skip_control_flow.t | 185 ----------------- 4 files changed, 4 insertions(+), 544 deletions(-) delete mode 100644 dev/design/CONTROL_FLOW_FIX_RESULTS.md delete mode 100644 dev/prompts/CONTROL_FLOW_FIX_INSTRUCTIONS.md delete mode 100644 skip_control_flow.t diff --git a/.gitignore b/.gitignore index e424fb4ca..c8d18e9ab 100644 --- a/.gitignore +++ b/.gitignore @@ -52,6 +52,10 @@ logs/ # But allow patch files in import-perl5/patches/ !dev/import-perl5/patches/*.patch +skip_control_flow.t +dev/design/CONTROL_FLOW_FIX_RESULTS.md +dev/prompts/CONTROL_FLOW_FIX_INSTRUCTIONS.md + # Ignore test artifact files (numbered .txt and .pod files) [0-9][0-9][0-9][0-9][0-9][0-9].txt [0-9][0-9][0-9][0-9][0-9].pod diff --git a/dev/design/CONTROL_FLOW_FIX_RESULTS.md b/dev/design/CONTROL_FLOW_FIX_RESULTS.md deleted file mode 100644 index 036a5da74..000000000 --- a/dev/design/CONTROL_FLOW_FIX_RESULTS.md +++ /dev/null @@ -1,162 +0,0 @@ -# Control Flow Fix: Implementation Results and Findings - -**Date:** January 6, 2026 -**Related:** `CONTROL_FLOW_FINAL_STATUS.md`, `CONTROL_FLOW_FINAL_STEPS.md` - -## Summary - -This document captures the results of implementing the control-flow fix for tagged returns (`last LABEL`, `next LABEL`, `redo LABEL`, `goto LABEL`) and the subsequent work to resolve ASM `Frame.merge` crashes caused by non-empty JVM operand stacks at merge points. - -## Phase 1: Tagged Returns (Completed ✓) - -**Goal:** Enable `last LABEL`, `next LABEL`, `redo LABEL`, `goto LABEL` to work across subroutine boundaries. - -**Implementation:** -- Introduced `RuntimeControlFlowList` with `ControlFlowMarker` to carry control-flow metadata through the call stack -- Modified `EmitSubroutine` to check every subroutine return for control-flow markers and dispatch to appropriate loop labels or propagate upward -- Updated `EmitControlFlow` to emit markers for non-local jumps (when target label is not in current scope) - -**Result:** ✓ **SUCCESS** -- All 11 tests in `skip_control_flow.t` pass -- Tagged returns work correctly across nested subroutines and loops - -## Phase 2: ASM Frame.merge Crashes (In Progress) - -**Problem:** After implementing tagged returns, `pack.t` (which loads `Data::Dumper` and `Test2::API`) started failing with: -``` -java.lang.ArrayIndexOutOfBoundsException: Index 1 out of bounds for length 1 - at org.objectweb.asm.Frame.merge(Frame.java:1280) -``` - -**Root Cause:** -The JVM operand stack was non-empty at certain `GOTO` instructions that jump to merge points (labels with multiple incoming edges). ASM's frame computation requires all incoming edges to a merge point to have compatible stack heights. - -### Approach 1: Spill-to-Local with Preallocated Pool (Current) - -**Strategy:** Before evaluating subexpressions that can produce non-local control flow (subroutine calls, operators), spill intermediate operands to local variables, keeping the JVM operand stack empty. - -**Implementation:** -1. **Preallocated Spill-Slot Pool:** - - Added `spillSlots[]` array to `JavaClassInfo`, preallocated in `EmitterMethodCreator` - - Default: 16 slots (configurable via `JPERL_SPILL_SLOTS` env var) - - `acquireSpillSlot()` / `releaseSpillSlot()` manage the pool - - Fallback to `allocateLocalVariable()` if pool exhausted - -2. **Applied Spills:** - - **String concatenation** (`EmitOperator.emitConcatenation`): spill LHS before evaluating RHS - - **Binary operators** (`EmitBinaryOperator`): spill LHS before evaluating RHS (for operators that can trigger control flow) - - **Scalar assignment** (`EmitVariable.handleAssignOperator`): spill RHS value before evaluating LHS - -3. **Control Switch:** - - `JPERL_NO_SPILL_BINARY_LHS=1` disables spills (for A/B testing) - - Default: spills enabled - -**Advantages:** -- ✓ By-construction invariant: operand stack is empty when we evaluate potentially-escaping subexpressions -- ✓ No dependency on `TempLocalCountVisitor` sizing (uses fixed preallocated pool) -- ✓ Deterministic and predictable -- ✓ Works with ASM's existing frame computation - -**Limitations:** -- ⚠ May not cover all edge cases (still finding failure sites in `pack.t`) -- ⚠ Could theoretically exhaust the spill-slot pool on very deeply nested expressions (though 16 slots should be sufficient for typical code) - -**Status:** In progress. Most common patterns covered, but `pack.t` still fails on some edge cases. - -### Approach 2: AnalyzerAdapter-Based Stack Cleanup (Abandoned) - -**Strategy:** Wrap the generated `apply()` method's `MethodVisitor` with ASM's `AnalyzerAdapter`, which tracks the operand stack linearly. Before each non-local `GOTO`, emit `POP/POP2` instructions to empty the stack based on `AnalyzerAdapter.stack`. - -**Implementation Attempted:** -1. Added `asm-commons` dependency -2. Wrapped `apply()` method visitor with `AnalyzerAdapter` in `EmitterMethodCreator` -3. Added `JavaClassInfo.emitPopOperandStackToEmpty(mv)` to emit POPs based on adapter's stack -4. Called `emitPopOperandStackToEmpty()` before all `GOTO returnLabel` and loop label jumps - -**Why It Failed:** -- ❌ `AnalyzerAdapter` tracks the stack **linearly** during emission, not across control-flow merges -- ❌ At a `GOTO L`, the adapter only knows the stack state on the **current linear path** -- ❌ It cannot know what stack state other predecessor paths will have when they reach `L` -- ❌ Result: we can "pop to empty" on one path, but another path might still arrive at the same label with items on the stack → incompatible stack heights at merge - -**Fundamental Limitation:** -`AnalyzerAdapter` is not a full control-flow dataflow analyzer. It cannot guarantee the invariant "all incoming edges to a merge point have the same stack height" because it doesn't compute merged states during emission. - -**Conclusion:** This approach cannot work without a full two-pass compiler (emit bytecode, analyze with `Analyzer`, rewrite to insert POPs on all incoming edges). - -### Approach 3: Full Control-Flow Analysis + Rewrite (Not Attempted) - -**Strategy:** -1. Generate bytecode normally -2. Run ASM's `Analyzer` to compute stack heights at every instruction (including merges) -3. Rewrite the method to insert `POP/POP2` on all incoming edges to merge points as needed - -**Advantages:** -- ✓ Would be truly systematic and handle all cases -- ✓ No need for manual spilling or stack tracking during emission - -**Disadvantages:** -- ❌ Requires a full additional compiler pass -- ❌ Complex rewriting logic -- ❌ May be overkill for this problem - -**Status:** Not pursued. The spill-slot pool approach is simpler and should be sufficient. - -## Current Status - -**Working:** -- ✓ Tagged returns (`last LABEL`, `next LABEL`, `redo LABEL`, `goto LABEL`) across subroutine boundaries -- ✓ `skip_control_flow.t` (11/11 tests pass) -- ✓ Spill-slot pool infrastructure in place -- ✓ Spills applied to concat, binary operators, scalar assignment - -**In Progress:** -- ⚠ `pack.t` still fails with ASM frame merge errors at `anon453` instructions 104/108 -- Root cause: When a subroutine call (inside an expression) returns a control-flow marker, the propagation logic tries to jump to `returnLabel`, but there are values on the JVM operand stack from the outer expression context -- Current `stackLevelManager` doesn't track the actual JVM operand stack during expression evaluation - it only tracks "logical" stack levels (loop nesting, etc.) - -**Two Possible Solutions:** - -### Solution A: Comprehensive Stack Tracking (Preferred) -Track every JVM operand stack operation throughout expression evaluation: -- Add `increment(1)` after every operation that pushes to stack -- Add `decrement(1)` after every operation that pops from stack -- This would make `stackLevelManager.getStackLevel()` accurately reflect the JVM operand stack depth -- Then `stackLevelManager.emitPopInstructions(mv, 0)` would correctly clean the stack before control-flow propagation - -**Requires changes to:** -- All binary operators -- All method calls -- All local variable stores/loads -- All expression evaluation sites - -### Solution B: Targeted Spills (Current Approach) -Continue applying spills to ensure stack is empty before subroutine calls: -- Already applied to: concat, binary ops, scalar assignment -- Still need to identify remaining patterns where subroutine calls happen with non-empty stack - -**Next Steps:** -1. Decide between Solution A (comprehensive tracking) vs Solution B (targeted spills) -2. If Solution A: Implement stack tracking in binary operators and expression evaluation -3. If Solution B: Continue identifying and fixing specific failure patterns -4. Re-test `pack.t` until it passes -5. Run full regression suite -6. Prepare PR - -## Lessons Learned - -1. **By-construction invariants are more reliable than runtime tracking** when dealing with ASM frame computation. - -2. **`AnalyzerAdapter` is not sufficient for merge-point analysis** — it only tracks linear paths during emission. - -3. **Preallocated resource pools** (spill slots) are better than dynamic allocation (`TempLocalCountVisitor` + buffer) for avoiding VerifyError and frame computation issues. - -4. **The spill approach is not "whack-a-mole"** if we apply it systematically to all expression evaluation sites that can trigger non-local control flow (subroutine calls, operators that can return marked lists). - -5. **Environment variable switches** (`JPERL_NO_SPILL_BINARY_LHS`) are valuable for A/B testing and debugging. - -## References - -- `CONTROL_FLOW_FINAL_STATUS.md` - Original design for tagged returns -- `CONTROL_FLOW_FINAL_STEPS.md` - Implementation steps -- `ASM_FRAME_COMPUTATION_BLOCKER.md` - Earlier notes on frame computation issues diff --git a/dev/prompts/CONTROL_FLOW_FIX_INSTRUCTIONS.md b/dev/prompts/CONTROL_FLOW_FIX_INSTRUCTIONS.md deleted file mode 100644 index 2bf9bf759..000000000 --- a/dev/prompts/CONTROL_FLOW_FIX_INSTRUCTIONS.md +++ /dev/null @@ -1,197 +0,0 @@ -# Control Flow Fix Instructions - -## Problem Statement - -The Perl control flow for labeled blocks with `last LABEL` is broken in scalar context when called through multiple stack frames. This affects Test::More's `skip()` function and other labeled block control flow. - -## Current Status - -### What Works -- Single frame control flow: `last LABEL` works when called directly in the same frame -- Void context: `last LABEL` works through multiple frames in void context -- Test::More has been updated to use `last SKIP` directly (no TestMoreHelper workaround) - -### What's Broken -- **Scalar context control flow**: `last LABEL` fails when called through 2+ frames in scalar context -- Tests 2, 5, 8 in `skip_control_flow.t` demonstrate this failure - -## Baseline Expectations - -Based on logs from 2026-01-06 09:27: -- **op/pack.t**: 14579/14726 tests passing -- **uni/variables.t**: 66683/66880 tests passing -- **op/lc.t**: 2710/2716 tests passing - -Current state shows regressions: -- **op/pack.t**: 245/14726 (regression of -14334 tests) -- **uni/variables.t**: 56/66880 (regression of -66627 tests) - -## Test Files - -### Unit Test -`src/test/resources/unit/skip_control_flow.t` - 11 tests demonstrating control flow issues - -**Backup location**: `/tmp/skip_control_flow.t.backup` - -Run with: -```bash -./jperl src/test/resources/unit/skip_control_flow.t -``` - -Expected results: -- Tests 1, 3, 4, 6, 7, 9, 10, 11: PASS -- **Tests 2, 5, 8: FAIL** (scalar context issue - these demonstrate the problem) - -### Integration Tests - -Test `uni/variables.t`: -```bash -cd perl5_t/t && ../../jperl uni/variables.t 2>&1 | grep "Looks like" -# Should show: planned 66880, ran 66880 (or close to it) -# Currently shows: planned 66880, ran 56 -``` - -Test `op/pack.t`: -```bash -cd perl5_t/t && ../../jperl op/pack.t 2>&1 | grep "Looks like" -# Should show: planned 14726, ran ~14579 -# Currently shows: planned 14726, ran 249 -``` - -Use test runner for batch testing: -```bash -perl dev/tools/perl_test_runner.pl perl5_t/t/op/pack.t perl5_t/t/uni/variables.t -``` - -## Build Process - -**CRITICAL**: Always build with `make` and wait for completion: - -```bash -make -# Wait for "BUILD SUCCESSFUL" message -# Do NOT interrupt the build -``` - -The `make` command runs: -- `./gradlew classes testUnitParallel --parallel shadowJar` -- Compiles Java code -- Runs unit tests -- Builds the JAR file - -**Do NOT use**: -- `./gradlew shadowJar` alone (skips tests) -- `./gradlew clean shadowJar` (unless specifically needed) - -## Development Workflow - -### 1. BEFORE Making Changes - -Add failing tests to `skip_control_flow.t` that demonstrate the problem: - -```perl -# Example: Add a test that shows the issue -# Test X) Description of what should work but doesn't -{ - my $out = ''; - sub test_func { last SOMELABEL } - SOMELABEL: { - $out .= 'A'; - my $result = test_func(); # Scalar context - $out .= 'B'; - } - $out .= 'C'; - ok_tap($out eq 'AC', 'description of expected behavior'); -} -``` - -Run the test to confirm it fails: -```bash -./jperl src/test/resources/unit/skip_control_flow.t -``` - -### 2. Make Your Changes - -Focus areas: -- `src/main/java/org/perlonjava/codegen/EmitBlock.java` - Block code generation -- `src/main/java/org/perlonjava/runtime/RuntimeControlFlowRegistry.java` - Control flow marker registry -- Loop constructs in `EmitForeach.java`, `EmitStatement.java` - -### 3. Build and Test - -```bash -# Build (wait for completion) -make - -# Test unit tests -./jperl src/test/resources/unit/skip_control_flow.t - -# Test integration -cd perl5_t/t && ../../jperl uni/variables.t 2>&1 | grep "Looks like" -cd perl5_t/t && ../../jperl op/pack.t 2>&1 | grep "Looks like" -``` - -### 4. Verify No Regressions - -Compare test counts to baseline: -- `uni/variables.t`: Should run ~66683/66880 tests (not stop at 56) -- `op/pack.t`: Should run ~14579/14726 tests (not stop at 245) -- `skip_control_flow.t`: All 11 tests should pass - -## Key Technical Details - -### Control Flow Registry - -`RuntimeControlFlowRegistry` manages non-local control flow markers: -- `register(marker)` - Sets a control flow marker -- `hasMarker()` - Checks if marker exists -- `checkLoopAndGetAction(label)` - Checks and clears matching marker -- `markerMatchesLabel(label)` - Checks if marker matches without clearing -- `clear()` - Clears the marker - -### The Problem - -When `last LABEL` is called in scalar context through multiple frames: -1. The marker is registered correctly -2. The registry check happens -3. But the control flow doesn't properly exit the labeled block -4. This causes tests to continue executing when they should stop - -### Previous Attempts - -Several approaches were tried and caused regressions: -1. Adding registry checks in `EmitBlock.java` after each statement - - Caused `op/pack.t` to stop at test 245 -2. Unconditional registry clearing at block exit - - Caused `op/pack.t` to stop at test 245 -3. Conditional registry clearing (only if marker matches) - - Still caused `op/pack.t` to stop at test 245 - -The issue is that bare labeled blocks (not actual loops) need special handling. - -## Success Criteria - -1. All 11 tests in `skip_control_flow.t` pass (including tests 2, 5, 8) -2. `uni/variables.t` runs to completion (~66683/66880 tests) -3. `op/pack.t` runs to completion (~14579/14726 tests) -4. No new test failures introduced - -## Files to Review - -- `src/main/java/org/perlonjava/codegen/EmitBlock.java` -- `src/main/java/org/perlonjava/runtime/RuntimeControlFlowRegistry.java` -- `src/main/java/org/perlonjava/codegen/EmitForeach.java` -- `src/main/java/org/perlonjava/codegen/EmitStatement.java` -- `src/main/perl/lib/Test/More.pm` -- `src/test/resources/unit/skip_control_flow.t` - -## Notes - -- Work in a branch -- The JAR builds correctly - test results are real, not build issues -- The baseline (14579/14726 for op/pack.t) may be from a specific branch/commit -- Focus on making the unit tests pass first, then verify integration tests -- Real loops (for/while/foreach) have their own registry checks that work correctly -- The issue is specific to bare labeled blocks in scalar context - -Good luck! diff --git a/skip_control_flow.t b/skip_control_flow.t deleted file mode 100644 index c5ddf0ee3..000000000 --- a/skip_control_flow.t +++ /dev/null @@ -1,185 +0,0 @@ -#!/usr/bin/env perl -use strict; -use warnings; - -# Minimal TAP without Test::More (we need this to work even when skip()/TODO are broken) -my $t = 0; -sub ok_tap { - my ($cond, $name) = @_; - $t++; - print(($cond ? "ok" : "not ok"), " $t - $name\n"); -} - -# 1) Single frame - MYLABEL -{ - my $out = ''; - sub test_once { last MYLABEL } - MYLABEL: { - $out .= 'A'; - test_once(); - $out .= 'B'; - } - $out .= 'C'; - ok_tap($out eq 'AC', 'last MYLABEL exits MYLABEL block (single frame)'); -} - -# 2) Two frames, scalar context - MYLABEL -{ - my $out = ''; - sub inner2 { last MYLABEL } - sub outer2 { my $x = inner2(); return $x; } - MYLABEL: { - $out .= 'A'; - my $r = outer2(); - $out .= 'B'; - } - $out .= 'C'; - ok_tap($out eq 'AC', 'last MYLABEL exits MYLABEL block (2 frames, scalar context)'); -} - -# 3) Two frames, void context - MYLABEL -{ - my $out = ''; - sub innerv { last MYLABEL } - sub outerv { innerv(); } - MYLABEL: { - $out .= 'A'; - outerv(); - $out .= 'B'; - } - $out .= 'C'; - ok_tap($out eq 'AC', 'last MYLABEL exits MYLABEL block (2 frames, void context)'); -} - -# 4) Single frame - LABEL2 -{ - my $out = ''; - sub test2_once { last LABEL2 } - LABEL2: { - $out .= 'A'; - test2_once(); - $out .= 'B'; - } - $out .= 'C'; - ok_tap($out eq 'AC', 'last LABEL2 exits LABEL2 block (single frame)'); -} - -# 5) Two frames, scalar context - LABEL2 -{ - my $out = ''; - sub inner_label2 { last LABEL2 } - sub outer_label2 { my $x = inner_label2(); return $x; } - LABEL2: { - $out .= 'A'; - my $r = outer_label2(); - $out .= 'B'; - } - $out .= 'C'; - ok_tap($out eq 'AC', 'last LABEL2 exits LABEL2 block (2 frames, scalar context)'); -} - -# 6) Two frames, void context - LABEL2 -{ - my $out = ''; - sub innerv_label2 { last LABEL2 } - sub outerv_label2 { innerv_label2(); } - LABEL2: { - $out .= 'A'; - outerv_label2(); - $out .= 'B'; - } - $out .= 'C'; - ok_tap($out eq 'AC', 'last LABEL2 exits LABEL2 block (2 frames, void context)'); -} - -# 7) Single frame - LABEL3 -{ - my $out = ''; - sub test3_once { last LABEL3 } - LABEL3: { - $out .= 'A'; - test3_once(); - $out .= 'B'; - } - $out .= 'C'; - ok_tap($out eq 'AC', 'last LABEL3 exits LABEL3 block (single frame)'); -} - -# 8) Two frames, scalar context - LABEL3 -{ - my $out = ''; - sub inner_label3 { last LABEL3 } - sub outer_label3 { my $x = inner_label3(); return $x; } - LABEL3: { - $out .= 'A'; - my $r = outer_label3(); - $out .= 'B'; - } - $out .= 'C'; - ok_tap($out eq 'AC', 'last LABEL3 exits LABEL3 block (2 frames, scalar context)'); -} - -# 9) Two frames, void context - LABEL3 -{ - my $out = ''; - sub innerv_label3 { last LABEL3 } - sub outerv_label3 { innerv_label3(); } - LABEL3: { - $out .= 'A'; - outerv_label3(); - $out .= 'B'; - } - $out .= 'C'; - ok_tap($out eq 'AC', 'last LABEL3 exits LABEL3 block (2 frames, void context)'); -} - -# 10) Stale marker bug - labeled block in eval leaves marker -{ - my $out = ''; - # This eval creates a labeled block that might leave a stale marker - eval "\${\x{30cd}single:\x{30cd}colon} = 'test'"; - $out .= 'A'; - - # This SKIP block should work normally, not be affected by stale marker - MYLABEL: { - $out .= 'B'; - $out .= 'C'; - } - $out .= 'D'; - ok_tap($out eq 'ABCD', 'labeled block in eval does not leave stale marker'); -} - -# 11) Registry clearing bug - large SKIP block (>3 statements) with skip() -{ - my $out = ''; - my $count = 0; - - # SKIP block with >3 statements (so registry check won't run inside) - # But registry clearing at exit WILL run - SKIP: { - my $a = 1; # statement 1 - my $b = 2; # statement 2 - my $c = 3; # statement 3 - my $d = 4; # statement 4 - $out .= 'S'; - last SKIP; # This sets a marker, but block has >3 statements so no check - $out .= 'X'; - } - # When SKIP exits, registry is cleared unconditionally - # This removes the marker that was correctly set by last SKIP - - $out .= 'A'; - - # This loop should run 3 times - for my $i (1..3) { - INNER: { - $out .= 'L'; - $count++; - } - } - - $out .= 'B'; - ok_tap($out eq 'SALLLB' && $count == 3, 'large SKIP block does not break subsequent loops'); -} - -print "1..$t\n"; From c3426129d31657c356d491bb3716488f2e793189 Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Wed, 7 Jan 2026 19:22:46 +0100 Subject: [PATCH 03/51] Fix ASM verifier errors in control flow handling --- .../org/perlonjava/codegen/Dereference.java | 114 ++++++++++++++++-- .../org/perlonjava/codegen/EmitBlock.java | 10 +- .../perlonjava/codegen/EmitControlFlow.java | 57 +++++++-- .../org/perlonjava/codegen/EmitForeach.java | 91 +++++++++----- .../org/perlonjava/codegen/EmitLiteral.java | 90 ++++++++++---- .../codegen/EmitLogicalOperator.java | 93 ++++++++++++-- .../org/perlonjava/codegen/EmitOperator.java | 75 +++++++++++- .../codegen/EmitOperatorChained.java | 31 +++++ .../org/perlonjava/codegen/EmitRegex.java | 43 +++++++ .../perlonjava/codegen/EmitSubroutine.java | 62 ++++++++-- .../org/perlonjava/codegen/EmitVariable.java | 17 ++- .../codegen/EmitterMethodCreator.java | 88 ++++++++++++-- .../org/perlonjava/codegen/JavaClassInfo.java | 43 +++++++ .../perlonjava/codegen/StackLevelManager.java | 11 +- .../perlonjava/operators/ModuleOperators.java | 8 ++ .../org/perlonjava/parser/DataSection.java | 31 +++-- .../perlonjava/parser/StatementParser.java | 31 ++++- .../scriptengine/PerlLanguageProvider.java | 9 +- .../perlonjava/PerlScriptExecutionTest.java | 13 ++ 19 files changed, 795 insertions(+), 122 deletions(-) diff --git a/src/main/java/org/perlonjava/codegen/Dereference.java b/src/main/java/org/perlonjava/codegen/Dereference.java index 3133dd29f..979f5656a 100644 --- a/src/main/java/org/perlonjava/codegen/Dereference.java +++ b/src/main/java/org/perlonjava/codegen/Dereference.java @@ -186,6 +186,13 @@ public static void handleHashElementOperator(EmitterVisitor emitterVisitor, Bina emitterVisitor.ctx.logDebug("visit(BinaryOperatorNode) $var{} "); varNode.accept(emitterVisitor.with(RuntimeContextType.LIST)); // target - left parameter + int leftSlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot(); + boolean pooledLeft = leftSlot >= 0; + if (!pooledLeft) { + leftSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); + } + emitterVisitor.ctx.mv.visitVarInsn(Opcodes.ASTORE, leftSlot); + // emit the {x} as a RuntimeList ListNode nodeRight = ((HashLiteralNode) node.right).asListNode(); @@ -201,29 +208,64 @@ public static void handleHashElementOperator(EmitterVisitor emitterVisitor, Bina // Optimization: if there's only one element and it's a string literal if (nodeRight.elements.size() == 1 && nodeZero instanceof StringNode) { // Special case: string literal - use get(String) directly + emitterVisitor.ctx.mv.visitVarInsn(Opcodes.ALOAD, leftSlot); emitterVisitor.ctx.mv.visitLdcInsn(((StringNode) nodeZero).value); emitterVisitor.ctx.mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/perlonjava/runtime/RuntimeHash", hashOperation, "(Ljava/lang/String;)Lorg/perlonjava/runtime/RuntimeScalar;", false); } else if (nodeRight.elements.size() == 1) { // Single element but not a string literal Node elem = nodeRight.elements.getFirst(); - elem.accept(scalarVisitor); + elem.accept(emitterVisitor.with(RuntimeContextType.SCALAR)); + + int keySlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot(); + boolean pooledKey = keySlot >= 0; + if (!pooledKey) { + keySlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); + } + emitterVisitor.ctx.mv.visitVarInsn(Opcodes.ASTORE, keySlot); + + emitterVisitor.ctx.mv.visitVarInsn(Opcodes.ALOAD, leftSlot); + emitterVisitor.ctx.mv.visitVarInsn(Opcodes.ALOAD, keySlot); emitterVisitor.ctx.mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/perlonjava/runtime/RuntimeHash", hashOperation, "(Lorg/perlonjava/runtime/RuntimeScalar;)Lorg/perlonjava/runtime/RuntimeScalar;", false); + + if (pooledKey) { + emitterVisitor.ctx.javaClassInfo.releaseSpillSlot(); + } } else { // Multiple elements: join them with $; (SUBSEP) // Get the $; global variable (SUBSEP) emitterVisitor.ctx.mv.visitLdcInsn("main::;"); emitterVisitor.ctx.mv.visitMethodInsn(Opcodes.INVOKESTATIC, "org/perlonjava/runtime/GlobalVariable", "getGlobalVariable", "(Ljava/lang/String;)Lorg/perlonjava/runtime/RuntimeScalar;", false); + + int sepSlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot(); + boolean pooledSep = sepSlot >= 0; + if (!pooledSep) { + sepSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); + } + emitterVisitor.ctx.mv.visitVarInsn(Opcodes.ASTORE, sepSlot); + // Emit the list of elements nodeRight.accept(emitterVisitor.with(RuntimeContextType.LIST)); + emitterVisitor.ctx.mv.visitVarInsn(Opcodes.ALOAD, sepSlot); + emitterVisitor.ctx.mv.visitInsn(Opcodes.SWAP); // Call join(separator, list) emitterVisitor.ctx.mv.visitMethodInsn(Opcodes.INVOKESTATIC, "org/perlonjava/operators/StringOperators", "join", "(Lorg/perlonjava/runtime/RuntimeScalar;Lorg/perlonjava/runtime/RuntimeBase;)Lorg/perlonjava/runtime/RuntimeScalar;", false); // Use the joined string as the hash key + emitterVisitor.ctx.mv.visitVarInsn(Opcodes.ALOAD, leftSlot); + emitterVisitor.ctx.mv.visitInsn(Opcodes.SWAP); emitterVisitor.ctx.mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/perlonjava/runtime/RuntimeHash", hashOperation, "(Lorg/perlonjava/runtime/RuntimeScalar;)Lorg/perlonjava/runtime/RuntimeScalar;", false); + + if (pooledSep) { + emitterVisitor.ctx.javaClassInfo.releaseSpillSlot(); + } + } + + if (pooledLeft) { + emitterVisitor.ctx.javaClassInfo.releaseSpillSlot(); } EmitOperator.handleVoidContext(emitterVisitor); @@ -306,6 +348,13 @@ public static void handleHashElementOperator(EmitterVisitor emitterVisitor, Bina emitterVisitor.ctx.logDebug("visit(BinaryOperatorNode) @var{} " + varNode); varNode.accept(emitterVisitor.with(RuntimeContextType.LIST)); // target - left parameter + int leftSlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot(); + boolean pooledLeft = leftSlot >= 0; + if (!pooledLeft) { + leftSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); + } + emitterVisitor.ctx.mv.visitVarInsn(Opcodes.ASTORE, leftSlot); + // emit the {x} as a RuntimeList ListNode nodeRight = ((HashLiteralNode) node.right).asListNode(); emitterVisitor.ctx.logDebug("visit(BinaryOperatorNode) @var{} as listNode: " + nodeRight); @@ -321,9 +370,27 @@ public static void handleHashElementOperator(EmitterVisitor emitterVisitor, Bina emitterVisitor.ctx.logDebug("visit(BinaryOperatorNode) $var{} autoquote " + node.right); nodeRight.accept(emitterVisitor.with(RuntimeContextType.LIST)); + int keyListSlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot(); + boolean pooledKeyList = keyListSlot >= 0; + if (!pooledKeyList) { + keyListSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); + } + emitterVisitor.ctx.mv.visitVarInsn(Opcodes.ASTORE, keyListSlot); + + emitterVisitor.ctx.mv.visitVarInsn(Opcodes.ALOAD, leftSlot); + emitterVisitor.ctx.mv.visitVarInsn(Opcodes.ALOAD, keyListSlot); + emitterVisitor.ctx.mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/perlonjava/runtime/RuntimeHash", hashOperation + "Slice", "(Lorg/perlonjava/runtime/RuntimeList;)Lorg/perlonjava/runtime/RuntimeList;", false); + if (pooledKeyList) { + emitterVisitor.ctx.javaClassInfo.releaseSpillSlot(); + } + + if (pooledLeft) { + emitterVisitor.ctx.javaClassInfo.releaseSpillSlot(); + } + // Handle context conversion for hash slices if (emitterVisitor.ctx.contextType == RuntimeContextType.SCALAR) { // Convert RuntimeList to RuntimeScalar (Perl scalar slice semantics) @@ -349,6 +416,13 @@ public static void handleHashElementOperator(EmitterVisitor emitterVisitor, Bina emitterVisitor.ctx.logDebug("visit(BinaryOperatorNode) @var{} " + varNode); varNode.accept(emitterVisitor.with(RuntimeContextType.LIST)); // target - left parameter + int leftSlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot(); + boolean pooledLeft = leftSlot >= 0; + if (!pooledLeft) { + leftSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); + } + emitterVisitor.ctx.mv.visitVarInsn(Opcodes.ASTORE, leftSlot); + // emit the {x} as a RuntimeList ListNode nodeRight = ((HashLiteralNode) node.right).asListNode(); emitterVisitor.ctx.logDebug("visit(BinaryOperatorNode) @var{} as listNode: " + nodeRight); @@ -364,9 +438,27 @@ public static void handleHashElementOperator(EmitterVisitor emitterVisitor, Bina emitterVisitor.ctx.logDebug("visit(BinaryOperatorNode) $var{} autoquote " + node.right); nodeRight.accept(emitterVisitor.with(RuntimeContextType.LIST)); + int keyListSlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot(); + boolean pooledKeyList = keyListSlot >= 0; + if (!pooledKeyList) { + keyListSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); + } + emitterVisitor.ctx.mv.visitVarInsn(Opcodes.ASTORE, keyListSlot); + + emitterVisitor.ctx.mv.visitVarInsn(Opcodes.ALOAD, leftSlot); + emitterVisitor.ctx.mv.visitVarInsn(Opcodes.ALOAD, keyListSlot); + emitterVisitor.ctx.mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/perlonjava/runtime/RuntimeHash", "getKeyValueSlice", "(Lorg/perlonjava/runtime/RuntimeList;)Lorg/perlonjava/runtime/RuntimeList;", false); + if (pooledKeyList) { + emitterVisitor.ctx.javaClassInfo.releaseSpillSlot(); + } + + if (pooledLeft) { + emitterVisitor.ctx.javaClassInfo.releaseSpillSlot(); + } + // Handle context conversion for key/value slice if (emitterVisitor.ctx.contextType == RuntimeContextType.SCALAR) { emitterVisitor.ctx.mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/perlonjava/runtime/RuntimeList", @@ -436,12 +528,12 @@ static void handleArrowOperator(EmitterVisitor emitterVisitor, BinaryOperatorNod // Push __SUB__ handleSelfCallOperator(emitterVisitor.with(RuntimeContextType.SCALAR), null); - int objectSlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot(); - boolean pooledObject = objectSlot >= 0; - if (!pooledObject) { - objectSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); + int subSlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot(); + boolean pooledSub = subSlot >= 0; + if (!pooledSub) { + subSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); } - mv.visitVarInsn(Opcodes.ASTORE, objectSlot); + mv.visitVarInsn(Opcodes.ASTORE, subSlot); int methodSlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot(); boolean pooledMethod = methodSlot >= 0; @@ -450,12 +542,12 @@ static void handleArrowOperator(EmitterVisitor emitterVisitor, BinaryOperatorNod } mv.visitVarInsn(Opcodes.ASTORE, methodSlot); - int subSlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot(); - boolean pooledSub = subSlot >= 0; - if (!pooledSub) { - subSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); + int objectSlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot(); + boolean pooledObject = objectSlot >= 0; + if (!pooledObject) { + objectSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); } - mv.visitVarInsn(Opcodes.ASTORE, subSlot); + mv.visitVarInsn(Opcodes.ASTORE, objectSlot); // Generate native RuntimeBase[] array for parameters instead of RuntimeList ListNode paramList = ListNode.makeList(arguments); diff --git a/src/main/java/org/perlonjava/codegen/EmitBlock.java b/src/main/java/org/perlonjava/codegen/EmitBlock.java index 2dbf29cfb..f218bf3c5 100644 --- a/src/main/java/org/perlonjava/codegen/EmitBlock.java +++ b/src/main/java/org/perlonjava/codegen/EmitBlock.java @@ -32,6 +32,14 @@ public static void emitBlock(EmitterVisitor emitterVisitor, BlockNode node) { emitterVisitor.with(RuntimeContextType.VOID); // statements in the middle of the block have context VOID List list = node.elements; + int lastNonNullIndex = -1; + for (int i = list.size() - 1; i >= 0; i--) { + if (list.get(i) != null) { + lastNonNullIndex = i; + break; + } + } + // Create labels for the block as a loop, like `L1: {...}` Label redoLabel = new Label(); Label nextLabel = new Label(); @@ -89,7 +97,7 @@ public static void emitBlock(EmitterVisitor emitterVisitor, BlockNode node) { ByteCodeSourceMapper.setDebugInfoLineNumber(emitterVisitor.ctx, element.getIndex()); // Emit the statement with current context - if (i == list.size() - 1) { + if (i == lastNonNullIndex) { // Special case for the last element emitterVisitor.ctx.logDebug("Last element: " + element); element.accept(emitterVisitor); diff --git a/src/main/java/org/perlonjava/codegen/EmitControlFlow.java b/src/main/java/org/perlonjava/codegen/EmitControlFlow.java index 48a96570d..407355795 100644 --- a/src/main/java/org/perlonjava/codegen/EmitControlFlow.java +++ b/src/main/java/org/perlonjava/codegen/EmitControlFlow.java @@ -103,7 +103,7 @@ static void handleNextOperator(EmitterContext ctx, OperatorNode node) { ctx.logDebug("visit(next): asmStackLevel: " + ctx.javaClassInfo.stackLevelManager.getStackLevel()); // Clean up the stack before jumping by popping values up to the loop's stack level - ctx.javaClassInfo.stackLevelManager.emitPopInstructions(ctx.mv, loopLabels.asmStackLevel); + ctx.javaClassInfo.resetStackLevel(); // Handle return values based on context if (loopLabels.context != RuntimeContextType.VOID) { @@ -160,7 +160,22 @@ static void handleReturnOperator(EmitterVisitor emitterVisitor, OperatorNode nod } // Clean up stack before return - ctx.javaClassInfo.stackLevelManager.emitPopInstructions(ctx.mv, 0); + ctx.javaClassInfo.resetStackLevel(); + + // Bare return: ensure we still push a value (empty list) before jumping to returnLabel. + // The method epilogue expects a RuntimeBase/RuntimeList on the JVM operand stack. + if (node.operand == null || (node.operand instanceof ListNode list && list.elements.isEmpty())) { + ctx.mv.visitTypeInsn(Opcodes.NEW, "org/perlonjava/runtime/RuntimeList"); + ctx.mv.visitInsn(Opcodes.DUP); + ctx.mv.visitMethodInsn( + Opcodes.INVOKESPECIAL, + "org/perlonjava/runtime/RuntimeList", + "", + "()V", + false); + ctx.mv.visitJumpInsn(Opcodes.GOTO, ctx.javaClassInfo.returnLabel); + return; + } // Handle special case for single-element return lists if (node.operand instanceof ListNode list) { @@ -191,7 +206,7 @@ static void handleGotoSubroutine(EmitterVisitor emitterVisitor, OperatorNode sub ctx.logDebug("visit(goto &sub): Emitting TAILCALL marker"); // Clean up stack before creating the marker - ctx.javaClassInfo.stackLevelManager.emitPopInstructions(ctx.mv, 0); + ctx.javaClassInfo.resetStackLevel(); // Create new RuntimeControlFlowList for tail call ctx.mv.visitTypeInsn(Opcodes.NEW, "org/perlonjava/runtime/RuntimeControlFlowList"); @@ -309,9 +324,20 @@ static void handleGotoLabel(EmitterVisitor emitterVisitor, OperatorNode node) { "", "(Lorg/perlonjava/runtime/ControlFlowType;Ljava/lang/String;Ljava/lang/String;I)V", false); - - // Clean stack and jump to returnLabel - ctx.javaClassInfo.stackLevelManager.emitPopInstructions(ctx.mv, 0); + + int markerSlot = ctx.javaClassInfo.acquireSpillSlot(); + boolean pooledMarker = markerSlot >= 0; + if (!pooledMarker) { + markerSlot = ctx.symbolTable.allocateLocalVariable(); + } + ctx.mv.visitVarInsn(Opcodes.ASTORE, markerSlot); + + // Clean stack and jump to returnLabel with the marker on stack. + ctx.javaClassInfo.resetStackLevel(); + ctx.mv.visitVarInsn(Opcodes.ALOAD, markerSlot); + if (pooledMarker) { + ctx.javaClassInfo.releaseSpillSlot(); + } ctx.mv.visitJumpInsn(Opcodes.GOTO, ctx.javaClassInfo.returnLabel); return; } @@ -346,16 +372,27 @@ static void handleGotoLabel(EmitterVisitor emitterVisitor, OperatorNode node) { "", "(Lorg/perlonjava/runtime/ControlFlowType;Ljava/lang/String;Ljava/lang/String;I)V", false); - - // Clean stack and jump to returnLabel - ctx.javaClassInfo.stackLevelManager.emitPopInstructions(ctx.mv, 0); + + int markerSlot = ctx.javaClassInfo.acquireSpillSlot(); + boolean pooledMarker = markerSlot >= 0; + if (!pooledMarker) { + markerSlot = ctx.symbolTable.allocateLocalVariable(); + } + ctx.mv.visitVarInsn(Opcodes.ASTORE, markerSlot); + + // Clean stack and jump to returnLabel with the marker on stack. + ctx.javaClassInfo.resetStackLevel(); + ctx.mv.visitVarInsn(Opcodes.ALOAD, markerSlot); + if (pooledMarker) { + ctx.javaClassInfo.releaseSpillSlot(); + } ctx.mv.visitJumpInsn(Opcodes.GOTO, ctx.javaClassInfo.returnLabel); return; } // Local goto: use fast GOTO (existing code) // Clean up stack before jumping to maintain stack consistency - ctx.javaClassInfo.stackLevelManager.emitPopInstructions(ctx.mv, targetLabel.asmStackLevel); + ctx.javaClassInfo.resetStackLevel(); // Emit the goto instruction ctx.mv.visitJumpInsn(Opcodes.GOTO, targetLabel.gotoLabel); diff --git a/src/main/java/org/perlonjava/codegen/EmitForeach.java b/src/main/java/org/perlonjava/codegen/EmitForeach.java index d9751c623..1c8c86033 100644 --- a/src/main/java/org/perlonjava/codegen/EmitForeach.java +++ b/src/main/java/org/perlonjava/codegen/EmitForeach.java @@ -39,9 +39,11 @@ public class EmitForeach { public static void emitFor1(EmitterVisitor emitterVisitor, For1Node node) { emitterVisitor.ctx.logDebug("FOR1 start"); + Node variableNode = node.variable; + // Check if the loop variable is a complex lvalue expression like $$f // If so, emit as while loop with explicit assignment - if (node.variable instanceof OperatorNode opNode && + if (variableNode instanceof OperatorNode opNode && opNode.operand instanceof OperatorNode nestedOpNode && opNode.operator.equals("$") && nestedOpNode.operator.equals("$")) { @@ -60,7 +62,7 @@ public static void emitFor1(EmitterVisitor emitterVisitor, For1Node node) { // Check if the variable is global boolean loopVariableIsGlobal = false; String globalVarName = null; - if (node.variable instanceof OperatorNode opNode && opNode.operator.equals("$")) { + if (variableNode instanceof OperatorNode opNode && opNode.operator.equals("$")) { if (opNode.operand instanceof IdentifierNode idNode) { String varName = opNode.operator + idNode.name; int varIndex = emitterVisitor.ctx.symbolTable.getVariableIndex(varName); @@ -72,7 +74,7 @@ public static void emitFor1(EmitterVisitor emitterVisitor, For1Node node) { } // First declare the variables if it's a my/our operator - if (node.variable instanceof OperatorNode opNode && + if (variableNode instanceof OperatorNode opNode && (opNode.operator.equals("my") || opNode.operator.equals("our"))) { boolean isWarningEnabled = Warnings.warningManager.isWarningEnabled("redefine"); if (isWarningEnabled) { @@ -80,9 +82,26 @@ public static void emitFor1(EmitterVisitor emitterVisitor, For1Node node) { Warnings.warningManager.setWarningState("redefine", false); } // emit the variable declarations - node.variable.accept(emitterVisitor.with(RuntimeContextType.VOID)); - // rewrite the variable node without the declaration - node.variable = opNode.operand; + variableNode.accept(emitterVisitor.with(RuntimeContextType.VOID)); + // Use the variable node without the declaration for codegen, but do not mutate the AST. + variableNode = opNode.operand; + + if (opNode.operator.equals("my") && variableNode instanceof OperatorNode declVar + && declVar.operator.equals("$") && declVar.operand instanceof IdentifierNode declId) { + String varName = declVar.operator + declId.name; + int varIndex = emitterVisitor.ctx.symbolTable.getVariableIndex(varName); + if (varIndex == -1) { + varIndex = emitterVisitor.ctx.symbolTable.addVariable(varName, "my", declVar); + mv.visitTypeInsn(Opcodes.NEW, "org/perlonjava/runtime/RuntimeScalar"); + mv.visitInsn(Opcodes.DUP); + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, + "org/perlonjava/runtime/RuntimeScalar", + "", + "()V", + false); + mv.visitVarInsn(Opcodes.ASTORE, varIndex); + } + } if (isWarningEnabled) { // restore warnings @@ -93,6 +112,26 @@ public static void emitFor1(EmitterVisitor emitterVisitor, For1Node node) { loopVariableIsGlobal = false; } + if (variableNode instanceof OperatorNode opNode && + opNode.operator.equals("state") && opNode.operand instanceof OperatorNode declVar + && declVar.operator.equals("$") && declVar.operand instanceof IdentifierNode declId) { + variableNode.accept(emitterVisitor.with(RuntimeContextType.VOID)); + variableNode = opNode.operand; + String varName = declVar.operator + declId.name; + int varIndex = emitterVisitor.ctx.symbolTable.getVariableIndex(varName); + if (varIndex == -1) { + varIndex = emitterVisitor.ctx.symbolTable.addVariable(varName, "state", declVar); + mv.visitTypeInsn(Opcodes.NEW, "org/perlonjava/runtime/RuntimeScalar"); + mv.visitInsn(Opcodes.DUP); + mv.visitMethodInsn(Opcodes.INVOKESPECIAL, + "org/perlonjava/runtime/RuntimeScalar", + "", + "()V", + false); + mv.visitVarInsn(Opcodes.ASTORE, varIndex); + } + } + // For global $_ as loop variable, we need to: // 1. Evaluate the list first (before any localization takes effect) // 2. For statement modifiers: localize $_ ourselves @@ -122,6 +161,8 @@ public static void emitFor1(EmitterVisitor emitterVisitor, For1Node node) { Local.localRecord localRecord = Local.localSetup(emitterVisitor.ctx, node, mv); + int iteratorIndex = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); + // Check if the list was pre-evaluated by EmitBlock (for nested for loops with local $_) if (node.preEvaluatedArrayIndex >= 0) { // Use the pre-evaluated array that was stored before local $_ was emitted @@ -140,6 +181,7 @@ public static void emitFor1(EmitterVisitor emitterVisitor, For1Node node) { // Get iterator from the pre-evaluated array mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/perlonjava/runtime/RuntimeArray", "iterator", "()Ljava/util/Iterator;", false); + mv.visitVarInsn(Opcodes.ASTORE, iteratorIndex); } else if (isGlobalUnderscore) { // Global $_ as loop variable: evaluate list to array of aliases first // This preserves aliasing semantics while ensuring list is evaluated before any @@ -160,10 +202,12 @@ public static void emitFor1(EmitterVisitor emitterVisitor, For1Node node) { // Get iterator from the array of aliases mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/perlonjava/runtime/RuntimeArray", "iterator", "()Ljava/util/Iterator;", false); + mv.visitVarInsn(Opcodes.ASTORE, iteratorIndex); } else { // Standard path: obtain iterator for the list node.list.accept(emitterVisitor.with(RuntimeContextType.LIST)); mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/perlonjava/runtime/RuntimeBase", "iterator", "()Ljava/util/Iterator;", false); + mv.visitVarInsn(Opcodes.ASTORE, iteratorIndex); } mv.visitLabel(loopStart); @@ -172,30 +216,26 @@ public static void emitFor1(EmitterVisitor emitterVisitor, For1Node node) { EmitStatement.emitSignalCheck(mv); // Check if iterator has more elements - mv.visitInsn(Opcodes.DUP); + mv.visitVarInsn(Opcodes.ALOAD, iteratorIndex); mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, "java/util/Iterator", "hasNext", "()Z", true); mv.visitJumpInsn(Opcodes.IFEQ, loopEnd); // Handle multiple variables case - if (node.variable instanceof ListNode varList) { + if (variableNode instanceof ListNode varList) { for (int i = 0; i < varList.elements.size(); i++) { - // Duplicate iterator - mv.visitInsn(Opcodes.DUP); - - // Check if iterator has more elements - mv.visitInsn(Opcodes.DUP); - mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, "java/util/Iterator", "hasNext", "()Z", true); Label hasValueLabel = new Label(); Label endValueLabel = new Label(); + mv.visitVarInsn(Opcodes.ALOAD, iteratorIndex); + mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, "java/util/Iterator", "hasNext", "()Z", true); mv.visitJumpInsn(Opcodes.IFNE, hasValueLabel); // No more elements - assign undef - mv.visitInsn(Opcodes.POP); // Pop the iterator copy EmitOperator.emitUndef(mv); mv.visitJumpInsn(Opcodes.GOTO, endValueLabel); // Has more elements - get next value mv.visitLabel(hasValueLabel); + mv.visitVarInsn(Opcodes.ALOAD, iteratorIndex); mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, "java/util/Iterator", "next", "()Ljava/lang/Object;", true); mv.visitTypeInsn(Opcodes.CHECKCAST, "org/perlonjava/runtime/RuntimeScalar"); @@ -212,7 +252,7 @@ public static void emitFor1(EmitterVisitor emitterVisitor, For1Node node) { } } else { // Original single variable case - mv.visitInsn(Opcodes.DUP); + mv.visitVarInsn(Opcodes.ALOAD, iteratorIndex); mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, "java/util/Iterator", "next", "()Ljava/lang/Object;", true); mv.visitTypeInsn(Opcodes.CHECKCAST, "org/perlonjava/runtime/RuntimeScalar"); @@ -225,7 +265,7 @@ public static void emitFor1(EmitterVisitor emitterVisitor, For1Node node) { "aliasGlobalVariable", "(Ljava/lang/String;Lorg/perlonjava/runtime/RuntimeScalar;)V", false); - } else if (node.variable instanceof OperatorNode operatorNode) { + } else if (variableNode instanceof OperatorNode operatorNode) { // Local variable case String varName = operatorNode.operator + ((IdentifierNode) operatorNode.operand).name; int varIndex = emitterVisitor.ctx.symbolTable.getVariableIndex(varName); @@ -234,8 +274,6 @@ public static void emitFor1(EmitterVisitor emitterVisitor, For1Node node) { } } - emitterVisitor.ctx.javaClassInfo.incrementStackLevel(1); - Label redoLabel = new Label(); mv.visitLabel(redoLabel); @@ -308,9 +346,6 @@ public static void emitFor1(EmitterVisitor emitterVisitor, For1Node node) { emitterVisitor.ctx.symbolTable.exitScope(scopeIndex); - emitterVisitor.ctx.javaClassInfo.decrementStackLevel(1); - mv.visitInsn(Opcodes.POP); - if (emitterVisitor.ctx.contextType != RuntimeContextType.VOID) { // Foreach loop returns empty string when it completes normally // This is different from an empty list in scalar context (which would be undef) @@ -530,18 +565,21 @@ private static void emitFor1AsWhileLoop(EmitterVisitor emitterVisitor, For1Node node.list.accept(emitterVisitor.with(RuntimeContextType.LIST)); mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/perlonjava/runtime/RuntimeBase", "iterator", "()Ljava/util/Iterator;", false); + int iteratorIndex = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); + mv.visitVarInsn(Opcodes.ASTORE, iteratorIndex); + mv.visitLabel(loopStart); // Check for pending signals (alarm, etc.) at loop entry EmitStatement.emitSignalCheck(mv); // Check if iterator has more elements - mv.visitInsn(Opcodes.DUP); + mv.visitVarInsn(Opcodes.ALOAD, iteratorIndex); mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, "java/util/Iterator", "hasNext", "()Z", true); mv.visitJumpInsn(Opcodes.IFEQ, loopEnd); // Get next value - mv.visitInsn(Opcodes.DUP); + mv.visitVarInsn(Opcodes.ALOAD, iteratorIndex); mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, "java/util/Iterator", "next", "()Ljava/lang/Object;", true); mv.visitTypeInsn(Opcodes.CHECKCAST, "org/perlonjava/runtime/RuntimeScalar"); @@ -553,8 +591,6 @@ private static void emitFor1AsWhileLoop(EmitterVisitor emitterVisitor, For1Node "set", "(Lorg/perlonjava/runtime/RuntimeScalar;)Lorg/perlonjava/runtime/RuntimeScalar;", false); mv.visitInsn(Opcodes.POP); - emitterVisitor.ctx.javaClassInfo.incrementStackLevel(1); - Label redoLabel = new Label(); mv.visitLabel(redoLabel); @@ -573,9 +609,6 @@ private static void emitFor1AsWhileLoop(EmitterVisitor emitterVisitor, For1Node mv.visitLabel(loopEnd); - emitterVisitor.ctx.javaClassInfo.decrementStackLevel(1); - mv.visitInsn(Opcodes.POP); - if (emitterVisitor.ctx.contextType != RuntimeContextType.VOID) { // Foreach loop returns empty string when it completes normally // This is different from an empty list in scalar context (which would be undef) diff --git a/src/main/java/org/perlonjava/codegen/EmitLiteral.java b/src/main/java/org/perlonjava/codegen/EmitLiteral.java index 4599020ef..bc1c22471 100644 --- a/src/main/java/org/perlonjava/codegen/EmitLiteral.java +++ b/src/main/java/org/perlonjava/codegen/EmitLiteral.java @@ -70,23 +70,45 @@ public static void emitArrayLiteral(EmitterVisitor emitterVisitor, ArrayLiteralN mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "org/perlonjava/runtime/RuntimeArray", "", "()V", false); // Stack: [RuntimeArray] + JavaClassInfo.SpillRef arrayRef = null; + arrayRef = emitterVisitor.ctx.javaClassInfo.tryAcquirePooledSpillRef(); + if (arrayRef != null) { + emitterVisitor.ctx.javaClassInfo.storeSpillRef(mv, arrayRef); + } + // Stack: [] (if arrayRef != null) else [RuntimeArray] + // Populate the array with elements for (Node element : node.elements) { - // Duplicate the RuntimeArray reference for the add operation - mv.visitInsn(Opcodes.DUP); - // Stack: [RuntimeArray] [RuntimeArray] + // Generate code for the element in LIST context + if (arrayRef != null) { + element.accept(elementContext); + JavaClassInfo.SpillRef elementRef = emitterVisitor.ctx.javaClassInfo.acquireSpillRefOrAllocate(emitterVisitor.ctx.symbolTable); + emitterVisitor.ctx.javaClassInfo.storeSpillRef(mv, elementRef); - emitterVisitor.ctx.javaClassInfo.incrementStackLevel(2); + emitterVisitor.ctx.javaClassInfo.loadSpillRef(mv, arrayRef); + emitterVisitor.ctx.javaClassInfo.loadSpillRef(mv, elementRef); + emitterVisitor.ctx.javaClassInfo.releaseSpillRef(elementRef); - // Generate code for the element in LIST context - element.accept(elementContext); - // Stack: [RuntimeArray] [RuntimeArray] [element] + // Add the element to the array + addElementToArray(mv, element); + // Stack: [] + } else { + mv.visitInsn(Opcodes.DUP); + // Stack: [RuntimeArray] - emitterVisitor.ctx.javaClassInfo.decrementStackLevel(2); + emitterVisitor.ctx.javaClassInfo.incrementStackLevel(1); + element.accept(elementContext); + emitterVisitor.ctx.javaClassInfo.decrementStackLevel(1); + + // Add the element to the array + addElementToArray(mv, element); + // Stack: [RuntimeArray] + } + } - // Add the element to the array - addElementToArray(mv, element); - // Stack: [RuntimeArray] + if (arrayRef != null) { + emitterVisitor.ctx.javaClassInfo.loadSpillRef(mv, arrayRef); + emitterVisitor.ctx.javaClassInfo.releaseSpillRef(arrayRef); } // Convert the array to a reference (array literals produce references) @@ -353,23 +375,47 @@ public static void emitList(EmitterVisitor emitterVisitor, ListNode node) { mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "org/perlonjava/runtime/RuntimeList", "", "()V", false); // Stack: [RuntimeList] + JavaClassInfo.SpillRef listRef = null; + listRef = emitterVisitor.ctx.javaClassInfo.tryAcquirePooledSpillRef(); + if (listRef != null) { + emitterVisitor.ctx.javaClassInfo.storeSpillRef(mv, listRef); + } + // Stack: [] (if listRef != null) else [RuntimeList] + // Populate the list with elements for (Node element : node.elements) { - // Duplicate the RuntimeList reference for the add operation - mv.visitInsn(Opcodes.DUP); - // Stack: [RuntimeList] [RuntimeList] + if (listRef != null) { + // Generate code for the element with an empty operand stack so non-local control flow + // cannot leak extra operands. + element.accept(emitterVisitor); + JavaClassInfo.SpillRef elementRef = emitterVisitor.ctx.javaClassInfo.acquireSpillRefOrAllocate(emitterVisitor.ctx.symbolTable); + emitterVisitor.ctx.javaClassInfo.storeSpillRef(mv, elementRef); - emitterVisitor.ctx.javaClassInfo.incrementStackLevel(2); + emitterVisitor.ctx.javaClassInfo.loadSpillRef(mv, listRef); + emitterVisitor.ctx.javaClassInfo.loadSpillRef(mv, elementRef); + emitterVisitor.ctx.javaClassInfo.releaseSpillRef(elementRef); - // Generate code for the element, preserving the list's context - element.accept(emitterVisitor); - // Stack: [RuntimeList] [RuntimeList] [element] + // Add the element to the list + addElementToList(mv, element, contextType); + // Stack: [] + } else { + mv.visitInsn(Opcodes.DUP); + // Stack: [RuntimeList] + + emitterVisitor.ctx.javaClassInfo.incrementStackLevel(1); + // Generate code for the element, preserving the list's context + element.accept(emitterVisitor); + emitterVisitor.ctx.javaClassInfo.decrementStackLevel(1); - emitterVisitor.ctx.javaClassInfo.decrementStackLevel(2); + // Add the element to the list + addElementToList(mv, element, contextType); + // Stack: [RuntimeList] + } + } - // Add the element to the list - addElementToList(mv, element, contextType); - // Stack: [RuntimeList] + if (listRef != null) { + emitterVisitor.ctx.javaClassInfo.loadSpillRef(mv, listRef); + emitterVisitor.ctx.javaClassInfo.releaseSpillRef(listRef); } emitterVisitor.ctx.logDebug("visit(ListNode) end"); } diff --git a/src/main/java/org/perlonjava/codegen/EmitLogicalOperator.java b/src/main/java/org/perlonjava/codegen/EmitLogicalOperator.java index fb914bc88..9f096b6a8 100644 --- a/src/main/java/org/perlonjava/codegen/EmitLogicalOperator.java +++ b/src/main/java/org/perlonjava/codegen/EmitLogicalOperator.java @@ -81,11 +81,21 @@ static void emitLogicalAssign(EmitterVisitor emitterVisitor, BinaryOperatorNode MethodVisitor mv = emitterVisitor.ctx.mv; Label endLabel = new Label(); // Label for the end of the operation + // Evaluate the left side once and spill it to keep the operand stack clean. + // This is critical when the right side may perform non-local control flow (return/last/next/redo) + // and jump away during evaluation. node.left.accept(emitterVisitor.with(RuntimeContextType.SCALAR)); // target - left parameter - // The left parameter is in the stack + int leftSlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot(); + boolean pooledLeft = leftSlot >= 0; + if (!pooledLeft) { + leftSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); + } + mv.visitVarInsn(Opcodes.ASTORE, leftSlot); + + // Reload left for boolean test + mv.visitVarInsn(Opcodes.ALOAD, leftSlot); mv.visitInsn(Opcodes.DUP); - // Stack is [left, left] // Convert the result to a boolean mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/perlonjava/runtime/RuntimeBase", getBoolean, "()Z", false); @@ -94,18 +104,29 @@ static void emitLogicalAssign(EmitterVisitor emitterVisitor, BinaryOperatorNode // If the boolean value is true, jump to endLabel (we keep the left operand) mv.visitJumpInsn(compareOpcode, endLabel); - node.right.accept(emitterVisitor.with(RuntimeContextType.SCALAR)); // Evaluate right operand in scalar context - // Stack is [left, right] + mv.visitInsn(Opcodes.POP); - mv.visitInsn(Opcodes.SWAP); // Stack becomes [right, left] + // Left was false: evaluate right operand in scalar context. + // Stack is clean here, so any non-local control flow jump doesn't leave stray values behind. + node.right.accept(emitterVisitor.with(RuntimeContextType.SCALAR)); + + // Load left back for assignment + mv.visitVarInsn(Opcodes.ALOAD, leftSlot); + // Stack is [right, left] + + mv.visitInsn(Opcodes.SWAP); // Stack becomes [left, right] // Assign right to left - mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/perlonjava/runtime/RuntimeBase", "addToScalar", "(Lorg/perlonjava/runtime/RuntimeScalar;)Lorg/perlonjava/runtime/RuntimeScalar;", false); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/perlonjava/runtime/RuntimeScalar", "set", "(Lorg/perlonjava/runtime/RuntimeScalar;)Lorg/perlonjava/runtime/RuntimeScalar;", false); // Stack is [right] // At this point, the stack either has the left (if it was true) or the right (if left was false) mv.visitLabel(endLabel); + if (pooledLeft) { + emitterVisitor.ctx.javaClassInfo.releaseSpillSlot(); + } + // If the context is VOID, pop the result from the stack EmitOperator.handleVoidContext(emitterVisitor); } @@ -236,6 +257,25 @@ private static void emitLogicalOperatorSimple(EmitterVisitor emitterVisitor, Bin MethodVisitor mv = emitterVisitor.ctx.mv; Label endLabel = new Label(); + if (emitterVisitor.ctx.contextType == RuntimeContextType.VOID) { + node.left.accept(emitterVisitor.with(RuntimeContextType.SCALAR)); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/perlonjava/runtime/RuntimeBase", getBoolean, "()Z", false); + mv.visitJumpInsn(compareOpcode, endLabel); + + // The condition value has been consumed by getBoolean() and the conditional jump. + // Keep StackLevelManager in sync with the actual operand stack (empty) so that + // downstream non-local control flow (return/last/next/redo/goto) doesn't emit POPs + // based on stale stack accounting. + emitterVisitor.ctx.javaClassInfo.resetStackLevel(); + + node.right.accept(emitterVisitor.with(RuntimeContextType.SCALAR)); + mv.visitInsn(Opcodes.POP); + + mv.visitLabel(endLabel); + emitterVisitor.ctx.javaClassInfo.resetStackLevel(); + return; + } + // check if the right operand contains a variable declaration OperatorNode declaration = FindDeclarationVisitor.findOperator(node.right, "my"); if (declaration != null) { @@ -274,29 +314,60 @@ public static void emitTernaryOperator(EmitterVisitor emitterVisitor, TernaryOpe Label elseLabel = new Label(); Label endLabel = new Label(); + MethodVisitor mv = emitterVisitor.ctx.mv; + int contextType = emitterVisitor.ctx.contextType; + // Visit the condition node in scalar context node.condition.accept(emitterVisitor.with(RuntimeContextType.SCALAR)); // Convert the result to a boolean - emitterVisitor.ctx.mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/perlonjava/runtime/RuntimeBase", "getBoolean", "()Z", false); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/perlonjava/runtime/RuntimeBase", "getBoolean", "()Z", false); // Jump to the else label if the condition is false - emitterVisitor.ctx.mv.visitJumpInsn(Opcodes.IFEQ, elseLabel); + mv.visitJumpInsn(Opcodes.IFEQ, elseLabel); // Visit the then branch + if (contextType == RuntimeContextType.VOID) { + node.trueExpr.accept(emitterVisitor); + mv.visitJumpInsn(Opcodes.GOTO, endLabel); + + // Visit the else label + mv.visitLabel(elseLabel); + node.falseExpr.accept(emitterVisitor); + + // Visit the end label + mv.visitLabel(endLabel); + + emitterVisitor.ctx.logDebug("TERNARY_OP end"); + return; + } + + int resultSlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot(); + boolean usedSpillSlot = resultSlot != -1; + if (!usedSpillSlot) { + resultSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); + } + node.trueExpr.accept(emitterVisitor); + mv.visitVarInsn(Opcodes.ASTORE, resultSlot); // Jump to the end label after executing the then branch - emitterVisitor.ctx.mv.visitJumpInsn(Opcodes.GOTO, endLabel); + mv.visitJumpInsn(Opcodes.GOTO, endLabel); // Visit the else label - emitterVisitor.ctx.mv.visitLabel(elseLabel); + mv.visitLabel(elseLabel); // Visit the else branch node.falseExpr.accept(emitterVisitor); + mv.visitVarInsn(Opcodes.ASTORE, resultSlot); // Visit the end label - emitterVisitor.ctx.mv.visitLabel(endLabel); + mv.visitLabel(endLabel); + + mv.visitVarInsn(Opcodes.ALOAD, resultSlot); + if (usedSpillSlot) { + emitterVisitor.ctx.javaClassInfo.releaseSpillSlot(); + } emitterVisitor.ctx.logDebug("TERNARY_OP end"); } diff --git a/src/main/java/org/perlonjava/codegen/EmitOperator.java b/src/main/java/org/perlonjava/codegen/EmitOperator.java index dcee1748e..d13bde41b 100644 --- a/src/main/java/org/perlonjava/codegen/EmitOperator.java +++ b/src/main/java/org/perlonjava/codegen/EmitOperator.java @@ -114,9 +114,23 @@ static void handleBinmodeOperator(EmitterVisitor emitterVisitor, BinaryOperatorN // Emit the File Handle emitFileHandle(emitterVisitor.with(RuntimeContextType.SCALAR), node.left); + int handleSlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot(); + boolean pooledHandle = handleSlot >= 0; + if (!pooledHandle) { + handleSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); + } + emitterVisitor.ctx.mv.visitVarInsn(Opcodes.ASTORE, handleSlot); + // Accept the right operand in LIST context node.right.accept(emitterVisitor.with(RuntimeContextType.LIST)); + emitterVisitor.ctx.mv.visitVarInsn(Opcodes.ALOAD, handleSlot); + emitterVisitor.ctx.mv.visitInsn(Opcodes.SWAP); + + if (pooledHandle) { + emitterVisitor.ctx.javaClassInfo.releaseSpillSlot(); + } + // Emit the operator emitOperator(node, emitterVisitor); } @@ -194,6 +208,9 @@ static void handleOperator(EmitterVisitor emitterVisitor, OperatorNode node) { // Push context emitterVisitor.pushCallContext(); + int callContextSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); + emitterVisitor.ctx.mv.visitVarInsn(Opcodes.ISTORE, callContextSlot); + // Create array for varargs operators MethodVisitor mv = emitterVisitor.ctx.mv; @@ -201,12 +218,16 @@ static void handleOperator(EmitterVisitor emitterVisitor, OperatorNode node) { mv.visitIntInsn(Opcodes.SIPUSH, operand.elements.size()); mv.visitTypeInsn(Opcodes.ANEWARRAY, "org/perlonjava/runtime/RuntimeBase"); + int argsArraySlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot(); + boolean pooledArgsArray = argsArraySlot >= 0; + if (!pooledArgsArray) { + argsArraySlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); + } + mv.visitVarInsn(Opcodes.ASTORE, argsArraySlot); + // Populate the array with arguments int index = 0; for (Node arg : operand.elements) { - mv.visitInsn(Opcodes.DUP); // Duplicate array reference - mv.visitIntInsn(Opcodes.SIPUSH, index); - // Generate code for argument String argContext = (String) arg.getAnnotation("context"); if (argContext != null && argContext.equals("SCALAR")) { @@ -215,11 +236,32 @@ static void handleOperator(EmitterVisitor emitterVisitor, OperatorNode node) { arg.accept(listVisitor); } + int argSlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot(); + boolean pooledArg = argSlot >= 0; + if (!pooledArg) { + argSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); + } + mv.visitVarInsn(Opcodes.ASTORE, argSlot); + + mv.visitVarInsn(Opcodes.ALOAD, argsArraySlot); + mv.visitIntInsn(Opcodes.SIPUSH, index); + mv.visitVarInsn(Opcodes.ALOAD, argSlot); mv.visitInsn(Opcodes.AASTORE); // Store in array + + if (pooledArg) { + emitterVisitor.ctx.javaClassInfo.releaseSpillSlot(); + } index++; } + mv.visitVarInsn(Opcodes.ILOAD, callContextSlot); + mv.visitVarInsn(Opcodes.ALOAD, argsArraySlot); + emitOperator(node, emitterVisitor); + + if (pooledArgsArray) { + emitterVisitor.ctx.javaClassInfo.releaseSpillSlot(); + } } } @@ -373,8 +415,31 @@ static void handleRangeOperator(EmitterVisitor emitterVisitor, BinaryOperatorNod // Handles the 'substr' operator, which extracts a substring from a string. static void handleSubstr(EmitterVisitor emitterVisitor, BinaryOperatorNode node) { // Accept the left operand in SCALAR context and the right operand in LIST context. - node.left.accept(emitterVisitor.with(RuntimeContextType.SCALAR)); - node.right.accept(emitterVisitor.with(RuntimeContextType.LIST)); + // Spill the left operand before evaluating the right side so non-local control flow + // propagation can't jump to returnLabel with an extra value on the JVM operand stack. + if (ENABLE_SPILL_BINARY_LHS) { + MethodVisitor mv = emitterVisitor.ctx.mv; + node.left.accept(emitterVisitor.with(RuntimeContextType.SCALAR)); + + int leftSlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot(); + boolean pooled = leftSlot >= 0; + if (!pooled) { + leftSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); + } + mv.visitVarInsn(Opcodes.ASTORE, leftSlot); + + node.right.accept(emitterVisitor.with(RuntimeContextType.LIST)); + + mv.visitVarInsn(Opcodes.ALOAD, leftSlot); + mv.visitInsn(Opcodes.SWAP); + + if (pooled) { + emitterVisitor.ctx.javaClassInfo.releaseSpillSlot(); + } + } else { + node.left.accept(emitterVisitor.with(RuntimeContextType.SCALAR)); + node.right.accept(emitterVisitor.with(RuntimeContextType.LIST)); + } emitOperator(node, emitterVisitor); } diff --git a/src/main/java/org/perlonjava/codegen/EmitOperatorChained.java b/src/main/java/org/perlonjava/codegen/EmitOperatorChained.java index 9331ee66e..2b47af3c9 100644 --- a/src/main/java/org/perlonjava/codegen/EmitOperatorChained.java +++ b/src/main/java/org/perlonjava/codegen/EmitOperatorChained.java @@ -49,7 +49,22 @@ static public void emitChainedComparison(EmitterVisitor emitterVisitor, BinaryOp // Emit first comparison operands.get(0).accept(scalarVisitor); + + int leftSlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot(); + boolean pooledLeft = leftSlot >= 0; + if (!pooledLeft) { + leftSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); + } + emitterVisitor.ctx.mv.visitVarInsn(Opcodes.ASTORE, leftSlot); + operands.get(1).accept(scalarVisitor); + + emitterVisitor.ctx.mv.visitVarInsn(Opcodes.ALOAD, leftSlot); + emitterVisitor.ctx.mv.visitInsn(Opcodes.SWAP); + + if (pooledLeft) { + emitterVisitor.ctx.javaClassInfo.releaseSpillSlot(); + } // Create a BinaryOperatorNode for the first comparison BinaryOperatorNode firstCompNode = new BinaryOperatorNode( operators.get(0), @@ -77,8 +92,24 @@ static public void emitChainedComparison(EmitterVisitor emitterVisitor, BinaryOp // Previous was true, do next comparison emitterVisitor.ctx.mv.visitInsn(Opcodes.POP); + operands.get(i).accept(scalarVisitor); + + int chainLeftSlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot(); + boolean pooledChainLeft = chainLeftSlot >= 0; + if (!pooledChainLeft) { + chainLeftSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); + } + emitterVisitor.ctx.mv.visitVarInsn(Opcodes.ASTORE, chainLeftSlot); + operands.get(i + 1).accept(scalarVisitor); + + emitterVisitor.ctx.mv.visitVarInsn(Opcodes.ALOAD, chainLeftSlot); + emitterVisitor.ctx.mv.visitInsn(Opcodes.SWAP); + + if (pooledChainLeft) { + emitterVisitor.ctx.javaClassInfo.releaseSpillSlot(); + } // Create a BinaryOperatorNode for this comparison BinaryOperatorNode compNode = new BinaryOperatorNode( operators.get(i), diff --git a/src/main/java/org/perlonjava/codegen/EmitRegex.java b/src/main/java/org/perlonjava/codegen/EmitRegex.java index 1c2036e3f..3e797ab0b 100644 --- a/src/main/java/org/perlonjava/codegen/EmitRegex.java +++ b/src/main/java/org/perlonjava/codegen/EmitRegex.java @@ -37,7 +37,22 @@ static void handleBindRegex(EmitterVisitor emitterVisitor, BinaryOperatorNode no // Handle non-regex operator case (e.g., $v =~ $qr OR $v =~ qr//) node.right.accept(scalarVisitor); + + int regexSlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot(); + boolean pooledRegex = regexSlot >= 0; + if (!pooledRegex) { + regexSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); + } + emitterVisitor.ctx.mv.visitVarInsn(Opcodes.ASTORE, regexSlot); + node.left.accept(scalarVisitor); + + emitterVisitor.ctx.mv.visitVarInsn(Opcodes.ALOAD, regexSlot); + emitterVisitor.ctx.mv.visitInsn(Opcodes.SWAP); + + if (pooledRegex) { + emitterVisitor.ctx.javaClassInfo.releaseSpillSlot(); + } emitMatchRegex(emitterVisitor); // Use caller's context for regex matching } @@ -171,9 +186,23 @@ static void handleReplaceRegex(EmitterVisitor emitterVisitor, OperatorNode node) "org/perlonjava/regex/RuntimeRegex", "getReplacementRegex", "(Lorg/perlonjava/runtime/RuntimeScalar;Lorg/perlonjava/runtime/RuntimeScalar;Lorg/perlonjava/runtime/RuntimeScalar;)Lorg/perlonjava/runtime/RuntimeScalar;", false); + int regexSlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot(); + boolean pooledRegex = regexSlot >= 0; + if (!pooledRegex) { + regexSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); + } + emitterVisitor.ctx.mv.visitVarInsn(Opcodes.ASTORE, regexSlot); + // Use default variable $_ if none specified handleVariableBinding(operand, 3, scalarVisitor); + emitterVisitor.ctx.mv.visitVarInsn(Opcodes.ALOAD, regexSlot); + emitterVisitor.ctx.mv.visitInsn(Opcodes.SWAP); + + if (pooledRegex) { + emitterVisitor.ctx.javaClassInfo.releaseSpillSlot(); + } + emitMatchRegex(emitterVisitor); } @@ -214,9 +243,23 @@ static void handleMatchRegex(EmitterVisitor emitterVisitor, OperatorNode node) { "org/perlonjava/regex/RuntimeRegex", "getQuotedRegex", "(Lorg/perlonjava/runtime/RuntimeScalar;Lorg/perlonjava/runtime/RuntimeScalar;)Lorg/perlonjava/runtime/RuntimeScalar;", false); + int regexSlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot(); + boolean pooledRegex = regexSlot >= 0; + if (!pooledRegex) { + regexSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); + } + emitterVisitor.ctx.mv.visitVarInsn(Opcodes.ASTORE, regexSlot); + // Use default variable $_ if none specified handleVariableBinding(operand, 2, scalarVisitor); + emitterVisitor.ctx.mv.visitVarInsn(Opcodes.ALOAD, regexSlot); + emitterVisitor.ctx.mv.visitInsn(Opcodes.SWAP); + + if (pooledRegex) { + emitterVisitor.ctx.javaClassInfo.releaseSpillSlot(); + } + emitMatchRegex(emitterVisitor); } diff --git a/src/main/java/org/perlonjava/codegen/EmitSubroutine.java b/src/main/java/org/perlonjava/codegen/EmitSubroutine.java index fe1b48a51..7c84d5497 100644 --- a/src/main/java/org/perlonjava/codegen/EmitSubroutine.java +++ b/src/main/java/org/perlonjava/codegen/EmitSubroutine.java @@ -352,9 +352,25 @@ static void handleApplyOperator(EmitterVisitor emitterVisitor, BinaryOperatorNod Label propagateToCaller = new Label(); Label checkLoopLabels = new Label(); + int baseStackLevel = emitterVisitor.ctx.javaClassInfo.stackLevelManager.getStackLevel(); + int belowResultStackLevel = Math.max(0, baseStackLevel - 1); + JavaClassInfo.SpillRef[] baseSpills = new JavaClassInfo.SpillRef[belowResultStackLevel]; + // Store result in temp slot mv.visitVarInsn(Opcodes.ASTORE, emitterVisitor.ctx.javaClassInfo.controlFlowTempSlot); + // If the caller kept values on the JVM operand stack below the call result (e.g. a left operand), + // spill them now so control-flow propagation can jump to returnLabel with an empty stack. + for (int i = belowResultStackLevel - 1; i >= 0; i--) { + baseSpills[i] = emitterVisitor.ctx.javaClassInfo.acquireSpillRefOrAllocate(emitterVisitor.ctx.symbolTable); + emitterVisitor.ctx.javaClassInfo.storeSpillRef(mv, baseSpills[i]); + } + + // We just removed the entire base stack from the JVM operand stack via ASTORE. + // Keep StackLevelManager in sync; otherwise later emitPopInstructions() may POP the wrong values + // (including control-flow markers), producing invalid stackmap frames. + emitterVisitor.ctx.javaClassInfo.resetStackLevel(); + // Load and check if it's a control flow marker mv.visitVarInsn(Opcodes.ALOAD, emitterVisitor.ctx.javaClassInfo.controlFlowTempSlot); mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, @@ -411,21 +427,39 @@ static void handleApplyOperator(EmitterVisitor emitterVisitor, BinaryOperatorNod mv.visitVarInsn(Opcodes.ILOAD, emitterVisitor.ctx.javaClassInfo.controlFlowActionSlot); mv.visitInsn(Opcodes.ICONST_0); mv.visitJumpInsn(Opcodes.IF_ICMPNE, checkNext); - emitterVisitor.ctx.javaClassInfo.stackLevelManager.emitPopInstructions(mv, loopLabels.asmStackLevel); - mv.visitJumpInsn(Opcodes.GOTO, loopLabels.lastLabel); + if (loopLabels.lastLabel == emitterVisitor.ctx.javaClassInfo.returnLabel) { + mv.visitJumpInsn(Opcodes.GOTO, propagateToCaller); + } else { + emitterVisitor.ctx.javaClassInfo.stackLevelManager.emitPopInstructions(mv, loopLabels.asmStackLevel); + if (loopLabels.context != RuntimeContextType.VOID) { + EmitOperator.emitUndef(mv); + } + mv.visitJumpInsn(Opcodes.GOTO, loopLabels.lastLabel); + } // if (type == NEXT (1)) goto nextLabel mv.visitLabel(checkNext); mv.visitVarInsn(Opcodes.ILOAD, emitterVisitor.ctx.javaClassInfo.controlFlowActionSlot); mv.visitInsn(Opcodes.ICONST_1); mv.visitJumpInsn(Opcodes.IF_ICMPNE, checkRedo); - emitterVisitor.ctx.javaClassInfo.stackLevelManager.emitPopInstructions(mv, loopLabels.asmStackLevel); - mv.visitJumpInsn(Opcodes.GOTO, loopLabels.nextLabel); + if (loopLabels.nextLabel == emitterVisitor.ctx.javaClassInfo.returnLabel) { + mv.visitJumpInsn(Opcodes.GOTO, propagateToCaller); + } else { + emitterVisitor.ctx.javaClassInfo.stackLevelManager.emitPopInstructions(mv, loopLabels.asmStackLevel); + if (loopLabels.context != RuntimeContextType.VOID) { + EmitOperator.emitUndef(mv); + } + mv.visitJumpInsn(Opcodes.GOTO, loopLabels.nextLabel); + } // if (type == REDO (2)) goto redoLabel mv.visitLabel(checkRedo); - emitterVisitor.ctx.javaClassInfo.stackLevelManager.emitPopInstructions(mv, loopLabels.asmStackLevel); - mv.visitJumpInsn(Opcodes.GOTO, loopLabels.redoLabel); + if (loopLabels.redoLabel == emitterVisitor.ctx.javaClassInfo.returnLabel) { + mv.visitJumpInsn(Opcodes.GOTO, propagateToCaller); + } else { + emitterVisitor.ctx.javaClassInfo.stackLevelManager.emitPopInstructions(mv, loopLabels.asmStackLevel); + mv.visitJumpInsn(Opcodes.GOTO, loopLabels.redoLabel); + } mv.visitLabel(nextLoopCheck); } @@ -435,13 +469,27 @@ static void handleApplyOperator(EmitterVisitor emitterVisitor, BinaryOperatorNod // Propagate: jump to returnLabel with the marked list mv.visitLabel(propagateToCaller); - emitterVisitor.ctx.javaClassInfo.stackLevelManager.emitPopInstructions(mv, 0); + for (JavaClassInfo.SpillRef ref : baseSpills) { + if (ref != null) { + emitterVisitor.ctx.javaClassInfo.releaseSpillRef(ref); + } + } mv.visitVarInsn(Opcodes.ALOAD, emitterVisitor.ctx.javaClassInfo.controlFlowTempSlot); mv.visitJumpInsn(Opcodes.GOTO, emitterVisitor.ctx.javaClassInfo.returnLabel); // Not a control flow marker - load it back and continue mv.visitLabel(notControlFlow); + for (JavaClassInfo.SpillRef ref : baseSpills) { + if (ref != null) { + emitterVisitor.ctx.javaClassInfo.loadSpillRef(mv, ref); + emitterVisitor.ctx.javaClassInfo.releaseSpillRef(ref); + } + } + if (belowResultStackLevel > 0) { + emitterVisitor.ctx.javaClassInfo.incrementStackLevel(belowResultStackLevel); + } mv.visitVarInsn(Opcodes.ALOAD, emitterVisitor.ctx.javaClassInfo.controlFlowTempSlot); + emitterVisitor.ctx.javaClassInfo.incrementStackLevel(1); } if (emitterVisitor.ctx.contextType == RuntimeContextType.SCALAR) { diff --git a/src/main/java/org/perlonjava/codegen/EmitVariable.java b/src/main/java/org/perlonjava/codegen/EmitVariable.java index 2d1d046b6..d87b4841a 100644 --- a/src/main/java/org/perlonjava/codegen/EmitVariable.java +++ b/src/main/java/org/perlonjava/codegen/EmitVariable.java @@ -571,11 +571,24 @@ static void handleAssignOperator(EmitterVisitor emitterVisitor, BinaryOperatorNo ); } + // Spill RHS list before evaluating the LHS so LHS evaluation can safely propagate + // non-local control flow without leaving RHS values on the operand stack. + int rhsListSlot = ctx.javaClassInfo.acquireSpillSlot(); + boolean pooledRhsList = rhsListSlot >= 0; + if (!pooledRhsList) { + rhsListSlot = ctx.symbolTable.allocateLocalVariable(); + } + mv.visitVarInsn(Opcodes.ASTORE, rhsListSlot); + // For declared references, we need special handling // The my operator needs to be processed to create the variables first - node.left.accept(emitterVisitor.with(RuntimeContextType.LIST)); // emit the variable - mv.visitInsn(Opcodes.SWAP); // move the target first + node.left.accept(emitterVisitor.with(RuntimeContextType.LIST)); // emit the variable (target) + mv.visitVarInsn(Opcodes.ALOAD, rhsListSlot); // reload RHS list mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/perlonjava/runtime/RuntimeBase", "setFromList", "(Lorg/perlonjava/runtime/RuntimeList;)Lorg/perlonjava/runtime/RuntimeArray;", false); + + if (pooledRhsList) { + ctx.javaClassInfo.releaseSpillSlot(); + } EmitOperator.handleScalarContext(emitterVisitor, node); break; default: diff --git a/src/main/java/org/perlonjava/codegen/EmitterMethodCreator.java b/src/main/java/org/perlonjava/codegen/EmitterMethodCreator.java index f8f8e3bd0..aaa37b8ec 100644 --- a/src/main/java/org/perlonjava/codegen/EmitterMethodCreator.java +++ b/src/main/java/org/perlonjava/codegen/EmitterMethodCreator.java @@ -16,7 +16,9 @@ import org.perlonjava.runtime.RuntimeContextType; import java.io.PrintWriter; -import java.io.StringWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.lang.annotation.Annotation; import java.lang.reflect.*; @@ -139,6 +141,23 @@ public static byte[] getBytecode(EmitterContext ctx, Node ast, boolean useTryCat // When JPERL_ASM_DEBUG is enabled, do a diagnostic pass without COMPUTE_FRAMES so we can // disassemble + analyze the produced bytecode. frameComputeCrash.printStackTrace(); + try { + String failingClass = (ctx != null && ctx.javaClassInfo != null) + ? ctx.javaClassInfo.javaClassName + : ""; + int failingIndex = ast != null ? ast.getIndex() : -1; + String fileName = (ctx != null && ctx.errorUtil != null) ? ctx.errorUtil.getFileName() : ""; + int lineNumber = -1; + if (ctx != null && ctx.errorUtil != null && failingIndex >= 0) { + // ErrorMessageUtil caches line-number scanning state; reset it for an accurate lookup here. + ctx.errorUtil.setTokenIndex(-1); + ctx.errorUtil.setLineNumber(1); + lineNumber = ctx.errorUtil.getLineNumber(failingIndex); + } + String at = lineNumber >= 0 ? (fileName + ":" + lineNumber) : fileName; + System.err.println("ASM frame compute crash in generated class: " + failingClass + " (astIndex=" + failingIndex + ", at " + at + ")"); + } catch (Throwable ignored) { + } if (asmDebug) { try { // Reset JavaClassInfo to avoid reusing partially-resolved Labels. @@ -168,6 +187,11 @@ private static byte[] getBytecodeInternal(EmitterContext ctx, Node ast, boolean String methodName = "apply"; byte[] classData = null; boolean asmDebug = System.getenv("JPERL_ASM_DEBUG") != null; + String asmDebugClassFilter = System.getenv("JPERL_ASM_DEBUG_CLASS"); + boolean asmDebugClassMatches = asmDebugClassFilter == null + || asmDebugClassFilter.isEmpty() + || className.contains(asmDebugClassFilter) + || className.replace('/', '.').contains(asmDebugClassFilter); try { String[] env = ctx.symbolTable.getVariableNames(); @@ -594,27 +618,75 @@ private static byte[] getBytecodeInternal(EmitterContext ctx, Node ast, boolean cw.visitEnd(); classData = cw.toByteArray(); // Generate the bytecode + if (asmDebug && asmDebugClassMatches && asmDebugClassFilter != null && !asmDebugClassFilter.isEmpty()) { + try { + Path outDir = Paths.get(System.getProperty("java.io.tmpdir"), "perlonjava-asm"); + Path outFile = outDir.resolve(className + ".class"); + Files.createDirectories(outFile.getParent()); + Files.write(outFile, classData); + } catch (Throwable ignored) { + } + } + if (ctx.compilerOptions.disassembleEnabled) { // Disassemble the bytecode for debugging purposes ClassReader cr = new ClassReader(classData); - StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter(sw); + PrintWriter pw = new PrintWriter(System.out); TraceClassVisitor tcv = new TraceClassVisitor(pw); cr.accept(tcv, ClassReader.EXPAND_FRAMES); + pw.flush(); + } + + if (asmDebug && !disableFrames && asmDebugClassMatches && asmDebugClassFilter != null && !asmDebugClassFilter.isEmpty()) { + try { + ClassReader cr = new ClassReader(classData); + + PrintWriter pw = new PrintWriter(System.err); + TraceClassVisitor tcv = new TraceClassVisitor(pw); + cr.accept(tcv, ClassReader.EXPAND_FRAMES); + pw.flush(); + + PrintWriter verifyPw = new PrintWriter(System.err); + String thisClassNameDot = className.replace('/', '.'); + final byte[] verifyClassData = classData; + ClassLoader verifyLoader = new ClassLoader(GlobalVariable.globalClassLoader) { + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + if (name.equals(thisClassNameDot)) { + synchronized (getClassLoadingLock(name)) { + Class loaded = findLoadedClass(name); + if (loaded == null) { + loaded = defineClass(name, verifyClassData, 0, verifyClassData.length); + } + if (resolve) { + resolveClass(loaded); + } + return loaded; + } + } + return super.loadClass(name, resolve); + } + }; - System.out.println(sw); + try { + CheckClassAdapter.verify(cr, verifyLoader, true, verifyPw); + } catch (Throwable verifyErr) { + verifyErr.printStackTrace(verifyPw); + } + verifyPw.flush(); + } catch (Throwable t) { + t.printStackTrace(); + } } - if (asmDebug) { + if (asmDebug && disableFrames && asmDebugClassMatches) { try { ClassReader cr = new ClassReader(classData); - StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter(sw); + PrintWriter pw = new PrintWriter(System.err); TraceClassVisitor tcv = new TraceClassVisitor(pw); cr.accept(tcv, ClassReader.EXPAND_FRAMES); pw.flush(); - System.err.println(sw); PrintWriter verifyPw = new PrintWriter(System.err); String thisClassNameDot = className.replace('/', '.'); diff --git a/src/main/java/org/perlonjava/codegen/JavaClassInfo.java b/src/main/java/org/perlonjava/codegen/JavaClassInfo.java index 08cfdd0c7..39a58e80d 100644 --- a/src/main/java/org/perlonjava/codegen/JavaClassInfo.java +++ b/src/main/java/org/perlonjava/codegen/JavaClassInfo.java @@ -1,6 +1,9 @@ package org.perlonjava.codegen; import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.perlonjava.symbols.ScopedSymbolTable; import java.util.ArrayDeque; import java.util.Deque; @@ -42,6 +45,16 @@ public class JavaClassInfo { public int[] spillSlots; public int spillTop; + public static final class SpillRef { + public final int slot; + public final boolean pooled; + + public SpillRef(int slot, boolean pooled) { + this.slot = slot; + this.pooled = pooled; + } + } + /** * Manages the stack level for the class. */ @@ -81,6 +94,36 @@ public void releaseSpillSlot() { } } + public SpillRef tryAcquirePooledSpillRef() { + int slot = acquireSpillSlot(); + if (slot < 0) { + return null; + } + return new SpillRef(slot, true); + } + + public SpillRef acquireSpillRefOrAllocate(ScopedSymbolTable symbolTable) { + int slot = acquireSpillSlot(); + if (slot >= 0) { + return new SpillRef(slot, true); + } + return new SpillRef(symbolTable.allocateLocalVariable(), false); + } + + public void storeSpillRef(MethodVisitor mv, SpillRef ref) { + mv.visitVarInsn(Opcodes.ASTORE, ref.slot); + } + + public void loadSpillRef(MethodVisitor mv, SpillRef ref) { + mv.visitVarInsn(Opcodes.ALOAD, ref.slot); + } + + public void releaseSpillRef(SpillRef ref) { + if (ref.pooled) { + releaseSpillSlot(); + } + } + /** * Pushes a new set of loop labels onto the loop label stack. * diff --git a/src/main/java/org/perlonjava/codegen/StackLevelManager.java b/src/main/java/org/perlonjava/codegen/StackLevelManager.java index 1a46cc36a..e56a6aa19 100644 --- a/src/main/java/org/perlonjava/codegen/StackLevelManager.java +++ b/src/main/java/org/perlonjava/codegen/StackLevelManager.java @@ -66,9 +66,18 @@ public void reset() { * @param targetStackLevel the desired stack level to adjust to. */ public void emitPopInstructions(MethodVisitor mv, int targetStackLevel) { - int s = this.stackLevel; + int current = this.stackLevel; + if (current <= targetStackLevel) { + return; + } + + int s = current; while (s-- > targetStackLevel) { mv.visitInsn(Opcodes.POP); } + + // Keep the tracked stack level consistent with the actual JVM operand stack. + // If we emitted POPs, the stack height is now the target level. + this.stackLevel = targetStackLevel; } } diff --git a/src/main/java/org/perlonjava/operators/ModuleOperators.java b/src/main/java/org/perlonjava/operators/ModuleOperators.java index 2c94a7c12..75c81521c 100644 --- a/src/main/java/org/perlonjava/operators/ModuleOperators.java +++ b/src/main/java/org/perlonjava/operators/ModuleOperators.java @@ -729,6 +729,14 @@ public static RuntimeScalar require(RuntimeScalar runtimeScalar) { RuntimeBase baseResult = doFile(runtimeScalar, true, true, RuntimeContextType.SCALAR); RuntimeScalar result = baseResult.scalar(); + String requireDebug = System.getenv("JPERL_REQUIRE_DEBUG"); + if (requireDebug != null && !requireDebug.isEmpty()) { + // Keep this terse to avoid overwhelming logs. + boolean defined = result.defined().getBoolean(); + boolean boolValue = defined && result.getBoolean(); + System.err.println("require(" + fileName + ") => defined=" + defined + ", bool=" + boolValue + ", type=" + result.type + ", str=" + result); + } + // Check if `do` returned undef (file not found or I/O error) if (!result.defined().getBoolean()) { String err = getGlobalVariable("main::@").toString(); diff --git a/src/main/java/org/perlonjava/parser/DataSection.java b/src/main/java/org/perlonjava/parser/DataSection.java index 73acbb91d..f5ef99700 100644 --- a/src/main/java/org/perlonjava/parser/DataSection.java +++ b/src/main/java/org/perlonjava/parser/DataSection.java @@ -102,8 +102,13 @@ static int parseDataSection(Parser parser, int tokenIndex, List toke return tokens.size(); } - if (token.text.equals("__DATA__") || (token.text.equals("__END__") && parser.isTopLevelScript)) { + if (token.text.equals("__DATA__") || token.text.equals("__END__")) { processedPackages.add(handleName); + + // __END__ should always stop parsing, but only top-level scripts (and __DATA__) should + // populate the DATA handle content. + boolean populateData = token.text.equals("__DATA__") || parser.isTopLevelScript; + tokenIndex++; // Skip any whitespace immediately after __DATA__ @@ -116,21 +121,23 @@ static int parseDataSection(Parser parser, int tokenIndex, List toke tokenIndex++; } - // Capture all remaining content until end marker - StringBuilder dataContent = new StringBuilder(); - while (tokenIndex < tokens.size()) { - LexerToken currentToken = tokens.get(tokenIndex); + if (populateData) { + // Capture all remaining content until end marker + StringBuilder dataContent = new StringBuilder(); + while (tokenIndex < tokens.size()) { + LexerToken currentToken = tokens.get(tokenIndex); + + // Stop if we hit an end marker + if (isEndMarker(currentToken)) { + break; + } - // Stop if we hit an end marker - if (isEndMarker(currentToken)) { - break; + dataContent.append(currentToken.text); + tokenIndex++; } - dataContent.append(currentToken.text); - tokenIndex++; + createDataHandle(parser, dataContent.toString()); } - - createDataHandle(parser, dataContent.toString()); } // Return tokens.size() to indicate we've consumed everything return tokens.size(); diff --git a/src/main/java/org/perlonjava/parser/StatementParser.java b/src/main/java/org/perlonjava/parser/StatementParser.java index 18fcb22e7..fb571ff1b 100644 --- a/src/main/java/org/perlonjava/parser/StatementParser.java +++ b/src/main/java/org/perlonjava/parser/StatementParser.java @@ -95,7 +95,36 @@ public static Node parseForStatement(Parser parser, String label) { // Parse optional loop variable Node varNode = null; LexerToken token = TokenUtils.peek(parser); // "my" "$" "(" "CORE::my" - if (token.text.equals("my") || token.text.equals("our") || token.text.equals("CORE") || token.text.equals("$")) { + if (token.type == LexerTokenType.IDENTIFIER && + (token.text.equals("my") || token.text.equals("our") || token.text.equals("state"))) { + // Ensure `for my $x (...)` is parsed as a variable declaration, not as `$x`. + // This is critical for strict-vars correctness inside the loop body. + int declIndex = parser.tokenIndex; + parser.parsingForLoopVariable = true; + TokenUtils.consume(parser, LexerTokenType.IDENTIFIER); + varNode = OperatorParser.parseVariableDeclaration(parser, token.text, declIndex); + parser.parsingForLoopVariable = false; + } else if (token.type == LexerTokenType.IDENTIFIER && token.text.equals("CORE") + && parser.tokens.get(parser.tokenIndex).text.equals("CORE") + && parser.tokens.size() > parser.tokenIndex + 1 + && parser.tokens.get(parser.tokenIndex + 1).text.equals("::")) { + // Handle CORE::my/our/state + TokenUtils.consume(parser, LexerTokenType.IDENTIFIER); // CORE + TokenUtils.consume(parser, LexerTokenType.OPERATOR, "::"); + LexerToken coreOp = TokenUtils.peek(parser); + if (coreOp.type == LexerTokenType.IDENTIFIER && + (coreOp.text.equals("my") || coreOp.text.equals("our") || coreOp.text.equals("state"))) { + int declIndex = parser.tokenIndex; + parser.parsingForLoopVariable = true; + TokenUtils.consume(parser, LexerTokenType.IDENTIFIER); + varNode = OperatorParser.parseVariableDeclaration(parser, coreOp.text, declIndex); + parser.parsingForLoopVariable = false; + } else { + parser.parsingForLoopVariable = true; + varNode = ParsePrimary.parsePrimary(parser); + parser.parsingForLoopVariable = false; + } + } else if (token.text.equals("$")) { parser.parsingForLoopVariable = true; varNode = ParsePrimary.parsePrimary(parser); parser.parsingForLoopVariable = false; diff --git a/src/main/java/org/perlonjava/scriptengine/PerlLanguageProvider.java b/src/main/java/org/perlonjava/scriptengine/PerlLanguageProvider.java index 2f769adc4..d84b3194f 100644 --- a/src/main/java/org/perlonjava/scriptengine/PerlLanguageProvider.java +++ b/src/main/java/org/perlonjava/scriptengine/PerlLanguageProvider.java @@ -171,7 +171,11 @@ public static RuntimeList executePerlCode(CompilerOptions compilerOptions, ctx.logDebug("createClassWithMethod"); // Create a new instance of ErrorMessageUtil, resetting the line counter ctx.errorUtil = new ErrorMessageUtil(ctx.compilerOptions.fileName, tokens); - ctx.symbolTable = globalSymbolTable.snapShot(); // reset the symbol table + // Snapshot the symbol table after parsing. + // The parser records lexical declarations (e.g., `for my $p (...)`) and pragma state + // (strict/warnings/features) into ctx.symbolTable. Resetting to a fresh global snapshot + // loses those declarations and causes strict-vars failures during codegen. + ctx.symbolTable = ctx.symbolTable.snapShot(); Class generatedClass = EmitterMethodCreator.createClassWithMethod( ctx, ast, @@ -221,7 +225,8 @@ public static RuntimeList executePerlAST(Node ast, // Create the Java class from the AST ctx.logDebug("createClassWithMethod"); ctx.errorUtil = new ErrorMessageUtil(ctx.compilerOptions.fileName, tokens); - ctx.symbolTable = globalSymbolTable.snapShot(); + // Snapshot the symbol table as seen by the parser (includes lexical decls + pragma state). + ctx.symbolTable = ctx.symbolTable.snapShot(); Class generatedClass = EmitterMethodCreator.createClassWithMethod( ctx, ast, diff --git a/src/test/java/org/perlonjava/PerlScriptExecutionTest.java b/src/test/java/org/perlonjava/PerlScriptExecutionTest.java index cc2cbe4c9..234438576 100644 --- a/src/test/java/org/perlonjava/PerlScriptExecutionTest.java +++ b/src/test/java/org/perlonjava/PerlScriptExecutionTest.java @@ -107,6 +107,19 @@ private static Stream getPerlScripts(boolean unitOnly) throws IOExceptio .sorted() // Ensure deterministic order .collect(Collectors.toList()); + String testFilter = System.getenv("JPERL_TEST_FILTER"); + if (testFilter != null && !testFilter.isEmpty()) { + sortedScripts = sortedScripts.stream() + .filter(s -> s.contains(testFilter)) + .collect(Collectors.toList()); + + if (sortedScripts.isEmpty()) { + throw new IOException("No tests matched JPERL_TEST_FILTER='" + testFilter + "'"); + } + + return sortedScripts.stream(); + } + // Sharding logic String shardIndexProp = System.getProperty("test.shard.index"); String shardTotalProp = System.getProperty("test.shard.total"); From a1f66e585dfca9e4094b823900591166efd34b92 Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Wed, 7 Jan 2026 22:25:43 +0100 Subject: [PATCH 04/51] Fix verifier stack mismatch for push/unshift and improve ASM debug --- .../org/perlonjava/codegen/EmitOperator.java | 29 ++- .../codegen/EmitterMethodCreator.java | 214 +++++++++++++++++- 2 files changed, 239 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/perlonjava/codegen/EmitOperator.java b/src/main/java/org/perlonjava/codegen/EmitOperator.java index d13bde41b..254d330cb 100644 --- a/src/main/java/org/perlonjava/codegen/EmitOperator.java +++ b/src/main/java/org/perlonjava/codegen/EmitOperator.java @@ -326,9 +326,32 @@ static void handleSpliceBuiltin(EmitterVisitor emitterVisitor, OperatorNode node // Handles the 'push' operator, which adds elements to an array. static void handlePushOperator(EmitterVisitor emitterVisitor, BinaryOperatorNode node) { - // Accept both left and right operands in LIST context. - node.left.accept(emitterVisitor.with(RuntimeContextType.LIST)); - node.right.accept(emitterVisitor.with(RuntimeContextType.LIST)); + // Spill the left operand before evaluating the right side so non-local control flow + // propagation can't jump to returnLabel with an extra value on the JVM operand stack. + if (ENABLE_SPILL_BINARY_LHS) { + MethodVisitor mv = emitterVisitor.ctx.mv; + node.left.accept(emitterVisitor.with(RuntimeContextType.LIST)); + + int leftSlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot(); + boolean pooled = leftSlot >= 0; + if (!pooled) { + leftSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); + } + mv.visitVarInsn(Opcodes.ASTORE, leftSlot); + + node.right.accept(emitterVisitor.with(RuntimeContextType.LIST)); + + mv.visitVarInsn(Opcodes.ALOAD, leftSlot); + mv.visitInsn(Opcodes.SWAP); + + if (pooled) { + emitterVisitor.ctx.javaClassInfo.releaseSpillSlot(); + } + } else { + // Accept both left and right operands in LIST context. + node.left.accept(emitterVisitor.with(RuntimeContextType.LIST)); + node.right.accept(emitterVisitor.with(RuntimeContextType.LIST)); + } emitOperator(node, emitterVisitor); } diff --git a/src/main/java/org/perlonjava/codegen/EmitterMethodCreator.java b/src/main/java/org/perlonjava/codegen/EmitterMethodCreator.java index aaa37b8ec..aef3b8f87 100644 --- a/src/main/java/org/perlonjava/codegen/EmitterMethodCreator.java +++ b/src/main/java/org/perlonjava/codegen/EmitterMethodCreator.java @@ -1,6 +1,7 @@ package org.perlonjava.codegen; import org.objectweb.asm.*; +import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.MethodNode; import org.objectweb.asm.tree.analysis.Analyzer; @@ -8,6 +9,7 @@ import org.objectweb.asm.tree.analysis.BasicValue; import org.objectweb.asm.tree.analysis.BasicInterpreter; import org.objectweb.asm.util.CheckClassAdapter; +import org.objectweb.asm.util.Printer; import org.objectweb.asm.util.TraceClassVisitor; import org.perlonjava.astnode.Node; import org.perlonjava.astvisitor.EmitterVisitor; @@ -47,6 +49,46 @@ public static String generateClassName() { return "org/perlonjava/anon" + classCounter++; } + private static String insnToString(AbstractInsnNode n) { + if (n == null) { + return ""; + } + int op = n.getOpcode(); + String opName = (op >= 0 && op < Printer.OPCODES.length) ? Printer.OPCODES[op] : ""; + + if (n instanceof org.objectweb.asm.tree.VarInsnNode vn) { + return opName + " " + vn.var; + } + if (n instanceof org.objectweb.asm.tree.MethodInsnNode mn) { + return opName + " " + mn.owner + "." + mn.name + mn.desc; + } + if (n instanceof org.objectweb.asm.tree.FieldInsnNode fn) { + return opName + " " + fn.owner + "." + fn.name + " : " + fn.desc; + } + if (n instanceof org.objectweb.asm.tree.TypeInsnNode tn) { + return opName + " " + tn.desc; + } + if (n instanceof org.objectweb.asm.tree.LdcInsnNode ln) { + return opName + " " + String.valueOf(ln.cst); + } + if (n instanceof org.objectweb.asm.tree.IntInsnNode in) { + return opName + " " + in.operand; + } + if (n instanceof org.objectweb.asm.tree.IincInsnNode ii) { + return opName + " " + ii.var + " " + ii.incr; + } + if (n instanceof org.objectweb.asm.tree.LineNumberNode ln) { + return "LINE " + ln.line; + } + if (n instanceof org.objectweb.asm.tree.LabelNode) { + return "LABEL"; + } + if (n instanceof org.objectweb.asm.tree.JumpInsnNode) { + return opName + "