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 --- diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index 67e5d5c3f..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); } @@ -799,6 +800,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) 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) { 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..f7bc09b41 100644 --- a/src/main/java/org/perlonjava/runtime/debugger/DebugHooks.java +++ b/src/main/java/org/perlonjava/runtime/debugger/DebugHooks.java @@ -3,9 +3,11 @@ 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; +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 +38,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 +55,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 +98,25 @@ 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; + } + + // 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); @@ -97,6 +130,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.