From 77364f5d0e1a762588e86b44be1f5737df676f49 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Mon, 9 Mar 2026 12:08:06 +0100 Subject: [PATCH 1/5] Fix debugger-related test regressions - Add PERL5DB environment variable support - Execute PERL5DB code to define custom DB::DB - Call user DB::DB instead of interactive debugger when defined - Fix %ENV inheritance in child processes - Copy Perl %ENV hash to ProcessBuilder environment - Ensures local $ENV{...} changes propagate to backticks/system() - Silence JVM VerifyError fallback message - Make message conditional on SHOW_FALLBACK flag Test impact: - op/debug.t: Tests 1-2 now pass - run/switchd.t: Requires -d:Module syntax (not implemented) Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../backend/jvm/EmitterMethodCreator.java | 4 +- .../runtime/debugger/DebugHooks.java | 63 +++++++++++++++++++ .../runtime/operators/SystemOperator.java | 37 +++++++++++ 3 files changed, 103 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java b/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java index 574590127..1a932583d 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java @@ -1519,7 +1519,9 @@ public static RuntimeCode createRuntimeCode( throw e; } catch (VerifyError e) { if (USE_INTERPRETER_FALLBACK) { - System.err.println("Note: JVM VerifyError (" + e.getMessage().split("\n")[0] + "), using interpreter backend."); + if (SHOW_FALLBACK) { + System.err.println("Note: JVM VerifyError (" + e.getMessage().split("\n")[0] + "), using interpreter backend."); + } return compileToInterpreter(ast, ctx, useTryCatch); } throw new RuntimeException(e); diff --git a/src/main/java/org/perlonjava/runtime/debugger/DebugHooks.java b/src/main/java/org/perlonjava/runtime/debugger/DebugHooks.java index 0d2e1c23e..36548d188 100644 --- a/src/main/java/org/perlonjava/runtime/debugger/DebugHooks.java +++ b/src/main/java/org/perlonjava/runtime/debugger/DebugHooks.java @@ -6,6 +6,7 @@ import org.perlonjava.runtime.runtimetypes.GlobalVariable; import org.perlonjava.runtime.runtimetypes.RuntimeArray; import org.perlonjava.runtime.runtimetypes.RuntimeBase; +import org.perlonjava.runtime.runtimetypes.RuntimeCode; import org.perlonjava.runtime.runtimetypes.RuntimeContextType; import org.perlonjava.runtime.runtimetypes.RuntimeHash; import org.perlonjava.runtime.runtimetypes.RuntimeList; @@ -36,6 +37,12 @@ public class DebugHooks { // Current execution context for expression evaluation private static InterpretedCode currentCode; private static RuntimeBase[] currentRegisters; + + // Flag to track if PERL5DB was set (user wants custom debugger) + private static boolean hasCustomDebugger = false; + + // Flag to track if we've already tried to execute PERL5DB + private static boolean perl5dbExecuted = false; /** * Main debug hook called by DEBUG opcode. @@ -47,6 +54,12 @@ public class DebugHooks { * @param registers Current register array (for variable access) */ public static void debug(String filename, int line, InterpretedCode code, RuntimeBase[] registers) { + // Execute PERL5DB on first call (defines user's DB::DB if set) + if (!perl5dbExecuted) { + perl5dbExecuted = true; + executePERL5DB(); + } + // Store context for expression evaluation currentCode = code; currentRegisters = registers; @@ -84,6 +97,12 @@ public static void debug(String filename, int line, InterpretedCode code, Runtim dbArgs.setFromList(new RuntimeList()); } + // If user has defined custom DB::DB, call it instead of our interactive debugger + if (hasCustomDebugger) { + callUserDbDb(); + return; + } + // Get source line for display String sourceLine = DebugState.getSourceLine(filename, line); @@ -97,6 +116,50 @@ public static void debug(String filename, int line, InterpretedCode code, Runtim // Enter command loop commandLoop(); } + + /** + * Execute PERL5DB environment variable if set. + * This allows users to define custom DB::DB subroutines. + */ + private static void executePERL5DB() { + String perl5db = System.getenv("PERL5DB"); + if (perl5db == null || perl5db.isEmpty()) { + return; + } + + hasCustomDebugger = true; + + // Execute PERL5DB code in DB package context + try { + // Temporarily disable debug mode to avoid infinite recursion + boolean savedDebugMode = DebugState.debugMode; + DebugState.debugMode = false; + + // Wrap in package DB to ensure subs are defined there + String wrappedCode = "package DB; " + perl5db; + EvalStringHandler.evalString(wrappedCode, new RuntimeBase[0], "", 1); + + DebugState.debugMode = savedDebugMode; + } catch (Exception e) { + // If PERL5DB execution fails, fall back to interactive debugger + hasCustomDebugger = false; + System.err.println("Warning: Error executing PERL5DB: " + e.getMessage()); + } + } + + /** + * Call user-defined DB::DB subroutine. + */ + private static void callUserDbDb() { + try { + RuntimeScalar dbDb = GlobalVariable.getGlobalCodeRef("DB::DB"); + if (dbDb.getDefinedBoolean()) { + RuntimeCode.apply(dbDb, new RuntimeArray(), RuntimeContextType.VOID); + } + } catch (Exception e) { + // Ignore errors in user's DB::DB - Perl does this too + } + } /** * Command loop - reads and executes debugger commands. diff --git a/src/main/java/org/perlonjava/runtime/operators/SystemOperator.java b/src/main/java/org/perlonjava/runtime/operators/SystemOperator.java index f5646c550..5581e415f 100644 --- a/src/main/java/org/perlonjava/runtime/operators/SystemOperator.java +++ b/src/main/java/org/perlonjava/runtime/operators/SystemOperator.java @@ -127,6 +127,9 @@ private static CommandResult executeCommand(String command, boolean captureOutpu ProcessBuilder processBuilder = new ProcessBuilder(shellCommand); String userDir = System.getProperty("user.dir"); processBuilder.directory(new File(userDir)); + + // Copy %ENV to the subprocess environment + copyPerlEnvToProcessBuilder(processBuilder); // CORRECT PERL BEHAVIOR: Handle streams according to Perl documentation // - system(): Both stdout and stderr go to terminal @@ -215,6 +218,9 @@ private static CommandResult executeCommandDirect(List commandArgs) { ProcessBuilder processBuilder = new ProcessBuilder(commandArgs); String userDir = System.getProperty("user.dir"); processBuilder.directory(new File(userDir)); + + // Copy %ENV to the subprocess environment + copyPerlEnvToProcessBuilder(processBuilder); process = processBuilder.start(); @@ -373,6 +379,9 @@ private static int execCommand(String command) throws IOException, InterruptedEx ProcessBuilder processBuilder = new ProcessBuilder(shellCommand); String userDir = System.getProperty("user.dir"); processBuilder.directory(new File(userDir)); + + // Copy %ENV to the subprocess environment + copyPerlEnvToProcessBuilder(processBuilder); // For exec(), we want the command to take over completely processBuilder.inheritIO(); @@ -393,6 +402,9 @@ private static int execCommandDirect(List commandArgs) throws IOExceptio ProcessBuilder processBuilder = new ProcessBuilder(commandArgs); String userDir = System.getProperty("user.dir"); processBuilder.directory(new File(userDir)); + + // Copy %ENV to the subprocess environment + copyPerlEnvToProcessBuilder(processBuilder); // For exec(), we want the command to take over completely processBuilder.inheritIO(); @@ -422,6 +434,31 @@ public static RuntimeScalar fork(int ctx, RuntimeBase... args) { // Return undef to indicate failure return scalarUndef; } + + /** + * Copies the Perl %ENV hash to the ProcessBuilder environment. + * This ensures that changes to %ENV in Perl are reflected in child processes. + * + * @param processBuilder The ProcessBuilder to update + */ + private static void copyPerlEnvToProcessBuilder(ProcessBuilder processBuilder) { + try { + RuntimeHash envHash = GlobalVariable.getGlobalHash("main::ENV"); + java.util.Map pbEnv = processBuilder.environment(); + + // Clear the inherited environment and replace with Perl's %ENV + pbEnv.clear(); + + for (java.util.Map.Entry entry : envHash.elements.entrySet()) { + String value = entry.getValue().toString(); + if (value != null) { + pbEnv.put(entry.getKey(), value); + } + } + } catch (Exception e) { + // If we can't access %ENV, just use inherited environment (default behavior) + } + } /** * Helper class to hold command execution results. From 252cec743f8ba49c462d0db686bae9e38136e997 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Mon, 9 Mar 2026 13:10:20 +0100 Subject: [PATCH 2/5] Fix interpreter: process CompilerFlagNode for pragmas in eval The interpreter backend was not applying strict/warnings/features from pragmas like 'use strict' inside eval strings. This happened because CompilerFlagNode was marked with compileTimeOnly=true, and BytecodeCompiler was skipping all such nodes. Unlike the JVM emitter which visits CompilerFlagNode to set flags on the symbol table, BytecodeCompiler was completely ignoring it. This fix adds special handling to visit CompilerFlagNode before the compileTimeOnly check, allowing pragmas to properly affect the compilation of subsequent statements. Test case: eval q{ use strict; $x } - now correctly reports: "Global symbol "\$x" requires explicit package name" Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../org/perlonjava/backend/bytecode/BytecodeCompiler.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index 67e5d5c3f..290004660 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -799,6 +799,13 @@ public void visit(BlockNode node) { // Skip the 'local $_' child when For1Node handles it via LOCAL_SCALAR_SAVE_LEVEL if (i == 0 && skipFirstChild) continue; Node stmt = node.elements.get(i); + // Visit CompilerFlagNode to set strict/warning/feature flags, even if marked compileTimeOnly. + // This is needed because pragmas like 'use strict' create CompilerFlagNode with compileTimeOnly=true, + // but we still need to process it to set the flags before compiling subsequent statements. + if (stmt instanceof CompilerFlagNode cfn) { + cfn.accept(this); + continue; + } if (stmt instanceof AbstractNode an && an.getBooleanAnnotation("compileTimeOnly")) continue; // Track line number for this statement (like codegen's setDebugInfoLineNumber) From 5968130507f6f7028e4e68bdde1469d0ce8e00df Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Mon, 9 Mar 2026 13:33:55 +0100 Subject: [PATCH 3/5] Fix strict vars for Unicode identifiers above Latin-1 range MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The isNonAsciiLengthOneScalarAllowedUnderNoUtf8 function was incorrectly allowing all non-ASCII characters (c > 127) as special variables under 'no utf8'. This should only apply to Latin-1 extended characters (128-255). Unicode characters above 255 (like Greek α = 945) should be treated as normal identifiers and blocked under strict vars. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../org/perlonjava/backend/bytecode/BytecodeCompiler.java | 5 +++-- src/main/java/org/perlonjava/backend/jvm/EmitVariable.java | 4 +++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index 290004660..274301ff5 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -322,8 +322,9 @@ private boolean isNonAsciiLengthOneScalarAllowedUnderNoUtf8(String sigil, String return false; } char c = name.charAt(0); - // Allow if character > 127 (Latin-1) and 'use utf8' is NOT enabled - return c > 127 && emitterContext != null && emitterContext.symbolTable != null + // Allow if character is in Latin-1 extended range (128-255) and 'use utf8' is NOT enabled + // Unicode characters above 255 (like Greek α = 945) should NOT be exempt + return c > 127 && c <= 255 && emitterContext != null && emitterContext.symbolTable != null && !emitterContext.symbolTable.isStrictOptionEnabled(Strict.HINT_UTF8); } diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitVariable.java b/src/main/java/org/perlonjava/backend/jvm/EmitVariable.java index d19c97dd9..e58d6b937 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitVariable.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitVariable.java @@ -85,7 +85,9 @@ private static boolean isNonAsciiLengthOneScalarAllowedUnderNoUtf8(EmitterContex return false; } char c = name.charAt(0); - return c > 127 && !ctx.symbolTable.isStrictOptionEnabled(Strict.HINT_UTF8); + // Allow if character is in Latin-1 extended range (128-255) and 'use utf8' is NOT enabled + // Unicode characters above 255 (like Greek α = 945) should NOT be exempt + return c > 127 && c <= 255 && !ctx.symbolTable.isStrictOptionEnabled(Strict.HINT_UTF8); } private static boolean isBuiltinSpecialContainerVar(String sigil, String name) { From b2114d344f759e9fdbef453426ee032670f79f3b Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Mon, 9 Mar 2026 13:39:23 +0100 Subject: [PATCH 4/5] Add debugger documentation to workshop slides - Add debugger bullet point to intro slides "What You Get" section - Add full debugger slide with command reference to technical slides - Update roadmap to reflect debugger is now stable Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../slides-part1-intro.md | 1 + .../slides-part2-technical.md | 26 +++++++++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/dev/presentations/German_Perl_Raku_Workshop_2026/slides-part1-intro.md b/dev/presentations/German_Perl_Raku_Workshop_2026/slides-part1-intro.md index a0e361e33..277c6e1b9 100644 --- a/dev/presentations/German_Perl_Raku_Workshop_2026/slides-part1-intro.md +++ b/dev/presentations/German_Perl_Raku_Workshop_2026/slides-part1-intro.md @@ -59,6 +59,7 @@ The JVM runs on 3+ billion devices. Built-in tooling (JFR flight recorder, JMX m - Access any **JDBC database** — no C drivers needed - Embed Perl in Java apps via **JSR-223** scripting API - Deploy to Docker, Kubernetes — **anywhere Java runs** +- **Interactive debugger** with step, breakpoints, and stack traces (`-d`) Note: JSR-223 is the standard Java scripting API, available since Java 6. It allows bidirectional Java ↔ Perl communication. diff --git a/dev/presentations/German_Perl_Raku_Workshop_2026/slides-part2-technical.md b/dev/presentations/German_Perl_Raku_Workshop_2026/slides-part2-technical.md index 16dd0956a..d1a46680e 100644 --- a/dev/presentations/German_Perl_Raku_Workshop_2026/slides-part2-technical.md +++ b/dev/presentations/German_Perl_Raku_Workshop_2026/slides-part2-technical.md @@ -635,6 +635,28 @@ This is why the dual backend matters beyond performance. GraalVM native image gi --- +## Interactive Debugger + +**Invoke with `-d` flag:** `./jperl -d script.pl` + +| Command | Action | +|---------|--------| +| `n` | Step over (next line) | +| `s` | Step into subroutine | +| `r` | Step out (return) | +| `c` | Continue to breakpoint | +| `b 42` | Set breakpoint at line 42 | +| `l` | List source around current line | +| `T` | Stack trace | +| `p $var` | Print variable value | + +Supports `$DB::single`, `%DB::sub`, custom `DB::DB` — compatible with Perl's debugger API. + +Note: +The debugger uses the Internal VM backend (forced with -d). DEBUG opcodes are inserted at each statement. DebugHooks handles breakpoint checking, command parsing, and expression evaluation in the current lexical scope. PERL5DB environment variable is supported for custom debuggers. + +--- + ## Current Limitations **JVM-incompatible:** @@ -652,11 +674,11 @@ Workarounds: jnr-posix for native access, Java threading APIs, file auto-close a ## Roadmap -**Stable now:** JVM backend, Perl class features, IPC, sockets +**Stable now:** JVM backend, Perl class features, IPC, sockets, interactive debugger **In progress:** Internal VM optimization, eval STRING performance -**Next:** More compatible regex engine, single-step debugger +**Next:** More compatible regex engine, additional debugger features --- From dd8041479282479570d37d1c4379e656a5d0c704 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Mon, 9 Mar 2026 14:09:40 +0100 Subject: [PATCH 5/5] Debugger: die instead of hanging when stdin is not a tty Use POSIX isatty() to detect non-interactive stdin and throw an error immediately instead of blocking forever waiting for input. This prevents test hangs when -d flag is used in non-interactive contexts. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../perlonjava/runtime/debugger/DebugHooks.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/main/java/org/perlonjava/runtime/debugger/DebugHooks.java b/src/main/java/org/perlonjava/runtime/debugger/DebugHooks.java index 36548d188..f7bc09b41 100644 --- a/src/main/java/org/perlonjava/runtime/debugger/DebugHooks.java +++ b/src/main/java/org/perlonjava/runtime/debugger/DebugHooks.java @@ -3,6 +3,7 @@ import org.perlonjava.backend.bytecode.EvalStringHandler; import org.perlonjava.backend.bytecode.InterpretedCode; import org.perlonjava.backend.bytecode.InterpreterState; +import org.perlonjava.runtime.nativ.PosixLibrary; import org.perlonjava.runtime.runtimetypes.GlobalVariable; import org.perlonjava.runtime.runtimetypes.RuntimeArray; import org.perlonjava.runtime.runtimetypes.RuntimeBase; @@ -103,6 +104,19 @@ public static void debug(String filename, int line, InterpretedCode code, Runtim return; } + // Check if stdin is interactive - if not, die instead of hanging + // Use POSIX isatty() to check if file descriptor 0 (stdin) is a terminal + boolean isInteractive = true; + try { + isInteractive = PosixLibrary.INSTANCE.isatty(0) != 0; + } catch (Exception e) { + // If isatty check fails, fall back to System.console() check + isInteractive = System.console() != null; + } + if (!isInteractive) { + throw new RuntimeException("Debugger requires interactive terminal (STDIN is not a tty)"); + } + // Get source line for display String sourceLine = DebugState.getSourceLine(filename, line);