Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 0 additions & 34 deletions dev/custom_bytecode/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,42 +105,8 @@ Tool prints list of operators needing emit cases. Add between markers:
// GENERATED_OPERATORS_END
```

### Critical: LASTOP Management

Tool reads `LASTOP` from Opcodes.java to determine starting opcode:

```java
// In Opcodes.java
public static final short REDO = 220;

// Last manually-assigned opcode (for tool reference)
private static final short LASTOP = 220; // ← UPDATE WHEN ADDING MANUAL OPCODES
```

**When adding manual opcodes:**
1. Add constant BEFORE generated section
2. Update `LASTOP = <your new opcode number>`
3. Run tool - it starts at LASTOP + 1

### Gotchas

**1. Don't Edit Generated Sections**
- Between `// GENERATED_*_START` and `// GENERATED_*_END`
- Tool overwrites on regeneration
- Your changes will be lost!

**2. LASTOP Drift**
```java
// WRONG: Forgot to update LASTOP
public static final short MY_NEW_OP = 221;
private static final short LASTOP = 220; // ← Still 220!

// Tool starts at 221, collides with MY_NEW_OP!

// RIGHT: Always update LASTOP
public static final short MY_NEW_OP = 221;
private static final short LASTOP = 221; // ← Updated!
```

**3. Import Path Conversion**
- Tool auto-converts: `org/perlonjava/operators/...` → `org.perlonjava.operators....`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -494,17 +494,6 @@ public InterpretedCode compile(Node node, EmitterContext ctx) {
// Visit the node to generate bytecode
node.accept(this);

// Convert result to scalar context if needed (for eval STRING)
if (currentCallContext == RuntimeContextType.SCALAR && lastResultReg >= 0) {
RuntimeBase lastResult = null; // Can't access at compile time
// Use ARRAY_SIZE to convert arrays/lists to scalar count
int scalarReg = allocateRegister();
emit(Opcodes.ARRAY_SIZE);
emitReg(scalarReg);
emitReg(lastResultReg);
lastResultReg = scalarReg;
}

// Emit RETURN with last result register
// If no result was produced, return undef instead of register 0 ("this")
int returnReg;
Expand Down Expand Up @@ -1379,7 +1368,15 @@ void handleHashKeyValueSlice(BinaryOperatorNode node, OperatorNode leftOp) {
emitReg(rd);
emitReg(hashReg);
emitReg(keysListReg);
lastResultReg = rd;
if (currentCallContext == RuntimeContextType.SCALAR) {
int scalarReg = allocateRegister();
emit(Opcodes.LIST_TO_SCALAR);
emitReg(scalarReg);
emitReg(rd);
lastResultReg = scalarReg;
} else {
lastResultReg = rd;
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c
try {
// Main dispatch loop - JVM JIT optimizes switch to tableswitch (O(1) jump)
while (pc < bytecode.length) {
// Update current PC for caller()/stack trace reporting.
// This allows ExceptionFormatter to map pc->tokenIndex->line using code.errorUtil,
// which also honors #line directives inside eval strings.
InterpreterState.setCurrentPc(pc);
int opcode = bytecode[pc++];

switch (opcode) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,27 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler,

bytecodeCompiler.lastResultReg = hashReg;
return;
} else if (sigilOp.operator.equals("*") && sigilOp.operand instanceof IdentifierNode) {
// Handle local *glob = value
node.right.accept(bytecodeCompiler);
int valueReg = bytecodeCompiler.lastResultReg;

String globalName = NameNormalizer.normalizeVariableName(
((IdentifierNode) sigilOp.operand).name,
bytecodeCompiler.getCurrentPackage());
int nameIdx = bytecodeCompiler.addToStringPool(globalName);

int globReg = bytecodeCompiler.allocateRegister();
bytecodeCompiler.emitWithToken(Opcodes.LOCAL_GLOB, node.getIndex());
bytecodeCompiler.emitReg(globReg);
bytecodeCompiler.emit(nameIdx);

bytecodeCompiler.emit(Opcodes.STORE_GLOB);
bytecodeCompiler.emitReg(globReg);
bytecodeCompiler.emitReg(valueReg);

bytecodeCompiler.lastResultReg = globReg;
return;
}
} else if (localOperand instanceof ListNode) {
// Handle local($x) = value or local($x, $y) = (v1, v2)
Expand Down Expand Up @@ -723,8 +744,16 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler,
bytecodeCompiler.emitReg(targetReg);
bytecodeCompiler.emitReg(valueReg);
} else {
// Regular lexical - use MOVE
bytecodeCompiler.emit(Opcodes.MOVE);
// Regular lexical - create a fresh RuntimeScalar, then copy the value into it.
// LOAD_UNDEF allocates a new mutable RuntimeScalar in the target register;
// SET_SCALAR copies the source value into it.
// This avoids two bugs:
// - MOVE aliases constants from the pool, corrupting them on later mutation
// - SET_SCALAR alone modifies the existing object in-place, which breaks
// 'local' variable restoration when the register was shared
bytecodeCompiler.emit(Opcodes.LOAD_UNDEF);
bytecodeCompiler.emitReg(targetReg);
bytecodeCompiler.emit(Opcodes.SET_SCALAR);
bytecodeCompiler.emitReg(targetReg);
bytecodeCompiler.emitReg(valueReg);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -840,6 +840,10 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode
bytecodeCompiler.emitWithToken(Opcodes.EVAL_STRING, node.getIndex());
bytecodeCompiler.emitReg(rd);
bytecodeCompiler.emitReg(stringReg);
// Encode the eval operator's own call context (VOID/SCALAR/LIST) so
// wantarray() inside the eval body and the eval return value follow
// the correct context even when the surrounding sub is VOID.
bytecodeCompiler.emit(bytecodeCompiler.currentCallContext);

bytecodeCompiler.lastResultReg = rd;
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@
*/
public class EvalStringHandler {

private static final boolean EVAL_TRACE =
System.getenv("JPERL_EVAL_TRACE") != null;

private static void evalTrace(String msg) {
if (EVAL_TRACE) {
System.err.println("[eval-trace] " + msg);
}
}

/**
* Evaluate a Perl string dynamically.
*
Expand All @@ -52,7 +61,18 @@ public static RuntimeScalar evalString(String perlCode,
String sourceName,
int sourceLine,
int callContext) {
return evalStringList(perlCode, currentCode, registers, sourceName, sourceLine, callContext).scalar();
}

public static RuntimeList evalStringList(String perlCode,
InterpretedCode currentCode,
RuntimeBase[] registers,
String sourceName,
int sourceLine,
int callContext) {
try {
evalTrace("EvalStringHandler enter ctx=" + callContext + " srcName=" + sourceName +
" srcLine=" + sourceLine + " codeLen=" + (perlCode != null ? perlCode.length() : -1));
// Step 1: Clear $@ at start of eval
GlobalVariable.getGlobalVariable("main::@").set("");

Expand Down Expand Up @@ -85,17 +105,19 @@ public static RuntimeScalar evalString(String perlCode,
String compilePackage = InterpreterState.currentPackage.get().toString();
symbolTable.setCurrentPackage(compilePackage, false);

evalTrace("EvalStringHandler compilePackage=" + compilePackage + " fileName=" + opts.fileName);

ErrorMessageUtil errorUtil = new ErrorMessageUtil(sourceName, tokens);
EmitterContext ctx = new EmitterContext(
new JavaClassInfo(),
symbolTable,
null, // mv
null, // cw
RuntimeContextType.SCALAR,
false, // isBoxed
errorUtil,
opts,
null // unitcheckBlocks
new JavaClassInfo(),
symbolTable,
null, // mv
null, // cw
callContext,
false, // isBoxed
errorUtil,
opts,
null // unitcheckBlocks
);

Parser parser = new Parser(ctx, tokens);
Expand All @@ -110,7 +132,7 @@ public static RuntimeScalar evalString(String perlCode,

// Sort parent variables by register index for consistent ordering
List<Map.Entry<String, Integer>> sortedVars = new ArrayList<>(
currentCode.variableRegistry.entrySet()
currentCode.variableRegistry.entrySet()
);
sortedVars.sort(Map.Entry.comparingByValue());

Expand Down Expand Up @@ -149,8 +171,8 @@ public static RuntimeScalar evalString(String perlCode,
continue;
}
} else if (!(value instanceof RuntimeArray ||
value instanceof RuntimeHash ||
value instanceof RuntimeCode)) {
value instanceof RuntimeHash ||
value instanceof RuntimeCode)) {
// Skip this register - it contains an internal object
continue;
}
Expand All @@ -165,23 +187,22 @@ public static RuntimeScalar evalString(String perlCode,
}

// Step 4: Compile AST to interpreter bytecode with adjusted variable registry.
// The compile-time package is already propagated via ctx.symbolTable (set above
// from currentCode.compilePackage), so BytecodeCompiler will use it for name
// resolution (e.g. *named -> FOO3::named) without needing setCompilePackage().
// The compile-time package is already propagated via ctx.symbolTable.
BytecodeCompiler compiler = new BytecodeCompiler(
sourceName + " (eval)",
sourceLine,
errorUtil,
adjustedRegistry // Pass adjusted registry for variable capture
sourceName + " (eval)",
sourceLine,
errorUtil,
adjustedRegistry // Pass adjusted registry for variable capture
);
InterpretedCode evalCode = compiler.compile(ast, ctx); // Pass ctx for context propagation

evalTrace("EvalStringHandler compiled bytecodeLen=" + (evalCode != null ? evalCode.bytecode.length : -1) +
" src=" + (evalCode != null ? evalCode.sourceName : "null"));
if (RuntimeCode.DISASSEMBLE) {
System.out.println(evalCode.disassemble());
}

// Step 4.5: Store source lines in debugger symbol table if $^P flags are set
// This implements Perl's eval source retention feature for debugging
// Generate eval filename and store lines in @{"_<(eval N)"}
int debugFlags = GlobalVariable.getGlobalVariable(GlobalContext.encodeSpecialVar("P")).getInt();
if (debugFlags != 0) {
String evalFilename = RuntimeCode.getNextEvalFilename();
Expand All @@ -197,18 +218,17 @@ public static RuntimeScalar evalString(String perlCode,
}

// Step 6: Execute the compiled code
// Use the true outer call context so wantarray() inside the eval body
// returns the correct value (undef for void, false for scalar, true for list).
RuntimeArray args = new RuntimeArray(); // Empty @_
RuntimeList result = evalCode.apply(args, callContext);

// Step 7: Return scalar result
return result.scalar();

evalTrace("EvalStringHandler exec ok ctx=" + callContext +
" resultScalar=" + (result != null ? result.scalar().toString() : "null") +
" resultBool=" + (result != null && result.scalar() != null ? result.scalar().getBoolean() : false) +
" $@=" + GlobalVariable.getGlobalVariable("main::@").toString());
return result;
} catch (Exception e) {
// Step 8: Handle errors - set $@ and return undef
evalTrace("EvalStringHandler exec exception ctx=" + callContext + " ex=" + e.getClass().getSimpleName() + " msg=" + e.getMessage());
WarnDie.catchEval(e);
return RuntimeScalarCache.scalarUndef;
return new RuntimeList(new RuntimeScalar());
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ public class InterpreterState {
private static final ThreadLocal<Deque<InterpreterFrame>> frameStack =
ThreadLocal.withInitial(ArrayDeque::new);

private static final ThreadLocal<Deque<Integer>> pcStack =
ThreadLocal.withInitial(ArrayDeque::new);

/**
* Thread-local RuntimeScalar holding the runtime current package name.
*
Expand Down Expand Up @@ -60,6 +63,7 @@ public InterpreterFrame(InterpretedCode code, String packageName, String subrout
*/
public static void push(InterpretedCode code, String packageName, String subroutineName) {
frameStack.get().push(new InterpreterFrame(code, packageName, subroutineName));
pcStack.get().push(0);
}

/**
Expand All @@ -71,6 +75,19 @@ public static void pop() {
if (!stack.isEmpty()) {
stack.pop();
}

Deque<Integer> pcs = pcStack.get();
if (!pcs.isEmpty()) {
pcs.pop();
}
}

public static void setCurrentPc(int pc) {
Deque<Integer> pcs = pcStack.get();
if (!pcs.isEmpty()) {
pcs.pop();
pcs.push(pc);
}
}

/**
Expand All @@ -93,4 +110,8 @@ public static InterpreterFrame current() {
public static List<InterpreterFrame> getStack() {
return new ArrayList<>(frameStack.get());
}
}

public static List<Integer> getPcStack() {
return new ArrayList<>(pcStack.get());
}
}
Loading