.cognition/skills/debug-perlonjava/SKILL.md
Debug and fix test failures and regressions in PerlOnJava
npx skillsauth add fglock/perlonjava debug-perlonjavaInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
You are debugging failures in PerlOnJava, a Perl-to-JVM compiler with a bytecode interpreter fallback. This skill covers debugging workflows for test failures, regressions, and parity issues between backends.
git stash ⚠️⚠️⚠️DANGER: Changes are SILENTLY LOST when using git stash/stash pop!
git stash to temporarily revert changesgit diff > backup.patchIMPORTANT: Never push directly to master. Always use feature branches and PRs.
git checkout -b fix/descriptive-name
# ... make changes ...
git push origin fix/descriptive-name
gh pr create --title "Fix: description" --body "Details"
src/main/java/org/perlonjava/ (compiler, bytecode interpreter, runtime)src/test/resources/unit/*.t (run via make)perl5_t/t/ (Perl 5 compatibility suite)target/perlonjava-3.0.0.jar./jperlALWAYS use make commands. NEVER use raw mvn/gradlew commands.
| Command | What it does |
|---------|--------------|
| make | Build + run all unit tests (use before committing) |
| make dev | Build only, skip tests (for quick iteration during debugging) |
make # Standard build - compiles and runs tests
make dev # Quick build - compiles only, NO tests
cd perl5_t/t
../../jperl op/bop.t
# For re/pat.t and similar regex tests
JPERL_UNIMPLEMENTED=1 JPERL_OPTS=-Xss256m PERL_SKIP_BIG_MEM_TESTS=1 ./jperl perl5_t/t/re/pat.t
# For op/sprintf2.t
JPERL_UNIMPLEMENTED=1 ./jperl perl5_t/t/op/sprintf2.t
perl dev/tools/perl_test_runner.pl perl5_t/t/op
perl dev/tools/perl_test_runner.pl --jobs 8 --timeout 60 perl5_t/t
The test runner (dev/tools/perl_test_runner.pl) automatically sets environment variables for specific tests:
# JPERL_UNIMPLEMENTED="warn" for these tests:
re/pat_rt_report.t | re/pat.t | re/regex_sets.t | re/regexp_unicode_prop.t
op/pack.t | op/index.t | op/split.t | re/reg_pmod.t | op/sprintf.t | base/lex.t
# JPERL_OPTS="-Xss256m" for these tests:
re/pat.t | op/repeat.t | op/list.t
# PERL_SKIP_BIG_MEM_TESTS=1 for ALL tests
To reproduce what the test runner does for a specific test:
# For re/pat.t (needs all three):
cd perl5_t/t && JPERL_UNIMPLEMENTED=warn JPERL_OPTS=-Xss256m PERL_SKIP_BIG_MEM_TESTS=1 ../../jperl re/pat.t
# For re/subst.t (only PERL_SKIP_BIG_MEM_TESTS):
cd perl5_t/t && PERL_SKIP_BIG_MEM_TESTS=1 ../../jperl re/subst.t
# For op/bop.t (only PERL_SKIP_BIG_MEM_TESTS):
cd perl5_t/t && PERL_SKIP_BIG_MEM_TESTS=1 ../../jperl op/bop.t
./jperl --interpreter script.pl
./jperl --interpreter -e 'print "hello\n"'
JPERL_INTERPRETER=1 ./jperl script.pl # Global (affects require/do/eval)
# System Perl
perl -e 'my @a = (1,2,3); print "@a\n"'
# PerlOnJava
./jperl -e 'my @a = (1,2,3); print "@a\n"'
./jperl -e 'code' # JVM backend
JPERL_INTERPRETER=1 ./jperl -e 'code' # Interpreter backend
| Variable | Effect |
|----------|--------|
| JPERL_INTERPRETER=1 | Force interpreter mode globally (require/do/eval) |
| JPERL_DISABLE_INTERPRETER_FALLBACK=1 | Disable bytecode interpreter fallback for large subs |
| JPERL_SHOW_FALLBACK=1 | Print message when a sub falls back to interpreter |
| JPERL_EVAL_NO_INTERPRETER=1 | Disable interpreter for eval STRING |
| JPERL_OPTS="-Xss256m" | Pass JVM options (e.g., stack size) |
| Variable | Effect |
|----------|--------|
| JPERL_DISASSEMBLE=1 | Disassemble generated bytecode |
| JPERL_ASM_DEBUG=1 | Print JVM bytecode when ASM frame computation crashes |
| JPERL_EVAL_VERBOSE=1 | Verbose error reporting for eval compilation |
| JPERL_EVAL_TRACE=1 | Trace eval STRING execution path |
| JPERL_IO_DEBUG=1 | Trace file handle open/dup/write operations |
| JPERL_REQUIRE_DEBUG=1 | Trace require/use module loading |
| Variable | Effect |
|----------|--------|
| JPERL_UNIMPLEMENTED=1 | Allow unimplemented features (skip instead of die) |
| PERL_SKIP_BIG_MEM_TESTS=1 | Skip memory-intensive tests |
# Compare branch vs master
git checkout master && make dev
./jperl -e 'failing code'
git checkout branch && make dev
./jperl -e 'failing code'
Reduce the failing test to the smallest code that demonstrates the bug:
./jperl -e 'my $x = 58; eval q{($x) .= "z"}; print "x=$x\n"'
perl -e 'same code'
When parsing issues are suspected, compare the parse tree:
./jperl --parse -e 'code' # Show PerlOnJava AST
perl -MO=Deparse -e 'code' # Compare with Perl's interpretation
This helps identify operator precedence issues and incorrect parsing.
./jperl --disassemble -e 'minimal code' # JVM bytecode
./jperl --disassemble --interpreter -e 'minimal code' # Interpreter bytecode
# Record profile
$JAVA_HOME/bin/java -XX:StartFlightRecording=duration=10s,filename=profile.jfr \
-jar target/perlonjava-3.0.0.jar script.pl
# Analyze hotspots
$JAVA_HOME/bin/jfr print --events jdk.ExecutionSample profile.jfr 2>&1 | \
grep -E "^\s+[a-z].*line:" | sed 's/line:.*//' | sort | uniq -c | sort -rn | head -20
In Java source, add:
System.err.println("DEBUG: var=" + var);
Then rebuild with make dev.
# After fixing
make dev
./jperl -e 'test code' # Verify fix
make # Build + run unit tests (no regressions)
IMPORTANT: Always work in a feature branch and create a PR for review.
git checkout -b fix-descriptive-name
git add -A && git commit -m "Fix <what> by <how>
<Details of the bug and fix>
Generated with [Devin](https://cli.devin.ai/docs)
Co-Authored-By: Devin <[email protected]>"
git push -u origin fix-descriptive-name
# Create PR using gh CLI
gh pr create --title "Fix: description" --body "## Summary
- Fixed X by Y
## Test Plan
- [ ] Unit tests pass
- [ ] Reproducer now works correctly
Generated with [Devin](https://cli.devin.ai/docs)"
git checkout master
git pull
git branch -d fix-descriptive-name
Source → Lexer → Parser → AST ─┬─→ JVM Compiler → JVM bytecode (default)
└─→ BytecodeCompiler → InterpretedCode → BytecodeInterpreter
Both backends share the parser (same AST) and runtime (same operators, same RuntimeScalar/Array/Hash).
| Area | File | Notes |
|------|------|-------|
| Bytecode Compiler | backend/bytecode/BytecodeCompiler.java | AST → interpreter bytecode |
| Bytecode Interpreter | backend/bytecode/BytecodeInterpreter.java | Main dispatch loop |
| Assignment (interp) | backend/bytecode/CompileAssignment.java | Assignment compilation |
| Binary ops (interp) | backend/bytecode/CompileBinaryOperator.java | Binary operator compilation |
| Unary ops (interp) | backend/bytecode/CompileOperator.java | Unary operator compilation |
| Opcodes | backend/bytecode/Opcodes.java | Opcode constants |
| eval STRING | backend/bytecode/EvalStringHandler.java | eval STRING compilation |
| JVM Compiler | backend/jvm/EmitterMethodCreator.java | AST → JVM bytecode |
| JVM Subroutine | backend/jvm/EmitSubroutine.java | Sub compilation (JVM) |
| JVM Binary ops | backend/jvm/EmitBinaryOperator.java | Binary ops (JVM) |
| Compilation router | app/scriptengine/PerlLanguageProvider.java | Picks backend |
| Runtime scalar | runtime/runtimetypes/RuntimeScalar.java | Scalar values |
| Runtime array | runtime/runtimetypes/RuntimeArray.java | Array values |
| Runtime hash | runtime/runtimetypes/RuntimeHash.java | Hash values |
| Math operators | runtime/operators/MathOperators.java | +, -, *, /, etc. |
| String operators | runtime/operators/StringOperators.java | ., x, etc. |
| Bitwise operators | runtime/operators/BitwiseOperators.java | &, |, ^, etc. |
| Regex runtime | runtime/regex/RuntimeRegex.java | Regex matching |
| Regex preprocessor | runtime/regex/RegexPreprocessor.java | Perl→Java regex |
All paths relative to src/main/java/org/perlonjava/.
When fixing interpreter bugs, ALWAYS investigate how the JVM backend handles the same operation before implementing a fix.
The interpreter and JVM backends share the same runtime classes (RuntimeScalar, RuntimeArray, RuntimeHash, RuntimeList, PerlRange, etc.). The JVM backend is the reference implementation - if the interpreter handles something differently, it's likely wrong.
Disassemble the JVM bytecode to see what runtime methods it calls:
./jperl --disassemble -e 'code that works'
Look for the runtime method calls in the disassembly (INVOKEVIRTUAL, INVOKESTATIC):
INVOKEVIRTUAL org/perlonjava/runtime/runtimetypes/RuntimeList.addToArray
INVOKEVIRTUAL org/perlonjava/runtime/runtimetypes/RuntimeBase.setFromList
Read those runtime methods to understand the correct behavior:
setFromList() handle different input types?addToArray, getList, etc.)?Use the same runtime methods in the interpreter instead of reimplementing the logic with special cases.
Wrong approach (special-casing types in interpreter):
if (valuesBase instanceof RuntimeList) { ... }
else if (valuesBase instanceof RuntimeArray) { ... }
else if (valuesBase instanceof PerlRange) { ... } // BAD: special case
else { ... }
Correct approach (use same runtime methods as JVM):
// JVM calls addToArray() which handles all types uniformly
RuntimeArray valuesArray = new RuntimeArray();
valuesBase.addToArray(valuesArray); // Works for RuntimeList, RuntimeArray, PerlRange, etc.
The JVM's setFromList() → addToArray() chain already handles PerlRange correctly via PerlRange.addToArray() → toList().addToArray(). The interpreter should use the same mechanism.
Symptom: Operation returns wrong type (list vs scalar).
Pattern: Code uses node.accept(this) instead of compileNode(node, -1, RuntimeContextType.SCALAR).
Fix: Use compileNode() helper with explicit context.
Symptom: "Unknown opcode" or silent wrong result.
Fix: Add opcode to Opcodes.java, handler to BytecodeInterpreter.java, emitter to BytecodeCompiler.java, disassembly to InterpretedCode.java.
Symptom: Variable returns undef inside eval/sub.
Pattern: Variable not registered in symbol table.
Fix: Ensure detectClosureVariables() registers captured variables via addVariableWithIndex().
Symptom: Side effects happen twice (e.g., shift removes two elements).
Pattern: RHS compiled once at top of function, then again in specific handler.
Fix: Remove redundant compilation, use valueReg from first compilation.
Symptom: Assignment doesn't modify original variable. Pattern: Expression returns copy instead of lvalue reference. Fix: Ensure lvalue context is preserved through compilation chain.
Symptom: Numeric value instead of expected string/reference.
Pattern: Incorrect scalar context conversion.
Fix: Remove spurious LIST_TO_COUNT or use proper scalar coercion.
Symptom: Block/eval returns unexpected value (e.g., 1 instead of undef).
Pattern: Last statement is for loop or similar that sets lastResultReg = -1.
Fix: In visit(BlockNode), initialize outerResultReg to undef when lastResultReg < 0.
Symptom: for loop only iterates last element when inside eval in scalar context.
Pattern: Loop list compiled with node.list.accept(this) instead of explicit LIST context.
Fix: Use compileNode(node.list, -1, RuntimeContextType.LIST) for loop lists.
Symptom: Operations inside eval behave differently based on how eval result is used.
Pattern: currentCallContext from eval propagates incorrectly to inner constructs.
Fix: Isolate context - loops/blocks should use their own context, not inherit from eval.
| Directory | Tests | Notes |
|-----------|-------|-------|
| perl5_t/t/op/ | Core operators | bop.t, sprintf.t, etc. |
| perl5_t/t/re/ | Regex | pat.t needs special env vars |
| perl5_t/t/io/ | I/O operations | filetest.t, etc. |
| perl5_t/t/uni/ | Unicode | |
| perl5_t/t/mro/ | Method resolution | |
# Build + test
make
# Build only (no tests)
make dev
# Run specific Perl5 test
perl dev/tools/perl_test_runner.pl perl5_t/t/op/bop.t
# Debug parsing
./jperl --parse -e 'code'
perl -MO=Deparse -e 'code'
# Debug bytecode
./jperl --disassemble -e 'code'
./jperl --disassemble --interpreter -e 'code'
# Compare output
diff <(./jperl -e 'code') <(perl -e 'code')
# Git workflow (always use branches!)
git checkout -b fix-name
# ... make changes ...
git add -A && git commit -m "Fix message"
git push -u origin fix-name
gh pr create --title "Fix: title" --body "Description"
development
# PerlOnJava Debugging Skills and Architecture Knowledge This document captures key knowledge about PerlOnJava internals learned during debugging sessions. ## Variable Storage and Scoping ### Three Types of Variable Declarations 1. **`my` variables** - Lexical scope - Stored in JVM local variable slots during normal execution - When captured by closures: stored as closure fields or in GlobalVariable with IDs - Symbol table entry: `decl = "my"`, has `index` (JVM slot number) 2. **`o
development
# PerlOnJava Interpreter Developer Guide - name all test files /tmp/test.pl ## Quick Reference **Performance:** 46.84M ops/sec (1.75x slower than compiler ✓) **Opcodes:** 0-157 (contiguous) for JVM tableswitch optimization **Runtime:** 100% API compatibility with compiler (zero duplication) ### Testing Modes **JPERL_EVAL_USE_INTERPRETER=1** - Forces all eval STRING to use the interpreter - Used for testing interpreter implementation of operators in eval context - Compiler still used for mai
development
# Profile PerlOnJava ## ⚠️⚠️⚠️ CRITICAL: NEVER USE `git stash` ⚠️⚠️⚠️ **DANGER: Changes are SILENTLY LOST when using git stash/stash pop!** - NEVER use `git stash` to temporarily revert changes - INSTEAD: Commit to a WIP branch or use `git diff > backup.patch` - This warning exists because completed work was lost during debugging Profile and optimize PerlOnJava runtime performance using Java Flight Recorder. ## Git Workflow **IMPORTANT: Never push directly to master. Always use feature bra
development
# Port CPAN Module to PerlOnJava ## ⚠️⚠️⚠️ CRITICAL: NEVER USE `git stash` ⚠️⚠️⚠️ **DANGER: Changes are SILENTLY LOST when using git stash/stash pop!** - NEVER use `git stash` to temporarily revert changes - INSTEAD: Commit to a WIP branch or use `git diff > backup.patch` - This warning exists because completed work was lost during debugging This skill guides you through porting a CPAN module with XS/C components to PerlOnJava using Java implementations. ## When to Use This Skill - User as