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
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:**
Expand All @@ -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

---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -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)
Expand Down
4 changes: 3 additions & 1 deletion src/main/java/org/perlonjava/backend/jvm/EmitVariable.java
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
77 changes: 77 additions & 0 deletions src/main/java/org/perlonjava/runtime/debugger/DebugHooks.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand All @@ -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;
Expand Down Expand Up @@ -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);

Expand All @@ -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], "<DB>", 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.
Expand Down
37 changes: 37 additions & 0 deletions src/main/java/org/perlonjava/runtime/operators/SystemOperator.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -215,6 +218,9 @@ private static CommandResult executeCommandDirect(List<String> 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();

Expand Down Expand Up @@ -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();
Expand All @@ -393,6 +402,9 @@ private static int execCommandDirect(List<String> 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();
Expand Down Expand Up @@ -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<String, String> pbEnv = processBuilder.environment();

// Clear the inherited environment and replace with Perl's %ENV
pbEnv.clear();

for (java.util.Map.Entry<String, RuntimeScalar> 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.
Expand Down