diff --git a/dev/custom_bytecode/SKILL.md b/dev/custom_bytecode/SKILL.md index 5dbc95944..8b470b147 100644 --- a/dev/custom_bytecode/SKILL.md +++ b/dev/custom_bytecode/SKILL.md @@ -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 = ` -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....` diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index 1fb01f516..d5a3b98c6 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -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; @@ -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; + } } /** diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index ef589e88b..cd123fc68 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -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) { diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java b/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java index 27f5a3608..056e6ee0a 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java @@ -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) @@ -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); } diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java index 971e9dc1f..7daa3ae7a 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java @@ -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 { diff --git a/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java b/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java index 5980ef615..6259c44b0 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java @@ -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. * @@ -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(""); @@ -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); @@ -110,7 +132,7 @@ public static RuntimeScalar evalString(String perlCode, // Sort parent variables by register index for consistent ordering List> sortedVars = new ArrayList<>( - currentCode.variableRegistry.entrySet() + currentCode.variableRegistry.entrySet() ); sortedVars.sort(Map.Entry.comparingByValue()); @@ -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; } @@ -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(); @@ -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()); } } diff --git a/src/main/java/org/perlonjava/backend/bytecode/InterpreterState.java b/src/main/java/org/perlonjava/backend/bytecode/InterpreterState.java index 72c6656d1..60e102066 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/InterpreterState.java +++ b/src/main/java/org/perlonjava/backend/bytecode/InterpreterState.java @@ -18,6 +18,9 @@ public class InterpreterState { private static final ThreadLocal> frameStack = ThreadLocal.withInitial(ArrayDeque::new); + private static final ThreadLocal> pcStack = + ThreadLocal.withInitial(ArrayDeque::new); + /** * Thread-local RuntimeScalar holding the runtime current package name. * @@ -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); } /** @@ -71,6 +75,19 @@ public static void pop() { if (!stack.isEmpty()) { stack.pop(); } + + Deque pcs = pcStack.get(); + if (!pcs.isEmpty()) { + pcs.pop(); + } + } + + public static void setCurrentPc(int pc) { + Deque pcs = pcStack.get(); + if (!pcs.isEmpty()) { + pcs.pop(); + pcs.push(pc); + } } /** @@ -93,4 +110,8 @@ public static InterpreterFrame current() { public static List getStack() { return new ArrayList<>(frameStack.get()); } -} \ No newline at end of file + + public static List getPcStack() { + return new ArrayList<>(pcStack.get()); + } +} diff --git a/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java b/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java index e0584e432..e2a553861 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java @@ -56,6 +56,15 @@ */ public class SlowOpcodeHandler { + 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); + } + } + // ================================================================= // SLICE AND DEREFERENCE OPERATIONS // ================================================================= @@ -261,6 +270,13 @@ public static int executeEvalString( int rd = bytecode[pc++]; int stringReg = bytecode[pc++]; + int evalCallContext = RuntimeContextType.SCALAR; + // Newer bytecode encodes the eval operator's own call context (VOID/SCALAR/LIST) + // so eval semantics are correct even when the surrounding statement is compiled + // in VOID context. + if (pc < bytecode.length) { + evalCallContext = bytecode[pc++]; + } // Get the code string - handle both RuntimeScalar and RuntimeList (from string interpolation) RuntimeBase codeValue = registers[stringReg]; @@ -272,23 +288,49 @@ public static int executeEvalString( codeScalar = codeValue.scalar(); } String perlCode = codeScalar.toString(); + evalTrace("EVAL_STRING opcode enter rd=r" + rd + " strReg=r" + stringReg + + " ctx=" + evalCallContext + " outerWantarray=" + ((RuntimeScalar) registers[2]).getInt() + + " src=" + (code != null ? code.sourceName : "null")); // Read outer wantarray from register 2 (set by BytecodeInterpreter from the call site context). // This is the true calling context (VOID/SCALAR/LIST) that wantarray() inside the // eval body must reflect — exactly as evalStringWithInterpreter receives callContext. - int callContext = ((RuntimeScalar) registers[2]).getInt(); - - // Call EvalStringHandler to parse, compile, and execute - RuntimeScalar result = EvalStringHandler.evalString( - perlCode, - code, // Current InterpretedCode for context - registers, // Current registers for variable access - code.sourceName, - code.sourceLine, - callContext // True outer context for wantarray inside eval body - ); + int callContext = evalCallContext; + if (registers[2] instanceof RuntimeScalar rs) { + // For backward compatibility with older bytecode, or if evalCallContext + // is not set correctly, fall back to the outer wantarray register. + if (callContext == 0 && rs.value != null) { + callContext = rs.getInt(); + } + } - registers[rd] = result; + if (callContext == RuntimeContextType.LIST) { + // Return list context result + RuntimeList result = EvalStringHandler.evalStringList( + perlCode, + code, // Current InterpretedCode for context + registers, // Current registers for variable access + code.sourceName, + code.sourceLine, + callContext + ); + registers[rd] = result; + evalTrace("EVAL_STRING opcode exit LIST stored=" + (registers[rd] != null ? registers[rd].getClass().getSimpleName() : "null") + + " scalar=" + result.scalar().toString()); + } else { + // Scalar/void context: return scalar result + RuntimeScalar result = EvalStringHandler.evalString( + perlCode, + code, // Current InterpretedCode for context + registers, // Current registers for variable access + code.sourceName, + code.sourceLine, + callContext + ); + registers[rd] = result; + evalTrace("EVAL_STRING opcode exit SCALAR/VOID stored=" + (registers[rd] != null ? registers[rd].getClass().getSimpleName() : "null") + + " val=" + result.toString() + " bool=" + result.getBoolean()); + } return pc; } diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitEval.java b/src/main/java/org/perlonjava/backend/jvm/EmitEval.java index 860c79f47..5c68353b4 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitEval.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitEval.java @@ -50,6 +50,15 @@ * */ public class EmitEval { + + 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); + } + } /** * Handles the emission of bytecode for the Perl 'eval' operator. * @@ -89,6 +98,9 @@ static void handleEvalOperator(EmitterVisitor emitterVisitor, OperatorNode node) EmitterContext ctx = emitterVisitor.ctx; MethodVisitor mv = ctx.mv; + evalTrace("EmitEval.handleEvalOperator op=" + node.operator + " ctx=" + emitterVisitor.ctx.contextType + + " file=" + emitterVisitor.ctx.compilerOptions.fileName + " token=" + node.tokenIndex); + // Log current symbol table state for debugging emitterVisitor.ctx.logDebug("(eval) ctx.symbolTable.getAllVisibleVariables"); @@ -569,12 +581,14 @@ private static void emitEvalInterpreterPath(EmitterVisitor emitterVisitor, Strin // For eval, use the context determined by how the eval result is used // This matches the compiler path which uses a compile-time constant if (emitterVisitor.ctx.contextType == RuntimeContextType.RUNTIME) { - // If context is RUNTIME, load it from wantarray variable - mv.visitVarInsn(Opcodes.ILOAD, emitterVisitor.ctx.symbolTable.getVariableIndex("wantarray")); + // If context is RUNTIME, load it from apply(@_, callContext) method argument. + // Slot 0=this, 1=@_, 2=callContext. + mv.visitVarInsn(Opcodes.ILOAD, 2); } else { // Otherwise use the compile-time constant (LIST/SCALAR/VOID) mv.visitLdcInsn(emitterVisitor.ctx.contextType); } + evalTrace("EmitEval.emitEvalInterpreterPath tag=" + evalTag + " pushCtx=" + emitterVisitor.ctx.contextType); // Stack: [RuntimeScalar(String), String, Object[], RuntimeArray(@_), int] // Call evalStringWithInterpreter which returns RuntimeList directly diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitLogicalOperator.java b/src/main/java/org/perlonjava/backend/jvm/EmitLogicalOperator.java index de2599f57..55f8b79b5 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitLogicalOperator.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitLogicalOperator.java @@ -20,6 +20,15 @@ */ public class EmitLogicalOperator { + 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); + } + } + /** * Emits bytecode for the flip-flop operator, which is used in range-like conditions. * @@ -277,6 +286,7 @@ private static void emitLogicalOperatorSimple(EmitterVisitor emitterVisitor, Bin Label endLabel = new Label(); if (emitterVisitor.ctx.contextType == RuntimeContextType.VOID) { + evalTrace("EmitLogicalOperatorSimple VOID op=" + node.operator + " emit LHS in SCALAR; RHS in SCALAR"); node.left.accept(emitterVisitor.with(RuntimeContextType.SCALAR)); mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/perlonjava/runtime/runtimetypes/RuntimeBase", getBoolean, "()Z", false); mv.visitJumpInsn(compareOpcode, endLabel); @@ -311,15 +321,18 @@ private static void emitLogicalOperatorSimple(EmitterVisitor emitterVisitor, Bin rewritten = true; } - // For RUNTIME context, preserve it; otherwise use SCALAR for boolean evaluation - int operandContext = emitterVisitor.ctx.contextType == RuntimeContextType.RUNTIME + // LHS is always evaluated in SCALAR context for the boolean/truthiness test. + // RHS inherits the enclosing context (including RUNTIME) since its value + // becomes the result of the logical operator and must honour wantarray. + int lhsContext = RuntimeContextType.SCALAR; + int rhsContext = emitterVisitor.ctx.contextType == RuntimeContextType.RUNTIME ? RuntimeContextType.RUNTIME : RuntimeContextType.SCALAR; resultRef = emitterVisitor.ctx.javaClassInfo.acquireSpillRefOrAllocate(emitterVisitor.ctx.symbolTable); // Evaluate LHS and store it. - node.left.accept(emitterVisitor.with(operandContext)); + node.left.accept(emitterVisitor.with(lhsContext)); emitterVisitor.ctx.javaClassInfo.storeSpillRef(mv, resultRef); // Boolean test on the stored LHS. @@ -328,7 +341,7 @@ private static void emitLogicalOperatorSimple(EmitterVisitor emitterVisitor, Bin mv.visitJumpInsn(compareOpcode, endLabel); // LHS didn't short-circuit: evaluate RHS, overwrite result. - node.right.accept(emitterVisitor.with(operandContext)); + node.right.accept(emitterVisitor.with(rhsContext)); emitterVisitor.ctx.javaClassInfo.storeSpillRef(mv, resultRef); // Return whichever side won the short-circuit. diff --git a/src/main/java/org/perlonjava/frontend/analysis/EmitterVisitor.java b/src/main/java/org/perlonjava/frontend/analysis/EmitterVisitor.java index 9510c0e3d..9ee9e17df 100644 --- a/src/main/java/org/perlonjava/frontend/analysis/EmitterVisitor.java +++ b/src/main/java/org/perlonjava/frontend/analysis/EmitterVisitor.java @@ -65,8 +65,9 @@ public EmitterVisitor with(int contextType) { public void pushCallContext() { // push call context to stack if (ctx.contextType == RuntimeContextType.RUNTIME) { - // Retrieve wantarray value from JVM local vars - ctx.mv.visitVarInsn(Opcodes.ILOAD, ctx.symbolTable.getVariableIndex("wantarray")); + // Retrieve callContext from apply(@_, callContext) method argument. + // Slot 0=this, 1=@_, 2=callContext. + ctx.mv.visitVarInsn(Opcodes.ILOAD, 2); } else { ctx.mv.visitLdcInsn(ctx.contextType); } diff --git a/src/main/java/org/perlonjava/frontend/parser/StatementResolver.java b/src/main/java/org/perlonjava/frontend/parser/StatementResolver.java index 3f7e9559f..c1656a31d 100644 --- a/src/main/java/org/perlonjava/frontend/parser/StatementResolver.java +++ b/src/main/java/org/perlonjava/frontend/parser/StatementResolver.java @@ -548,6 +548,17 @@ public static Node parseStatement(Parser parser, String label) { Node result = switch (token.text) { case "..." -> { TokenUtils.consume(parser); + // The yadayada operator `...` is only valid as a standalone statement. + // In expression contexts (e.g. "... + 0", "... if 1", "my $a = ..."), perl5 + // reports a syntax error at compile time. + LexerToken next = TokenUtils.peek(parser); + boolean isStatementTerminator = + next.type == LexerTokenType.EOF || + next.text.equals(";") || + next.text.equals("}"); + if (!isStatementTerminator) { + throw new PerlCompilerException(parser.tokenIndex, "syntax error", parser.ctx.errorUtil); + } yield dieWarnNode(parser, "die", new ListNode(List.of( new StringNode("Unimplemented", parser.tokenIndex)), parser.tokenIndex), parser.tokenIndex); } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/ErrorMessageUtil.java b/src/main/java/org/perlonjava/runtime/runtimetypes/ErrorMessageUtil.java index 485383d15..7025fe0b5 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/ErrorMessageUtil.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/ErrorMessageUtil.java @@ -18,6 +18,9 @@ public class ErrorMessageUtil { private int tokenIndex; private int lastLineNumber; + public record SourceLocation(String fileName, int lineNumber) { + } + /** * Constructs an ErrorMessageUtil with the specified file name and list of tokens. * @@ -252,5 +255,77 @@ public int getLineNumberAccurate(int index) { } return lineNumber; } + + public SourceLocation getSourceLocationAccurate(int index) { + String currentFileName = fileName; + int lineNumber = 1; + + boolean atBeginningOfLine = true; + int i = 0; + while (i <= index && i < tokens.size()) { + LexerToken tok = tokens.get(i); + if (tok.type == LexerTokenType.EOF) { + break; + } + + if (tok.type == LexerTokenType.NEWLINE) { + lineNumber++; + atBeginningOfLine = true; + i++; + continue; + } + + if (atBeginningOfLine && tok.type == LexerTokenType.OPERATOR && tok.text.equals("#")) { + int j = i + 1; + while (j < tokens.size() && tokens.get(j).type == LexerTokenType.WHITESPACE) { + j++; + } + if (j < tokens.size() && tokens.get(j).type == LexerTokenType.IDENTIFIER && tokens.get(j).text.equals("line")) { + j++; + while (j < tokens.size() && tokens.get(j).type == LexerTokenType.WHITESPACE) { + j++; + } + if (j < tokens.size() && tokens.get(j).type == LexerTokenType.NUMBER) { + int directiveLine = -1; + try { + directiveLine = Integer.parseInt(tokens.get(j).text); + } catch (NumberFormatException e) { + directiveLine = -1; + } + j++; + while (j < tokens.size() && tokens.get(j).type == LexerTokenType.WHITESPACE) { + j++; + } + if (j < tokens.size() && tokens.get(j).type == LexerTokenType.OPERATOR && tokens.get(j).text.equals("\"")) { + j++; + StringBuilder filenameBuilder = new StringBuilder(); + while (j < tokens.size() && !(tokens.get(j).type == LexerTokenType.OPERATOR && tokens.get(j).text.equals("\""))) { + filenameBuilder.append(tokens.get(j).text); + j++; + } + if (j < tokens.size() && tokens.get(j).type == LexerTokenType.OPERATOR && tokens.get(j).text.equals("\"")) { + String directiveFile = filenameBuilder.toString(); + if (!directiveFile.isEmpty()) { + currentFileName = directiveFile; + } + } + } + + if (directiveLine >= 1) { + // The directive applies to the following line. + lineNumber = directiveLine - 1; + } + } + } + } + + if (tok.type != LexerTokenType.WHITESPACE) { + atBeginningOfLine = false; + } + i++; + } + + return new SourceLocation(currentFileName, lineNumber); + } } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/ExceptionFormatter.java b/src/main/java/org/perlonjava/runtime/runtimetypes/ExceptionFormatter.java index d699a6170..a57c306eb 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/ExceptionFormatter.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/ExceptionFormatter.java @@ -55,6 +55,7 @@ private static ArrayList> formatThrowable(Throwable t) { // Each BytecodeInterpreter.execute() JVM frame corresponds to one Perl call // level; consuming them in order gives the correct nested call stack. var interpreterFrames = InterpreterState.getStack(); + var interpreterPcs = InterpreterState.getPcStack(); int interpreterFrameIndex = 0; for (var element : t.getStackTrace()) { @@ -86,6 +87,7 @@ private static ArrayList> formatThrowable(Throwable t) { String pkg = (interpreterFrameIndex == 0) ? InterpreterState.currentPackage.get().toString() : frame.packageName; + int currentInterpreterFrameIndex = interpreterFrameIndex; interpreterFrameIndex++; String subName = frame.subroutineName; @@ -95,11 +97,28 @@ private static ArrayList> formatThrowable(Throwable t) { var entry = new ArrayList(); entry.add(pkg); - entry.add(frame.code.sourceName); - entry.add(String.valueOf(frame.code.sourceLine)); + String filename = frame.code.sourceName; + String line = String.valueOf(frame.code.sourceLine); + if (currentInterpreterFrameIndex < interpreterPcs.size()) { + Integer tokenIndex = null; + int pc = interpreterPcs.get(currentInterpreterFrameIndex); + if (frame.code.pcToTokenIndex != null && !frame.code.pcToTokenIndex.isEmpty()) { + var entryPc = frame.code.pcToTokenIndex.floorEntry(pc); + if (entryPc != null) { + tokenIndex = entryPc.getValue(); + } + } + if (tokenIndex != null && frame.code.errorUtil != null) { + ErrorMessageUtil.SourceLocation loc = frame.code.errorUtil.getSourceLocationAccurate(tokenIndex); + filename = loc.fileName(); + line = String.valueOf(loc.lineNumber()); + } + } + entry.add(filename); + entry.add(line); entry.add(subName); stackTrace.add(entry); - lastFileName = frame.code.sourceName != null ? frame.code.sourceName : ""; + lastFileName = filename != null ? filename : ""; } } } else if (element.getClassName().contains("org.perlonjava.anon") || diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java index bc1a18868..21ed1ed17 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java @@ -63,6 +63,15 @@ public class RuntimeCode extends RuntimeBase implements RuntimeScalarReference { public static final boolean EVAL_VERBOSE = System.getenv("JPERL_EVAL_VERBOSE") != null; + public 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); + } + } + /** * Flag to enable disassembly of eval STRING bytecode. * When set, prints the interpreter bytecode for each eval STRING compilation. @@ -711,6 +720,9 @@ public static RuntimeList evalStringWithInterpreter( RuntimeArray args, int callContext) throws Throwable { + evalTrace("evalStringWithInterpreter enter tag=" + evalTag + " ctx=" + callContext + + " codeType=" + code.type + " codeLen=" + (code.toString() != null ? code.toString().length() : -1)); + // Retrieve the eval context that was saved at program compile-time EmitterContext ctx = RuntimeCode.evalContext.get(evalTag); @@ -734,6 +746,8 @@ public static RuntimeList evalStringWithInterpreter( try { String evalString = code.toString(); + evalTrace("evalStringWithInterpreter parse start tag=" + evalTag + " ctx=" + callContext + + " fileName=" + ctx.compilerOptions.fileName); // Handle Unicode source detection (same logic as evalStringHelper) boolean hasUnicode = false; if (!ctx.isEvalbytes && code.type != RuntimeScalarType.BYTE_STRING) { @@ -857,6 +871,9 @@ public static RuntimeList evalStringWithInterpreter( adjustedRegistry); compiler.setCompilePackage(capturedSymbolTable.getCurrentPackage()); interpretedCode = compiler.compile(ast, evalCtx); + evalTrace("evalStringWithInterpreter compiled tag=" + evalTag + + " bytecodeLen=" + (interpretedCode != null ? interpretedCode.bytecode.length : -1) + + " src=" + (interpretedCode != null ? interpretedCode.sourceName : "null")); if (DISASSEMBLE) { System.out.println(interpretedCode.disassemble()); } @@ -940,6 +957,11 @@ public static RuntimeList evalStringWithInterpreter( try { result = interpretedCode.apply(args, callContext); + evalTrace("evalStringWithInterpreter exec ok tag=" + evalTag + " ctx=" + callContext + + " resultClass=" + (result != null ? result.getClass().getSimpleName() : "null") + + " resultScalar=" + (result != null ? result.scalar().toString() : "null") + + " resultBool=" + (result != null && result.scalar() != null ? result.scalar().getBoolean() : false)); + // Clear $@ on successful execution RuntimeScalar err = GlobalVariable.getGlobalVariable("main::@"); err.set(""); @@ -947,6 +969,8 @@ public static RuntimeList evalStringWithInterpreter( return result; } catch (PerlDieException e) { + evalTrace("evalStringWithInterpreter exec die tag=" + evalTag + " ctx=" + callContext + + " payload=" + (e.getPayload() != null ? e.getPayload().getFirst().toString() : "null")); // Runtime error - set $@ and return undef/empty list RuntimeScalar err = GlobalVariable.getGlobalVariable("main::@"); RuntimeBase payload = e.getPayload(); @@ -976,13 +1000,9 @@ public static RuntimeList evalStringWithInterpreter( } } catch (Throwable e) { - // Other runtime errors - set $@ and return undef/empty list - RuntimeScalar err = GlobalVariable.getGlobalVariable("main::@"); - String message = e.getMessage(); - if (message == null || message.isEmpty()) { - message = e.getClass().getSimpleName(); - } - err.set(message); + evalTrace("evalStringWithInterpreter exec throwable tag=" + evalTag + " ctx=" + callContext + + " ex=" + e.getClass().getSimpleName() + " msg=" + e.getMessage()); + WarnDie.catchEval(e); // Return undef/empty list if (callContext == RuntimeContextType.LIST) { @@ -993,6 +1013,8 @@ public static RuntimeList evalStringWithInterpreter( } } finally { + evalTrace("evalStringWithInterpreter exit tag=" + evalTag + " ctx=" + callContext + + " $@=" + GlobalVariable.getGlobalVariable("main::@").toString()); // Restore dynamic variables (local) to their state before eval DynamicVariableManager.popToLocalLevel(dynamicVarLevel); diff --git a/src/main/perl/lib/B.pm b/src/main/perl/lib/B.pm index 62af71c8f..41a40621a 100644 --- a/src/main/perl/lib/B.pm +++ b/src/main/perl/lib/B.pm @@ -11,6 +11,11 @@ our $VERSION = '1.00_perlonjava'; # Flag indicating this is a stub implementation with limited introspection our $INCOMPLETE = 1; +# SV flags (very partial) +use constant { + SVf_IOK => 0x0001, +}; + # CV flags use constant { CVf_ANON => 0x0004, @@ -25,6 +30,24 @@ package B::SV { my ($class, $ref) = @_; return bless { ref => $ref }, $class; } + + sub FLAGS { + my $self = shift; + my $r = $self->{ref}; + + # For the debugger source arrays (@{"_<..."}), perl stores lines as PVIV with IOK. + # This stub implementation marks any defined, non-empty scalar as having IOK. + if (ref($r) eq 'SCALAR') { + my $v = $$r; + return (defined($v) && length($v)) ? B::SVf_IOK() : 0; + } + + return 0; + } +} + +package B::PVIV { + our @ISA = ('B::SV'); } package B::CV { @@ -129,12 +152,19 @@ sub svref_2object { return B::CV->new($ref); } + if ($type eq 'SCALAR') { + return B::PVIV->new($ref); + } + return B::SV->new($ref); } # Export CVf_ANON as a function sub CVf_ANON() { return 0x0004; } +# Export SVf_IOK as a function +sub SVf_IOK() { return 0x0001; } + # Special SV names our @specialsv_name = ('Nullsv', '&PL_sv_undef', '&PL_sv_yes', '&PL_sv_no');