diff --git a/docs/reference/cli-options.md b/docs/reference/cli-options.md index aff5d7253..807bd78d1 100644 --- a/docs/reference/cli-options.md +++ b/docs/reference/cli-options.md @@ -158,7 +158,13 @@ jperl [options] [program | -e 'command'] [arguments] ## Debugging Options -- **`--debug`** - Show debug information +- **`-d`** - Run script under the interactive Perl debugger + ```bash + ./jperl -d script.pl + ``` + See [Debugger Reference](debugger.md) for commands and usage. + +- **`--debug`** - Show debug information (compiler internals) ```bash ./jperl --debug -E 'say "test"' ``` @@ -233,7 +239,8 @@ The following standard Perl options are not yet implemented: - **`-t`** - Taint checks with warnings - **`-u`** - Dumps core after compiling - **`-U`** - Allows unsafe operations -- **`-d[t][:debugger]`** - Run under debugger +- **`-dt`** - Run under threaded debugger +- **`-d:debugger`** - Run under custom debugger module - **`-D[number/list]`** - Set debugging flags - **`-C [number/list]`** - Control Unicode features diff --git a/docs/reference/debugger.md b/docs/reference/debugger.md new file mode 100644 index 000000000..f610c520a --- /dev/null +++ b/docs/reference/debugger.md @@ -0,0 +1,151 @@ +# Perl Debugger Reference + +The PerlOnJava debugger provides an interactive debugging environment similar to Perl's built-in debugger. + +## Starting the Debugger + +```bash +./jperl -d script.pl [arguments] +``` + +The debugger stops at the first executable statement and displays: + +``` +main::(script.pl:5): +5: my $x = 10; + DB<1> +``` + +## Debugger Commands + +### Execution Control + +| Command | Description | +|---------|-------------| +| `s` | **Step into** - Execute one statement, stepping into subroutine calls | +| `n` | **Next** - Execute one statement, stepping over subroutine calls | +| `r` | **Return** - Execute until current subroutine returns | +| `c [line]` | **Continue** - Run until breakpoint, end, or specified line (one-time) | +| `q` | **Quit** - Exit the debugger and program | + +Press **Enter** to repeat the last command (default: `n`). + +### Breakpoints + +| Command | Description | +|---------|-------------| +| `b [line]` | Set breakpoint at current or specified line | +| `b file:line` | Set breakpoint at line in specified file | +| `B [line]` | Delete breakpoint at current or specified line | +| `B *` | Delete all breakpoints | +| `L` | List all breakpoints | + +### Source Display + +| Command | Description | +|---------|-------------| +| `l [range]` | List source code (e.g., `l 10-20`, `l 15`) | +| `.` | Show current line | +| `T` | Show stack trace (call stack) | + +### Expression Evaluation + +| Command | Description | +|---------|-------------| +| `p expr` | Print expression result | +| `x expr` | Dump expression with Data::Dumper formatting | + +### Help + +| Command | Description | +|---------|-------------| +| `h` or `?` | Show help | + +## Debug Variables + +The debugger provides access to standard Perl debug variables: + +| Variable | Description | +|----------|-------------| +| `$DB::single` | Single-step mode (1 = enabled) | +| `$DB::trace` | Trace mode (1 = enabled) | +| `$DB::signal` | Signal flag | +| `$DB::filename` | Current filename | +| `$DB::line` | Current line number | +| `%DB::sub` | Subroutine locations (`subname => "file:start-end"`) | +| `@DB::args` | Arguments of current subroutine | + +## Examples + +### Basic Stepping + +``` +$ ./jperl -d script.pl +main::(script.pl:5): +5: my $x = 10; + DB<1> n +main::(script.pl:6): +6: my $y = foo($x); + DB<2> s +main::(script.pl:2): +2: my ($arg) = @_; + DB<3> +``` + +### Setting Breakpoints + +``` + DB<1> b 15 +Breakpoint set at script.pl:15 + DB<1> b other.pl:20 +Breakpoint set at other.pl:20 + DB<1> L +Breakpoints: + script.pl:15 + other.pl:20 + DB<1> c +``` + +### Evaluating Expressions + +``` + DB<1> p $x + $y +42 + DB<1> p "@DB::args" +10 20 hello + DB<1> x \@array +$VAR1 = [ + 1, + 2, + 3 + ]; +``` + +### Inspecting Subroutine Locations + +``` + DB<1> p $DB::sub{"main::foo"} +script.pl:10-15 + DB<1> x \%DB::sub +$VAR1 = { + 'main::foo' => 'script.pl:10-15', + 'main::bar' => 'script.pl:20-25' + }; +``` + +## Limitations + +The following Perl debugger features are not yet implemented: + +- Watchpoints (`w` command) +- Actions (`a` command) +- Conditional breakpoints (`b line condition`) +- Custom debugger modules (`-d:Module`) +- Restart (`R` command) +- History and command editing +- Lexical variable inspection (expressions evaluate in package scope) + +## See Also + +- [CLI Options](cli-options.md) - All command-line options +- [perldebug](https://perldoc.perl.org/perldebug) - Perl's debugger documentation diff --git a/docs/reference/feature-matrix.md b/docs/reference/feature-matrix.md index 76c64469b..02fde7de9 100644 --- a/docs/reference/feature-matrix.md +++ b/docs/reference/feature-matrix.md @@ -68,16 +68,68 @@ PerlOnJava implements most core Perl features with some key differences: - ✅ **Perl-like runtime error messages**: Runtime errors are formatted similarly to Perl's. - ✅ **Comments**: Support for comments and POD (documentation) in code is implemented. - ✅ **Environment**: Support for `PERL5LIB`, `PERL5OPT` environment variables. -- 🚧 **Perl debugger**: The built-in Perl debugger (`perl -d`) is work in progress. - - ✅ Basic commands: `n` (next), `s` (step), `c` (continue), `q` (quit), `l` (list), `b` (breakpoint), `B` (delete breakpoint), `L` (list breakpoints), `h` (help) - - ✅ Debug variables: `$DB::single`, `$DB::trace`, `$DB::signal`, `$DB::filename`, `$DB::line` - - ❌ `-d:MOD` for Devel modules (e.g., `-d:NYTProf`) - - ❌ `perl5db.pl` compatibility - - ❌ Expression evaluation commands (`p`, `x`) - 🚧 **Perl-like warnings**: Warnings is work in progress. Some warnings need to be formatted to resemble Perl's output. +--- + +## Perl Debugger + +The built-in Perl debugger (`perl -d`) provides interactive debugging. See [Debugger Reference](debugger.md) for full documentation. + +### Execution Commands +| Command | Status | Description | +|---------|--------|-------------| +| `s` | ✅ | Step into - execute one statement, entering subroutines | +| `n` | ✅ | Next - execute one statement, stepping over subroutines | +| `r` | ✅ | Return - execute until current subroutine returns | +| `c [line]` | ✅ | Continue - run until breakpoint or specified line | +| `q` | ✅ | Quit - exit the debugger | + +### Breakpoints +| Command | Status | Description | +|---------|--------|-------------| +| `b [line]` | ✅ | Set breakpoint at line | +| `b file:line` | ✅ | Set breakpoint at line in file | +| `B [line]` | ✅ | Delete breakpoint | +| `B *` | ✅ | Delete all breakpoints | +| `L` | ✅ | List all breakpoints | +| `b line condition` | ❌ | Conditional breakpoints | + +### Source and Stack +| Command | Status | Description | +|---------|--------|-------------| +| `l [range]` | ✅ | List source code | +| `.` | ✅ | Show current line | +| `T` | ✅ | Stack trace | +| `w expr` | ❌ | Watch expression | +| `a line command` | ❌ | Set action at line | + +### Expression Evaluation +| Command | Status | Description | +|---------|--------|-------------| +| `p expr` | ✅ | Print expression result | +| `x expr` | ✅ | Dump expression with Data::Dumper | + +### Debug Variables +| Variable | Status | Description | +|----------|--------|-------------| +| `$DB::single` | ✅ | Single-step mode flag | +| `$DB::trace` | ✅ | Trace mode flag | +| `$DB::signal` | ✅ | Signal flag | +| `$DB::filename` | ✅ | Current filename | +| `$DB::line` | ✅ | Current line number | +| `%DB::sub` | ✅ | Subroutine locations (name → file:start-end) | +| `@DB::args` | ✅ | Current subroutine arguments | + +### Not Implemented +- ❌ `-d:Module` - Custom debugger modules (e.g., `-d:NYTProf`) +- ❌ `perl5db.pl` compatibility +- ❌ `R` - Restart program +- ❌ History and command editing + +--- -### Command line switches +## Command Line Switches - ✅ Accept input program in several ways: 1. **Piped input**: `echo 'print "Hello\n"' | ./jperl` - reads from pipe and executes immediately diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index a3d65a1eb..67e5d5c3f 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -814,7 +814,9 @@ public void visit(BlockNode node) { if (DebugState.debugMode && stmtTokenIndex >= 0) { boolean skipDebug = (stmt instanceof AbstractNode an && an.getBooleanAnnotation("skipDebug")); if (!skipDebug) { - int lineNumber = errorUtil.getLineNumber(stmtTokenIndex); + // Use getLineNumberAccurate() because subroutine bodies may be compiled + // lazily after the main script, making cached line numbers unreliable + int lineNumber = errorUtil.getLineNumberAccurate(stmtTokenIndex); int fileIdx = addToStringPool(sourceName); emit(Opcodes.DEBUG); emit(fileIdx); @@ -4159,6 +4161,13 @@ private void visitNamedSubroutine(SubroutineNode node) { emit(nameIdx); emitReg(codeReg); + // Step 7: Register subroutine location for %DB::sub (only in debug mode) + if (DebugState.debugMode && errorUtil != null) { + int startLine = errorUtil.getLineNumber(node.getIndex()); + // Use start line as end line for now (accurate end would require tracking block end) + DebugState.registerSubroutine(fullName, sourceName, startLine, startLine); + } + lastResultReg = -1; } diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index 7fc65a91a..9e94cde46 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -1541,7 +1541,7 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c int fileIdx = bytecode[pc++]; int line = bytecode[pc++]; String file = code.stringPool[fileIdx]; - DebugHooks.debug(file, line); + DebugHooks.debug(file, line, code, registers); } default -> { diff --git a/src/main/java/org/perlonjava/backend/bytecode/Disassemble.java b/src/main/java/org/perlonjava/backend/bytecode/Disassemble.java index 59919dc89..e36446bf8 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/Disassemble.java +++ b/src/main/java/org/perlonjava/backend/bytecode/Disassemble.java @@ -2080,6 +2080,14 @@ public static String disassemble(InterpretedCode interpretedCode) { break; } + case Opcodes.DEBUG: { + int fileIdx = interpretedCode.bytecode[pc++]; + int line = interpretedCode.bytecode[pc++]; + sb.append("DEBUG file=\"").append(interpretedCode.stringPool[fileIdx]) + .append("\" line=").append(line).append("\n"); + break; + } + default: sb.append("UNKNOWN(").append(opcode).append(")\n"); break; diff --git a/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java b/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java index 76af62b17..17281e4cb 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java @@ -338,7 +338,8 @@ public static RuntimeScalar evalString(String perlCode, // first evalString overload above: it corrupts die/warn location baking. BytecodeCompiler compiler = new BytecodeCompiler( sourceName + " (eval)", - sourceLine + sourceLine, + errorUtil ); InterpretedCode evalCode = compiler.compile(ast, ctx); // Pass ctx for context propagation if (RuntimeCode.DISASSEMBLE) { diff --git a/src/main/java/org/perlonjava/frontend/parser/SubroutineParser.java b/src/main/java/org/perlonjava/frontend/parser/SubroutineParser.java index 04425c2d7..0620a4a2b 100644 --- a/src/main/java/org/perlonjava/frontend/parser/SubroutineParser.java +++ b/src/main/java/org/perlonjava/frontend/parser/SubroutineParser.java @@ -10,6 +10,7 @@ import org.perlonjava.frontend.lexer.LexerTokenType; import org.perlonjava.frontend.semantic.ScopedSymbolTable; import org.perlonjava.frontend.semantic.SymbolTable; +import org.perlonjava.runtime.debugger.DebugState; import org.perlonjava.runtime.mro.InheritanceResolver; import org.perlonjava.runtime.runtimetypes.*; @@ -659,6 +660,14 @@ public static ListNode handleNamedSubWithFilter(Parser parser, String subName, S codeRef.value = new RuntimeCode(subName, attributes); } + // Register subroutine location for %DB::sub (only in debug mode) + if (DebugState.debugMode && parser.ctx.errorUtil != null && block != null) { + int startLine = parser.ctx.errorUtil.getLineNumber(block.tokenIndex); + // Use current position as end for now (could track block end for accuracy) + int endLine = parser.ctx.errorUtil.getLineNumber(parser.tokenIndex); + DebugState.registerSubroutine(fullName, parser.ctx.compilerOptions.fileName, startLine, endLine); + } + // Initialize placeholder metadata (accessed via codeRef.value) RuntimeCode placeholder = (RuntimeCode) codeRef.value; placeholder.prototype = prototype; diff --git a/src/main/java/org/perlonjava/runtime/debugger/DebugHooks.java b/src/main/java/org/perlonjava/runtime/debugger/DebugHooks.java index bf61d330b..0d2e1c23e 100644 --- a/src/main/java/org/perlonjava/runtime/debugger/DebugHooks.java +++ b/src/main/java/org/perlonjava/runtime/debugger/DebugHooks.java @@ -1,7 +1,14 @@ package org.perlonjava.runtime.debugger; +import org.perlonjava.backend.bytecode.EvalStringHandler; +import org.perlonjava.backend.bytecode.InterpretedCode; import org.perlonjava.backend.bytecode.InterpreterState; import org.perlonjava.runtime.runtimetypes.GlobalVariable; +import org.perlonjava.runtime.runtimetypes.RuntimeArray; +import org.perlonjava.runtime.runtimetypes.RuntimeBase; +import org.perlonjava.runtime.runtimetypes.RuntimeContextType; +import org.perlonjava.runtime.runtimetypes.RuntimeHash; +import org.perlonjava.runtime.runtimetypes.RuntimeList; import org.perlonjava.runtime.runtimetypes.RuntimeScalar; import java.io.BufferedReader; @@ -25,18 +32,31 @@ public class DebugHooks { // Command counter for prompt (1-indexed like Perl) private static int commandCounter = 1; + + // Current execution context for expression evaluation + private static InterpretedCode currentCode; + private static RuntimeBase[] currentRegisters; /** * Main debug hook called by DEBUG opcode. * Checks if we should stop and handles debugger interaction. * - * @param filename Current source filename - * @param line Current line number (1-based) + * @param filename Current source filename + * @param line Current line number (1-based) + * @param code Current InterpretedCode (for expression evaluation) + * @param registers Current register array (for variable access) */ - public static void debug(String filename, int line) { + public static void debug(String filename, int line, InterpretedCode code, RuntimeBase[] registers) { + // Store context for expression evaluation + currentCode = code; + currentRegisters = registers; + // Sync from Perl $DB::single variable to DebugState syncFromPerlVariables(); + // Sync %DB::sub with any newly compiled subroutines + syncDbSub(); + // Update current location DebugState.currentFile = filename; DebugState.currentLine = line; @@ -55,6 +75,15 @@ public static void debug(String filename, int line) { System.exit(0); } + // Populate @DB::args with current frame's arguments + RuntimeArray dbArgs = GlobalVariable.getGlobalArray("DB::args"); + RuntimeArray frameArgs = DebugState.getArgsForFrame(0); + if (frameArgs != null) { + dbArgs.setFromList(frameArgs.getList()); + } else { + dbArgs.setFromList(new RuntimeList()); + } + // Get source line for display String sourceLine = DebugState.getSourceLine(filename, line); @@ -126,6 +155,9 @@ private static boolean executeCommand(String cmd) { case 's': // step - step into return handleStep(args); + case 'r': // return - step out + return handleReturn(args); + case 'c': // continue return handleContinue(args); @@ -140,6 +172,10 @@ private static boolean executeCommand(String cmd) { handleShowCurrent(); return false; + case 'T': // stack trace + handleStackTrace(); + return false; + case 'h': // help case '?': handleHelp(); @@ -157,6 +193,14 @@ private static boolean executeCommand(String cmd) { handleListBreakpoints(); return false; + case 'p': // print expression + handlePrint(args); + return false; + + case 'x': // dump expression + handleDump(args); + return false; + default: // Unknown command - could be Perl expression to evaluate // For now, just show help @@ -173,6 +217,7 @@ private static boolean handleNext(String args) { // Set step-over depth to current depth // DEBUG hook will skip while callDepth > stepOverDepth DebugState.stepOverDepth = DebugState.callDepth; + DebugState.stepOutDepth = -1; DebugState.single = true; syncToPerlVariables(); return true; @@ -182,7 +227,21 @@ private static boolean handleNext(String args) { * Handle 's' (step) command - step into subroutine calls. */ private static boolean handleStep(String args) { - // Disable step-over, enable single-step + // Disable step-over/step-out, enable single-step + DebugState.stepOverDepth = -1; + DebugState.stepOutDepth = -1; + DebugState.single = true; + syncToPerlVariables(); + return true; + } + + /** + * Handle 'r' (return) command - step out of current subroutine. + */ + private static boolean handleReturn(String args) { + // Set step-out depth to current depth + // DEBUG hook will skip until callDepth < stepOutDepth + DebugState.stepOutDepth = DebugState.callDepth; DebugState.stepOverDepth = -1; DebugState.single = true; syncToPerlVariables(); @@ -193,17 +252,18 @@ private static boolean handleStep(String args) { * Handle 'c' (continue) command - run until breakpoint or end. */ private static boolean handleContinue(String args) { - // Disable single-step and step-over + // Disable single-step, step-over, step-out DebugState.single = false; DebugState.stepOverDepth = -1; + DebugState.stepOutDepth = -1; // If argument provided, it's a line number for one-time breakpoint if (!args.isEmpty()) { try { int targetLine = Integer.parseInt(args); String key = DebugState.currentFile + ":" + targetLine; - DebugState.breakpoints.add(key); - // TODO: Mark as one-time breakpoint to remove after hit + // Use one-time breakpoint so it's removed after being hit + DebugState.oneTimeBreakpoints.add(key); } catch (NumberFormatException e) { System.out.println("Invalid line number: " + args); return false; @@ -376,6 +436,36 @@ private static void handleListBreakpoints() { } } + /** + * Handle 'T' (stack trace) command - show call stack. + */ + private static void handleStackTrace() { + Throwable t = new Throwable(); + java.util.ArrayList> stackTrace = + org.perlonjava.runtime.runtimetypes.ExceptionFormatter.formatException(t); + + if (stackTrace.isEmpty()) { + System.out.println("(no stack trace available)"); + return; + } + + // Skip the first frame (handleStackTrace itself) + for (int i = 1; i < stackTrace.size(); i++) { + java.util.ArrayList frame = stackTrace.get(i); + String pkg = frame.get(0); + String file = frame.get(1); + String line = frame.get(2); + String sub = (frame.size() > 3 && frame.get(3) != null) ? frame.get(3) : "(main)"; + + // Format: . = pkg::sub() called from file line N + if (i == 1) { + System.out.printf(". = %s::%s() called from %s line %s%n", pkg, sub, file, line); + } else { + System.out.printf("@ = %s::%s() called from %s line %s%n", pkg, sub, file, line); + } + } + } + /** * Handle 'h' (help) command. */ @@ -383,18 +473,113 @@ private static void handleHelp() { System.out.println("Debugger commands:"); System.out.println(" n Next (step over) - execute until next statement"); System.out.println(" s Step into - step into subroutine calls"); - System.out.println(" c [line] Continue - run until breakpoint or end"); + System.out.println(" r Return - step out of current subroutine"); + System.out.println(" c [line] Continue - run until breakpoint or line"); System.out.println(" q Quit - exit the debugger"); + System.out.println(" T Stack trace - show call stack"); System.out.println(" l [range] List source (e.g., 'l 10-20' or 'l 15')"); System.out.println(" . Show current line"); System.out.println(" b [line] Set breakpoint (e.g., 'b 10' or 'b file.pl:10')"); System.out.println(" B [line] Delete breakpoint ('B *' deletes all)"); System.out.println(" L List all breakpoints"); + System.out.println(" p expr Print expression result"); + System.out.println(" x expr Dump expression (structured output)"); System.out.println(" h or ? Show this help"); System.out.println(""); System.out.println("Press Enter to repeat last command (default: n)"); } + /** + * Handle 'p' (print) command - evaluate and print expression. + */ + private static void handlePrint(String expr) { + if (expr.isEmpty()) { + System.out.println("Usage: p "); + return; + } + + // Temporarily disable debug mode during expression evaluation + boolean savedDebugMode = DebugState.debugMode; + DebugState.debugMode = false; + + try { + // Evaluate the expression using eval in scalar context + RuntimeScalar result = EvalStringHandler.evalString( + expr, + currentCode, + currentRegisters, + DebugState.currentFile, + DebugState.currentLine, + RuntimeContextType.SCALAR + ); + + // Check if eval had an error + RuntimeScalar evalError = GlobalVariable.getGlobalVariable("main::@"); + if (evalError.getDefinedBoolean() && !evalError.toString().isEmpty()) { + System.out.println("Error: " + evalError.toString().trim()); + } else { + // Print the result (like Perl's print) + System.out.println(result.toString()); + } + } catch (Exception e) { + System.out.println("Error evaluating expression: " + e.getMessage()); + } finally { + // Restore debug mode + DebugState.debugMode = savedDebugMode; + } + } + + /** + * Handle 'x' (dump) command - evaluate and dump expression structure. + */ + private static void handleDump(String expr) { + if (expr.isEmpty()) { + System.out.println("Usage: x "); + return; + } + + // Temporarily disable debug mode during expression evaluation + boolean savedDebugMode = DebugState.debugMode; + DebugState.debugMode = false; + + try { + // Wrap expression to use Data::Dumper-style output + // For now, use a simple approach: evaluate and show type info + String dumpExpr = "do { use Data::Dumper; local $Data::Dumper::Terse = 1; local $Data::Dumper::Indent = 1; Dumper(" + expr + ") }"; + + RuntimeScalar result = EvalStringHandler.evalString( + dumpExpr, + currentCode, + currentRegisters, + DebugState.currentFile, + DebugState.currentLine, + RuntimeContextType.SCALAR + ); + + // Check if eval had an error + RuntimeScalar evalError = GlobalVariable.getGlobalVariable("main::@"); + if (evalError.getDefinedBoolean() && !evalError.toString().isEmpty()) { + // Data::Dumper not available, fall back to simple output + result = EvalStringHandler.evalString( + expr, + currentCode, + currentRegisters, + DebugState.currentFile, + DebugState.currentLine, + RuntimeContextType.SCALAR + ); + System.out.println("0 " + result.toString()); + } else { + System.out.print(result.toString()); + } + } catch (Exception e) { + System.out.println("Error evaluating expression: " + e.getMessage()); + } finally { + // Restore debug mode + DebugState.debugMode = savedDebugMode; + } + } + /** * Called when entering a subroutine (for step-over tracking). */ @@ -448,4 +633,18 @@ public static void initializeDebugVariables() { GlobalVariable.getGlobalVariable("DB::filename").set(""); GlobalVariable.getGlobalVariable("DB::line").set(0); } + + /** + * Sync %DB::sub from DebugState.subLocations. + * Called periodically to ensure Perl code can access subroutine locations. + */ + public static void syncDbSub() { + if (!DebugState.debugMode) { + return; + } + RuntimeHash dbSub = GlobalVariable.getGlobalHash("DB::sub"); + for (var entry : DebugState.subLocations.entrySet()) { + dbSub.put(entry.getKey(), new RuntimeScalar(entry.getValue())); + } + } } diff --git a/src/main/java/org/perlonjava/runtime/debugger/DebugState.java b/src/main/java/org/perlonjava/runtime/debugger/DebugState.java index ed14b8d37..ac96a96c4 100644 --- a/src/main/java/org/perlonjava/runtime/debugger/DebugState.java +++ b/src/main/java/org/perlonjava/runtime/debugger/DebugState.java @@ -1,5 +1,9 @@ package org.perlonjava.runtime.debugger; +import org.perlonjava.runtime.runtimetypes.RuntimeArray; + +import java.util.ArrayDeque; +import java.util.Deque; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -79,16 +83,29 @@ public class DebugState { /** * Step-over depth tracking. - * When > 0, skip DEBUG calls until call depth returns to this level. + * When >= 0, skip DEBUG calls until call depth returns to this level. * Used to implement "n" (next) command - step over subroutine calls. */ public static volatile int stepOverDepth = -1; /** - * Current call depth for step-over tracking. + * Step-out depth tracking. + * When >= 0, skip DEBUG calls until call depth is less than this level. + * Used to implement "r" (return) command - step out of current subroutine. + */ + public static volatile int stepOutDepth = -1; + + /** + * Current call depth for step-over/step-out tracking. */ public static volatile int callDepth = 0; + /** + * One-time breakpoints: "file:line" strings that should be removed after being hit. + * Used by "c line" command to continue to a specific line once. + */ + public static final Set oneTimeBreakpoints = ConcurrentHashMap.newKeySet(); + /** * Flag to indicate debugger should quit. */ @@ -105,9 +122,11 @@ public static void reset() { currentLine = 0; breakpoints.clear(); breakpointConditions.clear(); + oneTimeBreakpoints.clear(); sourceLines.clear(); breakableLines.clear(); stepOverDepth = -1; + stepOutDepth = -1; callDepth = 0; quit = false; } @@ -121,9 +140,17 @@ public static void reset() { * @return true if debugger should stop here */ public static boolean shouldStop(String file, int line) { + String key = file + ":" + line; + + // Check for one-time breakpoint first (and remove if hit) + if (oneTimeBreakpoints.remove(key)) { + // Also remove from regular breakpoints if it was added there + breakpoints.remove(key); + return true; + } + // Fast path: nothing active if (!single && !trace && !signal) { - String key = file + ":" + line; return breakpoints.contains(key); } @@ -132,6 +159,11 @@ public static boolean shouldStop(String file, int line) { return false; } + // Step-out mode: skip until we're shallower than target depth + if (stepOutDepth >= 0 && callDepth >= stepOutDepth) { + return false; + } + return true; } @@ -159,4 +191,78 @@ public static String getSourceLine(String filename, int line) { } return ""; } + + /** + * Subroutine location registry for %DB::sub. + * Maps "package::subname" -> "filename:startline-endline" + */ + public static final Map subLocations = new ConcurrentHashMap<>(); + + /** + * Register a subroutine's location for %DB::sub. + * Only registers if debugMode is enabled. + * + * @param fullName Fully qualified subroutine name (package::subname) + * @param filename Source filename + * @param startLine Starting line number (1-based) + * @param endLine Ending line number (1-based) + */ + public static void registerSubroutine(String fullName, String filename, int startLine, int endLine) { + if (!debugMode) { + return; + } + String location = filename + ":" + startLine + "-" + endLine; + subLocations.put(fullName, location); + } + + /** + * Thread-local stack of subroutine arguments for @DB::args support. + * Each frame stores a copy of the @_ array when the subroutine was called. + */ + public static final ThreadLocal> argsStack = + ThreadLocal.withInitial(ArrayDeque::new); + + /** + * Push subroutine arguments onto the stack (called when entering a sub in debug mode). + * + * @param args The @_ array for this call frame + */ + public static void pushArgs(RuntimeArray args) { + if (!debugMode) { + return; + } + // Make a shallow copy of the args array + RuntimeArray copy = new RuntimeArray(); + copy.setFromList(args.getList()); + argsStack.get().push(copy); + } + + /** + * Pop subroutine arguments from the stack (called when exiting a sub in debug mode). + */ + public static void popArgs() { + if (!debugMode) { + return; + } + Deque stack = argsStack.get(); + if (!stack.isEmpty()) { + stack.pop(); + } + } + + /** + * Get arguments for a specific frame (0 = current, 1 = caller, etc). + * + * @param frame Frame number (0-based) + * @return The args array for that frame, or null if not available + */ + public static RuntimeArray getArgsForFrame(int frame) { + Deque stack = argsStack.get(); + if (frame < 0 || frame >= stack.size()) { + return null; + } + // Convert to array for indexed access + RuntimeArray[] stackArray = stack.toArray(new RuntimeArray[0]); + return stackArray[frame]; + } } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/ErrorMessageUtil.java b/src/main/java/org/perlonjava/runtime/runtimetypes/ErrorMessageUtil.java index 7fa1bfb63..77efd771d 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/ErrorMessageUtil.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/ErrorMessageUtil.java @@ -260,18 +260,15 @@ public int getLineNumber(int index) { /** * Get line number without relying on cache. - * Always counts from the last # line directive position. - * Safe for backwards iteration. + * Always counts from the beginning of the file. + * Safe for random access (used by debugger for lazily compiled subroutines). * * @param index the index of the token * @return the line number */ public int getLineNumberAccurate(int index) { - int startIndex = Math.max(-1, tokenIndex); - int lineNumber = lastLineNumber; - - for (int i = startIndex + 1; i <= index; i++) { - if (i < 0 || i >= tokens.size()) break; + int lineNumber = 1; + for (int i = 0; i <= index && i < tokens.size(); i++) { LexerToken tok = tokens.get(i); if (tok.type == LexerTokenType.EOF) break; if (tok.type == LexerTokenType.NEWLINE) { diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java index a58e8bc60..e721ca689 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java @@ -16,6 +16,8 @@ import org.perlonjava.frontend.semantic.ScopedSymbolTable; import org.perlonjava.frontend.semantic.SymbolTable; import org.perlonjava.runtime.mro.InheritanceResolver; +import org.perlonjava.runtime.debugger.DebugHooks; +import org.perlonjava.runtime.debugger.DebugState; import org.perlonjava.runtime.operators.ModuleOperators; import org.perlonjava.runtime.operators.WarnDie; @@ -1432,6 +1434,13 @@ public static RuntimeList caller(RuntimeList args, int ctx) { frame++; } + // Check if caller() is being called from package DB (for @DB::args support) + boolean calledFromDB = false; + if (stackTraceSize > 0) { + String callerPackage = stackTrace.getFirst().getFirst(); + calledFromDB = "DB".equals(callerPackage); + } + if (frame >= 0 && frame < stackTraceSize) { // Runtime stack trace if (ctx == RuntimeContextType.SCALAR) { @@ -1458,6 +1467,18 @@ public static RuntimeList caller(RuntimeList args, int ctx) { // If no subroutine name or empty, add undef res.add(RuntimeScalarCache.scalarUndef); } + + // Populate @DB::args when caller() is called from package DB + if (calledFromDB && DebugState.debugMode) { + RuntimeArray dbArgs = GlobalVariable.getGlobalArray("DB::args"); + RuntimeArray frameArgs = DebugState.getArgsForFrame(frame); + if (frameArgs != null) { + dbArgs.setFromList(frameArgs.getList()); + } else { + dbArgs.setFromList(new RuntimeList()); + } + } + // TODO: Add more caller() return values: // hasargs, wantarray, evaltext, is_require, hints, bitmask, hinthash } @@ -1920,13 +1941,21 @@ public RuntimeList apply(RuntimeArray a, int callContext) { throw new PerlCompilerException("Undefined subroutine called at "); } - RuntimeList result; - if (isStatic) { - result = (RuntimeList) this.methodHandle.invoke(a, callContext); - } else { - result = (RuntimeList) this.methodHandle.invoke(this.codeObject, a, callContext); + // Push args for @DB::args support and track call depth in debug mode + DebugState.pushArgs(a); + DebugHooks.enterSubroutine(); + try { + RuntimeList result; + if (isStatic) { + result = (RuntimeList) this.methodHandle.invoke(a, callContext); + } else { + result = (RuntimeList) this.methodHandle.invoke(this.codeObject, a, callContext); + } + return result; + } finally { + DebugHooks.exitSubroutine(); + DebugState.popArgs(); } - return result; } catch (InvocationTargetException e) { Throwable targetException = e.getTargetException(); if (!(targetException instanceof RuntimeException)) { @@ -1972,13 +2001,21 @@ public RuntimeList apply(String subroutineName, RuntimeArray a, int callContext) throw new PerlCompilerException("Undefined subroutine &" + (fullSubName != null ? fullSubName : "") + " called at "); } - RuntimeList result; - if (isStatic) { - result = (RuntimeList) this.methodHandle.invoke(a, callContext); - } else { - result = (RuntimeList) this.methodHandle.invoke(this.codeObject, a, callContext); + // Push args for @DB::args support and track call depth in debug mode + DebugState.pushArgs(a); + DebugHooks.enterSubroutine(); + try { + RuntimeList result; + if (isStatic) { + result = (RuntimeList) this.methodHandle.invoke(a, callContext); + } else { + result = (RuntimeList) this.methodHandle.invoke(this.codeObject, a, callContext); + } + return result; + } finally { + DebugHooks.exitSubroutine(); + DebugState.popArgs(); } - return result; } catch (InvocationTargetException e) { Throwable targetException = e.getTargetException(); if (!(targetException instanceof RuntimeException)) {