From 6c475f224129a1691cc1751b7b70a0e5a9bb5fd9 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Fri, 15 May 2026 09:42:58 +0200 Subject: [PATCH] fix: correct B::CV->START line for coderefs (Fennec::Lite FENNEC_ITEM) Fennec::Lite filters tests with start_line from B::svref_2object($cv)->START->line. The bundled B stub always returned line 0, so every group matched and RunByLine.t failed (correcu from "correct"++). - Track cvStartFile/cvStartLine on RuntimeCode; set for InterpretedCode, CompiledCode, parse-time placeholders, and JVM anon subs via makeCodeObject. - Add Internals::jperl_cv_start_location; wire B::CV::START to use it. Fixes jcpan -t Fennec::Lite. Generated with [Cursor](https://cursor.com/docs) Co-Authored-By: Cursor Co-authored-by: Cursor --- .../backend/bytecode/InterpretedCode.java | 2 ++ .../backend/jvm/EmitSubroutine.java | 16 ++++++++- .../backend/jvm/EmitterMethodCreator.java | 35 ++++++++++++++++--- .../frontend/parser/SubroutineParser.java | 18 ++++++++++ .../runtime/perlmodule/Internals.java | 32 +++++++++++++++++ .../runtime/runtimetypes/RuntimeCode.java | 26 ++++++++++++++ src/main/perl/lib/B.pm | 19 ++++++++-- 7 files changed, 140 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java b/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java index a05fbdcaa..1ef9cfcdd 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java +++ b/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java @@ -192,6 +192,8 @@ public InterpretedCode(int[] bytecode, Object[] constants, String[] stringPool, if (this.packageName == null && compilePackage != null) { this.packageName = compilePackage; } + this.cvStartFile = sourceName; + this.cvStartLine = sourceLine; // Scan bytecodes to find registers used by SCOPE_EXIT_CLEANUP opcodes. // These are the actual "my" variable registers that need cleanup during // exception propagation. Temporaries (hash element aliases, method return diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitSubroutine.java b/src/main/java/org/perlonjava/backend/jvm/EmitSubroutine.java index d68ef2a3c..ee595c8d4 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitSubroutine.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitSubroutine.java @@ -241,6 +241,18 @@ public static void emitSubroutine(EmitterContext ctx, SubroutineNode node) { if (CompilerOptions.DEBUG_ENABLED) ctx.logDebug("Generated class env: " + Arrays.toString(newEnv)); RuntimeCode.anonSubs.put(subCtx.javaClassInfo.javaClassName, generatedClass); // Cache the class + String cvStartFile = "-e"; + int cvStartLine = 0; + if (ctx.errorUtil != null && node.block != null) { + var loc = ctx.errorUtil.getSourceLocationAccurate(node.block.getIndex()); + cvStartLine = loc.lineNumber(); + if (loc.fileName() != null && !loc.fileName().isEmpty()) { + cvStartFile = loc.fileName(); + } + } else if (ctx.compilerOptions != null && ctx.compilerOptions.fileName != null) { + cvStartFile = ctx.compilerOptions.fileName; + } + // Transfer pad constants (cached string literals referenced via \) from compile time // to a registry so makeCodeObject() can attach them to the RuntimeCode at runtime. if (subCtx.javaClassInfo.padConstants != null && !subCtx.javaClassInfo.padConstants.isEmpty()) { @@ -289,11 +301,13 @@ public static void emitSubroutine(EmitterContext ctx, SubroutineNode node) { mv.visitInsn(Opcodes.ACONST_NULL); } mv.visitLdcInsn(ctx.symbolTable.getCurrentPackage()); + mv.visitLdcInsn(cvStartFile); + mv.visitLdcInsn(cvStartLine); mv.visitMethodInsn( Opcodes.INVOKESTATIC, "org/perlonjava/runtime/runtimetypes/RuntimeCode", "makeCodeObject", - "(Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;)Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;", + "(Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;I)Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;", false); } catch (InterpreterFallbackException fallback) { // JVM compilation failed (e.g., ASM frame crash) - use InterpretedCode instead diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java b/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java index 6669946e1..0d6100f30 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java @@ -1733,7 +1733,7 @@ public static RuntimeCode createRuntimeCode( if (SHOW_FALLBACK) { System.err.println("Note: JVM compilation succeeded."); } - return wrapAsCompiledCode(generatedClass, ctx); + return wrapAsCompiledCode(generatedClass, ctx, ast); } catch (MethodTooLargeException e) { if (USE_INTERPRETER_FALLBACK) { @@ -1788,9 +1788,10 @@ public static RuntimeCode createRuntimeCode( * * @param generatedClass The compiled JVM class * @param ctx The compiler context + * @param ast Body root node (for {@code B::CV->START} source line) * @return CompiledCode wrapping the compiled class */ - private static CompiledCode wrapAsCompiledCode(Class generatedClass, EmitterContext ctx) { + private static CompiledCode wrapAsCompiledCode(Class generatedClass, EmitterContext ctx, Node ast) { try { // Get the constructor (may have parameters for captured variables) String[] env = (ctx.capturedEnv != null) ? ctx.capturedEnv : ctx.symbolTable.getVariableNames(); @@ -1828,15 +1829,19 @@ private static CompiledCode wrapAsCompiledCode(Class generatedClass, EmitterC RuntimeScalar selfRef = new RuntimeScalar(); selfRef.type = RuntimeScalarType.CODE; // Note: ctx doesn't have prototype field, it's set separately by caller - selfRef.value = new CompiledCode(methodHandle, codeObject, null, generatedClass, ctx); + CompiledCode cc = new CompiledCode(methodHandle, codeObject, null, generatedClass, ctx); + applyCvStartFromAst(ctx, ast, cc); + selfRef.value = cc; field.set(codeObject, selfRef); - return (CompiledCode) selfRef.value; + return cc; } else { // Has captured variables - caller must instantiate later // Return a CompiledCode with null codeObject/methodHandle // The caller will fill these in via reflection (see SubroutineParser pattern) - return new CompiledCode(null, null, null, generatedClass, ctx); + CompiledCode cc = new CompiledCode(null, null, null, generatedClass, ctx); + applyCvStartFromAst(ctx, ast, cc); + return cc; } } catch (VerifyError ve) { @@ -1850,6 +1855,26 @@ private static CompiledCode wrapAsCompiledCode(Class generatedClass, EmitterC } } + private static void applyCvStartFromAst(EmitterContext ctx, Node ast, RuntimeCode target) { + if (target == null || ctx == null) { + return; + } + String cvFile = "-e"; + if (ctx.compilerOptions != null && ctx.compilerOptions.fileName != null) { + cvFile = ctx.compilerOptions.fileName; + } + int cvLine = 0; + if (ctx.errorUtil != null && ast != null) { + ErrorMessageUtil.SourceLocation loc = ctx.errorUtil.getSourceLocationAccurate(ast.getIndex()); + cvLine = loc.lineNumber(); + if (loc.fileName() != null && !loc.fileName().isEmpty()) { + cvFile = loc.fileName(); + } + } + target.cvStartFile = cvFile; + target.cvStartLine = cvLine; + } + /** * Compile AST to interpreter bytecode. *

diff --git a/src/main/java/org/perlonjava/frontend/parser/SubroutineParser.java b/src/main/java/org/perlonjava/frontend/parser/SubroutineParser.java index 71ed9640b..1d9b13089 100644 --- a/src/main/java/org/perlonjava/frontend/parser/SubroutineParser.java +++ b/src/main/java/org/perlonjava/frontend/parser/SubroutineParser.java @@ -1248,6 +1248,18 @@ public static ListNode handleNamedSubWithFilter(Parser parser, String subName, S ? fullName.substring(0, lastSep) : parser.ctx.symbolTable.getCurrentPackage(); + // B::CV->START line must be available before lazy compilation instantiates + // the JVM/interpreted body — e.g. Fennec::Lite calls B::svref_2object($cr) + // from _add_tests while registering coderefs. + if (parser.ctx.errorUtil != null && block != null) { + var loc = parser.ctx.errorUtil.getSourceLocationAccurate(block.tokenIndex); + placeholder.cvStartFile = loc.fileName(); + placeholder.cvStartLine = loc.lineNumber(); + } else if (parser.ctx.compilerOptions != null + && parser.ctx.compilerOptions.fileName != null) { + placeholder.cvStartFile = parser.ctx.compilerOptions.fileName; + } + // Optimization - https://github.com/fglock/PerlOnJava/issues/8 // Prepare capture variables Map outerVars = parser.ctx.symbolTable.getAllVisibleVariables(); @@ -1453,6 +1465,8 @@ public static ListNode handleNamedSubWithFilter(Parser parser, String subName, S field.set(placeholder.codeObject, codeRef); installClosureCaptureMetadata(placeholder, paramList); + placeholder.cvStartFile = compiledCode.cvStartFile; + placeholder.cvStartLine = compiledCode.cvStartLine; } else if (runtimeCode instanceof InterpretedCode interpretedCode) { // InterpretedCode path - update placeholder in-place (not replace codeRef.value) @@ -1486,6 +1500,8 @@ public static ListNode handleNamedSubWithFilter(Parser parser, String subName, S placeholder.subroutine = interpretedCode; placeholder.codeObject = interpretedCode; installClosureCaptureMetadata(placeholder, paramList); + placeholder.cvStartFile = interpretedCode.cvStartFile; + placeholder.cvStartLine = interpretedCode.cvStartLine; } } catch (VerifyError ve) { // VerifyError extends Error (not Exception), so it's not caught by catch(Exception). @@ -1519,6 +1535,8 @@ public static ListNode handleNamedSubWithFilter(Parser parser, String subName, S placeholder.subroutine = interpretedCode; placeholder.codeObject = interpretedCode; installClosureCaptureMetadata(placeholder, paramList); + placeholder.cvStartFile = interpretedCode.cvStartFile; + placeholder.cvStartLine = interpretedCode.cvStartLine; } catch (Exception e) { // Handle any exceptions during subroutine creation throw new PerlCompilerException("Subroutine error: " + e.getMessage()); diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/Internals.java b/src/main/java/org/perlonjava/runtime/perlmodule/Internals.java index 7aa771bdc..d6aa79810 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/Internals.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/Internals.java @@ -59,6 +59,7 @@ public static void initialize() { // to approximate the real-Perl GVf_IMPORTED_CV bit so callers // such as Pod::Coverage can skip imported helpers. internals.registerMethod("jperl_is_imported_sub", "jperl_is_imported_sub", "$"); + internals.registerMethod("jperl_cv_start_location", "jperlCvStartLocation", "$"); } catch (NoSuchMethodException e) { System.err.println("Warning: Missing Internals method: " + e.getMessage()); } @@ -613,4 +614,35 @@ public static RuntimeList jperl_is_imported_sub(RuntimeArray args, int ctx) { } return new RuntimeScalar().getList(); } + + /** + * Returns (filename, line) for the start of a coderef body — what Perl's + * {@code B::svref_2object($cv)->START->line} reports. Used by the bundled + * {@code B} stub (e.g. Fennec::Lite's {@code FENNEC_ITEM} filtering). + */ + public static RuntimeList jperlCvStartLocation(RuntimeArray args, int ctx) { + String defFile = "-e"; + if (args.size() == 0) { + return new RuntimeList(new RuntimeScalar(defFile), new RuntimeScalar(0)); + } + RuntimeScalar s = args.get(0); + if (s == null) { + return new RuntimeList(new RuntimeScalar(defFile), new RuntimeScalar(0)); + } + s = s.scalar(); + if (s.type != RuntimeScalarType.CODE || !(s.value instanceof RuntimeCode code)) { + return new RuntimeList(new RuntimeScalar(defFile), new RuntimeScalar(0)); + } + String file = code.cvStartFile; + int line = code.cvStartLine; + if ((line <= 0 || file == null || file.isEmpty()) + && code instanceof org.perlonjava.backend.bytecode.InterpretedCode ic) { + file = ic.sourceName != null ? ic.sourceName : defFile; + line = ic.sourceLine; + } + if (file == null || file.isEmpty()) { + file = defFile; + } + return new RuntimeList(new RuntimeScalar(file), new RuntimeScalar(line)); + } } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java index e6c5019a5..b11f36bd1 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java @@ -420,6 +420,13 @@ public static void requireLvalueCallable(RuntimeCode code, int callContext, Stri // behaviour, where the CV's CvGV points to a free-floating GV with the name. public boolean explicitlyRenamed = false; + /** + * Source location of the start of this CV's body (Perl {@code B::CV->START->line} / + * COP). Used by the stub {@code B} module; {@code 0} means unknown. + */ + public String cvStartFile; + public int cvStartLine; + /** * When a coderef is installed with {@code *Package::name = $cr}, records the * stash slot FQN for introspection ({@code Sub::Util::subname}, {@code B::CV}) @@ -2018,6 +2025,19 @@ public static RuntimeScalar makeCodeObject(Object codeObject, String prototype) * @throws Exception if an error occurs during method retrieval */ public static RuntimeScalar makeCodeObject(Object codeObject, String prototype, String packageName) throws Exception { + return makeCodeObject(codeObject, prototype, packageName, null, 0); + } + + /** + * Like {@link #makeCodeObject(Object, String, String)} plus COP source + * location for {@code B::CV->START} (e.g. Fennec::Lite line filtering). + */ + public static RuntimeScalar makeCodeObject( + Object codeObject, + String prototype, + String packageName, + String cvStartFile, + int cvStartLine) throws Exception { // Retrieve the class of the provided code object Class clazz = codeObject.getClass(); @@ -2031,6 +2051,12 @@ public static RuntimeScalar makeCodeObject(Object codeObject, String prototype, if (packageName != null) { code.packageName = packageName; } + if (cvStartFile != null && !cvStartFile.isEmpty()) { + code.cvStartFile = cvStartFile; + } + if (cvStartLine > 0) { + code.cvStartLine = cvStartLine; + } // Look up pad constants registered at compile time for this class. // These track cached string literals referenced via \ inside the sub, diff --git a/src/main/perl/lib/B.pm b/src/main/perl/lib/B.pm index 6cdccc04f..ef06e9a4b 100644 --- a/src/main/perl/lib/B.pm +++ b/src/main/perl/lib/B.pm @@ -218,8 +218,23 @@ package B::CV { sub START { # Return a B::COP (control op) so optree walkers find file/line info. - # Real Perl returns the first op of the sub body; for PerlOnJava we - # return a COP with the best location info we have. + # Real Perl returns the first op of the sub body; for PerlOnJava use + # Internals::jperl_cv_start_location when available. + my $self = shift; + my $ref = $self->{ref}; + if ($ref && ref($ref) eq 'CODE') { + local $@; + eval { require Internals; 1 } or return B::COP->new("-e", 0); + my @loc = Internals::jperl_cv_start_location($ref); + if (@loc >= 2) { + my ($file, $line) = @loc; + $file ||= "-e"; + $line ||= 0; + if ($line > 0) { + return B::COP->new($file, $line); + } + } + } return B::COP->new("-e", 0); }