dev/custom_bytecode/SKILL.md
# 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
npx skillsauth add fglock/perlonjava dev/custom_bytecodeInstall 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.
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)
JPERL_EVAL_USE_INTERPRETER=1 - Forces all eval STRING to use the interpreter
JPERL_EVAL_USE_INTERPRETER=1 ./jperl test.plJPERL_EVAL_VERBOSE=1 - Enable verbose eval error reporting
JPERL_EVAL_USE_INTERPRETER=1 JPERL_EVAL_VERBOSE=1 ./jperl test.pl--interpreter - Forces the interpreter EVERYWHERE
./jperl --interpreter test.plOpcodes.java - Opcode constants (0-157, contiguous)BytecodeInterpreter.java - Execution loop with range-based delegationBytecodeCompiler.java - AST to bytecode with register allocationInterpretedCode.java - Bytecode container with disassemblerSlowOpcodeHandler.java - Handlers for rare operations (151-154)Location: dev/tools/generate_opcode_handlers.pl
Automates creation of opcode handlers for built-in functions with simple signatures.
# Generate handlers for all eligible operators in OperatorHandler.java
perl dev/tools/generate_opcode_handlers.pl
# Rebuilds:
# - ScalarUnaryOpcodeHandler.java (31 ops: chr, ord, abs, sin, cos, etc.)
# - ScalarBinaryOpcodeHandler.java (12 ops: atan2, eq, ne, lt, le, gt, ge, cmp, etc.)
# - Opcodes.java (adds new opcode constants)
# - BytecodeInterpreter.java (adds dispatch cases)
# - InterpretedCode.java (adds disassembly cases)
Automatically:
Still Manual:
// GENERATED_OPERATORS_START/END)Included:
(RuntimeScalar) → RuntimeScalar(RuntimeScalar, RuntimeScalar) → RuntimeScalar(RuntimeScalar, RuntimeScalar, RuntimeScalar) → RuntimeScalarExcluded:
(int, RuntimeBase...) - getcTool prints list of operators needing emit cases. Add between markers:
// GENERATED_OPERATORS_START
} else if (op.equals("chr")) {
// chr($x) - convert codepoint to character
if (node.operand instanceof ListNode) {
ListNode list = (ListNode) node.operand;
if (!list.elements.isEmpty()) {
list.elements.get(0).accept(this);
} else {
throwCompilerException("chr requires an argument");
}
} else {
node.operand.accept(this);
}
int argReg = lastResultReg;
int rd = allocateRegister();
emit(Opcodes.CHR);
emitReg(rd);
emitReg(argReg);
lastResultReg = rd;
// GENERATED_OPERATORS_END
3. Import Path Conversion
org/perlonjava/operators/... → org.perlonjava.operators....4. BytecodeCompiler Not Automated
5. Signature Mismatches
# Build
make
# Test in interpreter mode (forces eval STRING to use interpreter)
JPERL_EVAL_USE_INTERPRETER=1 ./jperl /tmp/test.pl
# Test script example:
cat > /tmp/test.pl << 'EOF'
print "chr(65): ", eval("chr(65)"), "\n";
print "ord('A'): ", eval("ord('A')"), "\n";
print "abs(-42): ", eval("abs(-42)"), "\n";
EOF
# Expected output (after adding BytecodeCompiler cases):
# chr(65): A
# ord('A'): 65
# abs(-42): 42
# After adding new operators to OperatorHandler.java
perl dev/tools/generate_opcode_handlers.pl
# After updating LASTOP
perl dev/tools/generate_opcode_handlers.pl
# Tool output shows:
# - Existing opcodes skipped
# - New opcodes generated
# - Next available opcode number
# - List of operators needing BytecodeCompiler cases
Use Fast Opcode when:
Use slow opcode when:
Example: Unary + operator (forces numeric/scalar context)
// Find next available opcode number (currently 169+)
/** Unary +: Forces numeric/scalar context on operand */
public static final short UNARY_PLUS = 169;
Critical: Keep opcodes contiguous! No gaps allowed.
case Opcodes.UNARY_PLUS: {
int rd = bytecode[pc++];
int rs = bytecode[pc++];
// Force scalar context
RuntimeBase operand = registers[rs];
registers[rd] = operand.scalar();
break;
}
} else if (op.equals("+")) {
// Unary + operator
int savedContext = currentCallContext;
currentCallContext = RuntimeContextType.SCALAR;
try {
node.operand.accept(this);
int operandReg = lastResultReg;
int rd = allocateRegister();
emit(Opcodes.ARRAY_SIZE); // Converts array to size, passes through scalars
emitReg(rd);
emitReg(operandReg);
lastResultReg = rd;
} finally {
currentCallContext = savedContext;
}
}
case Opcodes.UNARY_PLUS:
rd = bytecode[pc++];
rs = bytecode[pc++];
sb.append("UNARY_PLUS r").append(rd).append(" = +r").append(rs).append("\n");
break;
WARNING: Missing disassembly cases cause PC misalignment! When disassembler hits unknown opcode, it doesn't advance PC for operands, corrupting all subsequent instructions.
Example: STORE_GLOBAL_ARRAY (13), STORE_GLOBAL_HASH (15)
These opcodes already existed but lacked interpreter/disassembly support.
case Opcodes.STORE_GLOBAL_ARRAY: {
int nameIdx = bytecode[pc++];
int srcReg = bytecode[pc++];
String name = code.stringPool[nameIdx];
RuntimeArray globalArray = GlobalVariable.getGlobalArray(name);
RuntimeBase value = registers[srcReg];
// Clear and populate
if (value instanceof RuntimeArray) {
globalArray.elements.clear();
globalArray.elements.addAll(((RuntimeArray) value).elements);
} else if (value instanceof RuntimeList) {
globalArray.setFromList((RuntimeList) value);
} else {
globalArray.setFromList(value.getList());
}
break;
}
Key Insight: Match compiler semantics exactly. Check compiler's EmitterVisitor or runtime methods to understand expected behavior.
case Opcodes.STORE_GLOBAL_ARRAY:
nameIdx = bytecode[pc++];
int srcReg = bytecode[pc++];
sb.append("STORE_GLOBAL_ARRAY @").append(stringPool[nameIdx])
.append(" = r").append(srcReg).append("\n");
break;
Perl Feature: f() = "X" where f returns mutable reference
Parse Structure:
BinaryOperatorNode: =
BinaryOperatorNode: ( # Function call
OperatorNode: &
IdentifierNode: 'f'
ListNode: [] # Arguments
StringNode: "X"
Implementation in BytecodeCompiler.java:
// In compileAssignmentOperator(), before error throw:
if (leftBin.operator.equals("(")) {
// Call function (returns RuntimeBaseProxy in lvalue context)
node.left.accept(this);
int lvalueReg = lastResultReg;
// Compile RHS
node.right.accept(this);
int rhsReg = lastResultReg;
// Assign using SET_SCALAR
emit(Opcodes.SET_SCALAR);
emitReg(lvalueReg);
emitReg(rhsReg);
lastResultReg = rhsReg;
currentCallContext = savedContext;
return;
}
How It Works:
lvalue field pointing to actual mutable location.set() on the proxy, which delegates to the lvaluesubstr($x,0,1) returns proxy to first character of $x# Build
make
# Test manually
./jperl -E 'my @x = (1,2,3); say +@x' # Should print 3
# Test disassembly (verifies PC advancement)
./jperl --disassemble -E 'my @x; say +@x' 2>&1 | grep UNARY
# Run unit tests
make test-unit
# Run specific test in interpreter mode
cd perl5_t/t && JPERL_EVAL_USE_INTERPRETER=1 ../../jperl op/bop.t
# Compare compiler vs interpreter results
./jperl op/bop.t # Compiler mode
JPERL_EVAL_USE_INTERPRETER=1 ./jperl op/bop.t # Interpreter mode
# Verify tableswitch preserved
javap -c -classpath build/classes/java/main \
org.perlonjava.backend.bytecode.BytecodeInterpreter | grep -A 5 "switch"
Must see tableswitch, not lookupswitch!
Example output showing tableswitch:
148: tableswitch { // 0 to 168
0: 840
1: 843
2: 893
3: 909
4: 976
If you see lookupswitch instead, you've introduced gaps in opcode numbering!
1. Disassembly is NOT Optional
2. Match Compiler Semantics Exactly
local $x must call makeLocal(), not just assign3. Never Hide Problems
4. Opcode Contiguity is Performance-Critical
5. Error Messages Must Include Context
throwCompilerException(message, tokenIndex)1. Forgetting PC Increment:
// WRONG: Infinite loop!
int rd = bytecode[pc] & 0xFF;
// RIGHT:
int rd = bytecode[pc++];
2. Opcode Gaps:
// WRONG: Breaks tableswitch!
public static final short OP_A = 82;
public static final short OP_B = 90; // Gap!
// RIGHT:
public static final short OP_A = 82;
public static final short OP_B = 83; // Sequential
3. Missing Disassembly:
// WRONG: Causes PC misalignment!
default:
sb.append("UNKNOWN\n"); // Doesn't read operands!
break;
// RIGHT: Every opcode must read its operands
case Opcodes.MY_OP:
int rd = bytecode[pc++];
int rs = bytecode[pc++];
sb.append("MY_OP r").append(rd).append(", r").append(rs).append("\n");
break;
Critical: JVM refuses to JIT-compile methods >~8000 bytes, causing 5-10x slowdown.
Solution: Delegate cold opcodes to secondary methods:
executeComparisons() - Comparison ops (31-41)executeArithmetic() - Multiply, divide, compound (19-30, 110-113)executeCollections() - Array/hash ops (43-56, 93-96)executeTypeOps() - Type/reference ops (62-70, 102-105)Monitor: Run dev/tools/check-bytecode-size.sh after changes.
Trade-off: Slower execution for faster startup and lower memory.
Interpreter and compiler call identical runtime methods:
Example:
// Interpreter: Direct call
registers[rd] = MathOperators.add(registers[rs1], registers[rs2]);
// Compiler: Generated bytecode calls same method
INVOKESTATIC org/perlonjava/operators/MathOperators.add(...)
Captured Variables:
PerlOnJava::_BEGIN_<id>::varnameExample:
my $x = 10;
sub foo { return $x * 2; } # Compiled, captures $x
$x = 20; # Interpreted
say foo(); # 40 (sees updated value)
High Priority:
Medium Priority: 4. Implement remaining slow operations (22/255 used) 5. Add more superinstructions (compound assignments) 6. Context propagation (like codegen's EmitterContext)
Low Priority: 7. Unboxed int registers (30-50% potential speedup) 8. Inline caching for method calls/globals 9. Specialized loop dispatcher
The interpreter is production-ready with:
Key Learning: Disassembly completeness is as important as runtime implementation. Missing disassembly cases corrupt PC and make debugging impossible.
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
# 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
development
Migrate from JNA to a modern native access library (eliminate sun.misc.Unsafe warnings)