diff --git a/src/org/rascalmpl/dap/RascalDebugAdapter.java b/src/org/rascalmpl/dap/RascalDebugAdapter.java index df3c865e54f..f80d5b0a18e 100644 --- a/src/org/rascalmpl/dap/RascalDebugAdapter.java +++ b/src/org/rascalmpl/dap/RascalDebugAdapter.java @@ -156,7 +156,7 @@ public CompletableFuture initialize(InitializeRequestArguments arg capabilities.setSupportsConfigurationDoneRequest(true); capabilities.setSupportsStepBack(false); - capabilities.setSupportsRestartFrame(false); + capabilities.setSupportsRestartFrame(true); capabilities.setSupportsSetVariable(false); capabilities.setSupportsRestartRequest(false); capabilities.setSupportsCompletionsRequest(true); @@ -395,6 +395,7 @@ private StackFrame createStackFrame(int id, ISourceLocation loc, String name){ StackFrame frame = new StackFrame(); frame.setId(id); frame.setName(name); + frame.setCanRestart(false); if(loc != null && !loc.getScheme().equals(promptLocation.getScheme())) { var offsets = columns.get(loc); var line = shiftLine(loc.getBeginLine()); @@ -402,6 +403,7 @@ private StackFrame createStackFrame(int id, ISourceLocation loc, String name){ frame.setLine(line); frame.setColumn(column); frame.setSource(getSourceFromISourceLocation(loc)); + frame.setCanRestart(true); } return frame; } @@ -766,6 +768,23 @@ public CompletableFuture completions(CompletionsArguments a return response; }, ownExecutor); } - + + @Override + public CompletableFuture restartFrame(RestartFrameArguments args) { + return CompletableFuture.supplyAsync(() -> { + if(args.getFrameId() == 0){ + // From DAP spec, we must send a stopped event. Here we use this to indicate that the REPL frame cannot be restarted + StoppedEventArguments stoppedEventArguments = new StoppedEventArguments(); + stoppedEventArguments.setThreadId(RascalDebugAdapter.mainThreadID); + stoppedEventArguments.setDescription("Cannot restart the REPL frame"); + stoppedEventArguments.setReason("restart"); + client.stopped(stoppedEventArguments); + } else{ + debugHandler.processMessage(DebugMessageFactory.requestRestartFrame(args.getFrameId())); + } + return null; + + }, ownExecutor); + } } diff --git a/src/org/rascalmpl/dap/RascalDebugEventTrigger.java b/src/org/rascalmpl/dap/RascalDebugEventTrigger.java index 1f925519dff..b5a9b4f9e6d 100644 --- a/src/org/rascalmpl/dap/RascalDebugEventTrigger.java +++ b/src/org/rascalmpl/dap/RascalDebugEventTrigger.java @@ -145,6 +145,17 @@ public void fireSuspendByExceptionEvent(Exception exception) { client.stopped(stoppedEventArguments); } + @Override + public void fireSuspendByRestartFrameEndEvent() { + suspendedState.suspended(); + + StoppedEventArguments stoppedEventArguments = new StoppedEventArguments(); + stoppedEventArguments.setThreadId(RascalDebugAdapter.mainThreadID); + stoppedEventArguments.setDescription("Paused after restarting frame."); + stoppedEventArguments.setReason("restart"); + client.stopped(stoppedEventArguments); + } + @Override public void fireSuspendEvent(RascalEvent.Detail detail) {} } diff --git a/src/org/rascalmpl/debug/AbstractInterpreterEventTrigger.java b/src/org/rascalmpl/debug/AbstractInterpreterEventTrigger.java index 16bec99a930..5745b9b6614 100644 --- a/src/org/rascalmpl/debug/AbstractInterpreterEventTrigger.java +++ b/src/org/rascalmpl/debug/AbstractInterpreterEventTrigger.java @@ -127,6 +127,14 @@ public void fireSuspendByStepEndEvent() { fireSuspendEvent(RascalEvent.Detail.STEP_END); } + /** + * Fires a resume event for this debug element with detail + * RascalEvent.Detail.CLIENT_REQUEST. + */ + public void fireSuspendByRestartFrameEndEvent() { + fireSuspendEvent(RascalEvent.Detail.RESTART_FRAME_END); + } + /** * Fires a suspend event for this debug element with detail * RascalEvent.Detail.STEP_END. diff --git a/src/org/rascalmpl/debug/DebugHandler.java b/src/org/rascalmpl/debug/DebugHandler.java index 0c835b61504..8a038f6fe3d 100644 --- a/src/org/rascalmpl/debug/DebugHandler.java +++ b/src/org/rascalmpl/debug/DebugHandler.java @@ -18,6 +18,8 @@ import static org.rascalmpl.debug.AbstractInterpreterEventTrigger.newNullEventTrigger; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; import java.util.Map; import java.util.function.IntSupplier; @@ -26,6 +28,7 @@ import org.rascalmpl.interpreter.Evaluator; import org.rascalmpl.interpreter.control_exceptions.InterruptException; import org.rascalmpl.interpreter.control_exceptions.QuitException; +import org.rascalmpl.interpreter.control_exceptions.RestartFrameException; import org.rascalmpl.interpreter.env.Environment; import org.rascalmpl.interpreter.env.Pair; import org.rascalmpl.interpreter.result.Result; @@ -107,6 +110,12 @@ private enum DebugStepMode { */ private Evaluator evaluator = null; + /** + * Flag indicating a frame restart has been requested. + * Set by the debugger thread, checked and used by the evaluated thread in suspended(). + */ + private AtomicInteger restartFrameId = new AtomicInteger(-1); + /** * Create a new debug handler with its own interpreter event trigger. */ @@ -276,6 +285,12 @@ public void suspended(Object runtime, IntSupplier getCallStackSize, AbstractAST // Ignore } } + + // Check if a frame restart was requested while suspended + int frameToRestart = restartFrameId.getAndSet(-1); + if (frameToRestart >= 0) { + throw new RestartFrameException(frameToRestart); + } } private boolean handleExceptionSuspension(Evaluator eval, Exception e) { @@ -487,6 +502,19 @@ public void processMessage(IDebugMessage message) { terminateAction.run(); } break; + + case RESTART_FRAME: + if(suspended) { + int frameId = (int) message.getPayload(); + assert frameId >= 0 && frameId < evaluator.getCurrentStack().size(): "Frame id out of bounds: " + frameId; + // Set flag for the evaluated thread to handle the restart + if (restartFrameId.compareAndSet(-1, frameId)) { + // Unsuspend to let the evaluated thread continue and hit the restart exception + setSuspendRequested(true); + setSuspended(false); + } + } + break; } break; } diff --git a/src/org/rascalmpl/debug/DebugMessageFactory.java b/src/org/rascalmpl/debug/DebugMessageFactory.java index 447cea5752a..54c8209c41e 100644 --- a/src/org/rascalmpl/debug/DebugMessageFactory.java +++ b/src/org/rascalmpl/debug/DebugMessageFactory.java @@ -46,6 +46,10 @@ public static IDebugMessage requestStepOut(){ public static IDebugMessage requestTermination() { return new DebugMessage(IDebugMessage.Action.TERMINATE, IDebugMessage.Subject.INTERPRETER, IDebugMessage.Detail.UNKNOWN); } + + public static IDebugMessage requestRestartFrame(int frameId) { + return new DebugMessage(IDebugMessage.Action.RESTART_FRAME, IDebugMessage.Subject.INTERPRETER, IDebugMessage.Detail.UNKNOWN, frameId); + } /* * Breakpoint requests. diff --git a/src/org/rascalmpl/debug/IDebugMessage.java b/src/org/rascalmpl/debug/IDebugMessage.java index 74ace5ae529..a54c87206dd 100644 --- a/src/org/rascalmpl/debug/IDebugMessage.java +++ b/src/org/rascalmpl/debug/IDebugMessage.java @@ -22,7 +22,7 @@ public interface IDebugMessage { * UNKNOWN if not provided. */ enum Action { - UNKNOWN, SET, DELETE, TERMINATE, SUSPEND, RESUME + UNKNOWN, SET, DELETE, TERMINATE, SUSPEND, RESUME, RESTART_FRAME } /** diff --git a/src/org/rascalmpl/debug/RascalEvent.java b/src/org/rascalmpl/debug/RascalEvent.java index 27d4288d2ca..d673ecd7bfd 100644 --- a/src/org/rascalmpl/debug/RascalEvent.java +++ b/src/org/rascalmpl/debug/RascalEvent.java @@ -31,7 +31,7 @@ public enum Kind { * Details additional to {@link Kind}. */ public enum Detail { - UNSPECIFIED, CLIENT_REQUEST, STEP_INTO, STEP_OVER, STEP_END, BREAKPOINT, STEP_OUT, EXCEPTION + UNSPECIFIED, CLIENT_REQUEST, STEP_INTO, STEP_OVER, STEP_END, BREAKPOINT, STEP_OUT, EXCEPTION, RESTART_FRAME_END } private final Kind kind; diff --git a/src/org/rascalmpl/interpreter/Evaluator.java b/src/org/rascalmpl/interpreter/Evaluator.java index 0f70665b0d0..839a49cfd97 100755 --- a/src/org/rascalmpl/interpreter/Evaluator.java +++ b/src/org/rascalmpl/interpreter/Evaluator.java @@ -1608,12 +1608,15 @@ public void notifyAboutSuspension(AbstractAST currentAST) { @Override public void notifyAboutSuspensionException(Exception t) { currentException = t; - if (!suspendTriggerListeners.isEmpty()) { // remove the breakable condition since exception can happen anywhere - for (IRascalSuspendTriggerListener listener : suspendTriggerListeners) { - listener.suspended(this, () -> getCallStack().size(), currentAST); + try{ + if (!suspendTriggerListeners.isEmpty()) { // remove the breakable condition since exception can happen anywhere + for (IRascalSuspendTriggerListener listener : suspendTriggerListeners) { + listener.suspended(this, () -> getCallStack().size(), currentAST); + } } + } finally { // clear the exception after notifying listeners + currentException = null; } - currentException = null; } public AbstractInterpreterEventTrigger getEventTrigger() { diff --git a/src/org/rascalmpl/interpreter/control_exceptions/RestartFrameException.java b/src/org/rascalmpl/interpreter/control_exceptions/RestartFrameException.java new file mode 100644 index 00000000000..7b86aa9bd4d --- /dev/null +++ b/src/org/rascalmpl/interpreter/control_exceptions/RestartFrameException.java @@ -0,0 +1,20 @@ +package org.rascalmpl.interpreter.control_exceptions; + +/** + * Exception thrown to request a frame restart in the debugger. + * This exception carries the target frame ID and propagates up the call stack + * until it reaches the appropriate frame that should be restarted. + */ +public class RestartFrameException extends ControlException { + private final static long serialVersionUID = 1L; + private final int targetFrameId; + + public RestartFrameException(int targetFrameId) { + super("Restart frame: " + targetFrameId); + this.targetFrameId = targetFrameId; + } + + public int getTargetFrameId() { + return targetFrameId; + } +} diff --git a/src/org/rascalmpl/interpreter/result/RascalFunction.java b/src/org/rascalmpl/interpreter/result/RascalFunction.java index a9e9ca00712..2f4e3d31c93 100644 --- a/src/org/rascalmpl/interpreter/result/RascalFunction.java +++ b/src/org/rascalmpl/interpreter/result/RascalFunction.java @@ -41,11 +41,13 @@ import org.rascalmpl.ast.Type.Structured; import org.rascalmpl.exceptions.ImplementationError; import org.rascalmpl.interpreter.Accumulator; +import org.rascalmpl.interpreter.Evaluator; import org.rascalmpl.interpreter.IEvaluator; import org.rascalmpl.interpreter.IEvaluatorContext; import org.rascalmpl.interpreter.control_exceptions.Failure; import org.rascalmpl.interpreter.control_exceptions.InterruptException; import org.rascalmpl.interpreter.control_exceptions.MatchFailed; +import org.rascalmpl.interpreter.control_exceptions.RestartFrameException; import org.rascalmpl.interpreter.control_exceptions.Return; import org.rascalmpl.interpreter.env.Environment; import org.rascalmpl.interpreter.matching.IMatchingResult; @@ -241,147 +243,184 @@ public boolean isAnonymous() { return getName() == null; } + private boolean handleRestartFrame(RestartFrameException rfe) { + // Placeholder for any additional handling needed for frame restarts + if(ctx instanceof Evaluator) { + Evaluator evaluator = (Evaluator) ctx; + var stackTrace = evaluator.getCurrentStack(); + if(stackTrace.size() == 1){ // should not happen because this is the REPL frame + throw new ImplementationError("Cannot restart frame of the REPL"); + } + if (rfe.getTargetFrameId() == stackTrace.size() -1) { // the frame to restart + return true; // indicate to restart the frame + } + else { // not the frame to restart, rethrow to let upper frames handle it + throw rfe; + } + } + throw new ImplementationError("Frame restart is only supported in Evaluator contexts"); + } + @Override public Result call(Type[] actualStaticTypes, IValue[] actuals, Map keyArgValues) { Result result = getMemoizedResult(actuals, keyArgValues); if (result != null) { return result; } + boolean restartFrame = true; + + // Outer loop to handle frame restarts + while (restartFrame) { + restartFrame = false; // reset flag + Environment old = ctx.getCurrentEnvt(); + AbstractAST currentAST = ctx.getCurrentAST(); + AbstractAST oldAST = currentAST; + Stack oldAccus = ctx.getAccumulators(); + Map renamings = new HashMap<>(); + Map dynamicRenamings = new HashMap<>(); + + try { + ctx.setCurrentAST(ast); + if (eval.getCallTracing()) { + printStartTrace(actuals); + } - Environment old = ctx.getCurrentEnvt(); - AbstractAST currentAST = ctx.getCurrentAST(); - AbstractAST oldAST = currentAST; - Stack oldAccus = ctx.getAccumulators(); - Map renamings = new HashMap<>(); - Map dynamicRenamings = new HashMap<>(); - - try { - ctx.setCurrentAST(ast); - if (eval.getCallTracing()) { - printStartTrace(actuals); - } + String label = isAnonymous() ? "Anonymous Function" : name; + Environment environment = new Environment(declarationEnvironment, ctx.getCurrentEnvt(), currentAST != null ? currentAST.getLocation() : null, ast.getLocation(), label); + environment.markAsFunctionFrame(); + ctx.setCurrentEnvt(environment); - String label = isAnonymous() ? "Anonymous Function" : name; - Environment environment = new Environment(declarationEnvironment, ctx.getCurrentEnvt(), currentAST != null ? currentAST.getLocation() : null, ast.getLocation(), label); - environment.markAsFunctionFrame(); - ctx.setCurrentEnvt(environment); + IMatchingResult[] matchers = prepareFormals(ctx); + ctx.setAccumulators(accumulators); + ctx.pushEnv(); - IMatchingResult[] matchers = prepareFormals(ctx); - ctx.setAccumulators(accumulators); - ctx.pushEnv(); + Type actualStaticTypesTuple = TF.tupleType(actualStaticTypes); + if (hasVarArgs) { + if (!mayMatch(actualStaticTypesTuple)) { + throw new MatchFailed(); + } + actuals = computeVarArgsActuals(actuals, getFormals()); + actualStaticTypesTuple = computeVarArgsActualTypes(actualStaticTypes, getFormals()); + } + + int size = actuals.length; + Environment[] olds = new Environment[size]; + int i = 0; - Type actualStaticTypesTuple = TF.tupleType(actualStaticTypes); - if (hasVarArgs) { - if (!mayMatch(actualStaticTypesTuple)) { + if (!hasVarArgs && size != this.formals.size()) { throw new MatchFailed(); } - actuals = computeVarArgsActuals(actuals, getFormals()); - actualStaticTypesTuple = computeVarArgsActualTypes(actualStaticTypes, getFormals()); - } - - int size = actuals.length; - Environment[] olds = new Environment[size]; - int i = 0; - - if (!hasVarArgs && size != this.formals.size()) { - throw new MatchFailed(); - } - - actualStaticTypesTuple = bindTypeParameters(actualStaticTypesTuple, actuals, getFormals(), renamings, dynamicRenamings, environment); - - if (size == 0) { - try { - bindKeywordArgs(keyArgValues); -// checkReturnTypeIsNotVoid(formals, actuals); - result = runBody(); - result = storeMemoizedResult(actuals,keyArgValues, result); - if (eval.getCallTracing()) { - printEndTrace(result.getValue()); + + actualStaticTypesTuple = bindTypeParameters(actualStaticTypesTuple, actuals, getFormals(), renamings, dynamicRenamings, environment); + + if (size == 0) { + try { + bindKeywordArgs(keyArgValues); +// checkReturnTypeIsNotVoid(formals, actuals); + result = runBody(); + result = storeMemoizedResult(actuals,keyArgValues, result); + if (eval.getCallTracing()) { + printEndTrace(result.getValue()); + } + return result; + } + catch (Return e) { + checkReturnTypeIsNotVoid(formals, actuals, renamings); + result = computeReturn(e, renamings, dynamicRenamings); + storeMemoizedResult(actuals,keyArgValues, result); + return result; } - return result; - } - catch (Return e) { - checkReturnTypeIsNotVoid(formals, actuals, renamings); - result = computeReturn(e, renamings, dynamicRenamings); - storeMemoizedResult(actuals,keyArgValues, result); - return result; } - } - matchers[0].initMatch(makeResult(actualStaticTypesTuple.getFieldType(0), actuals[0], ctx)); - olds[0] = ctx.getCurrentEnvt(); - ctx.pushEnv(); + matchers[0].initMatch(makeResult(actualStaticTypesTuple.getFieldType(0), actuals[0], ctx)); + olds[0] = ctx.getCurrentEnvt(); + ctx.pushEnv(); - // pattern matching requires backtracking due to list, set and map matching and - // non-linear use of variables between formal parameters of a function... + // pattern matching requires backtracking due to list, set and map matching and + // non-linear use of variables between formal parameters of a function... - while (i >= 0 && i < size) { - if (ctx.isInterrupted()) { - throw new InterruptException(ctx.getStackTrace(), currentAST.getLocation()); - } - if (matchers[i].hasNext() && matchers[i].next()) { - if (i == size - 1) { - // formals are now bound by side effect of the pattern matcher - try { - bindKeywordArgs(keyArgValues); - - result = runBody(); - storeMemoizedResult(actuals, keyArgValues, result); - return result; - } - catch (Failure e) { - // backtrack current pattern assignment - if (!e.hasLabel() || e.hasLabel() && e.getLabel().equals(getName())) { - continue; + while (i >= 0 && i < size) { + if (ctx.isInterrupted()) { + throw new InterruptException(ctx.getStackTrace(), currentAST.getLocation()); + } + if (matchers[i].hasNext() && matchers[i].next()) { + if (i == size - 1) { + // formals are now bound by side effect of the pattern matcher + try { + bindKeywordArgs(keyArgValues); + + result = runBody(); + storeMemoizedResult(actuals, keyArgValues, result); + return result; } - else { - throw new UnguardedFail(getAst(), e); + catch (Failure e) { + // backtrack current pattern assignment + if (!e.hasLabel() || e.hasLabel() && e.getLabel().equals(getName())) { + continue; + } + else { + throw new UnguardedFail(getAst(), e); + } } } - } - else { - i++; - matchers[i].initMatch(makeResult(actualStaticTypesTuple.getFieldType(i), actuals[i], ctx)); - olds[i] = ctx.getCurrentEnvt(); + else { + i++; + matchers[i].initMatch(makeResult(actualStaticTypesTuple.getFieldType(i), actuals[i], ctx)); + olds[i] = ctx.getCurrentEnvt(); + ctx.pushEnv(); + } + } else { + ctx.unwind(olds[i]); + i--; ctx.pushEnv(); } - } else { - ctx.unwind(olds[i]); - i--; - ctx.pushEnv(); } - } - // backtrack to other function body - throw new MatchFailed(); - } - catch (Return e) { - checkReturnTypeIsNotVoid(formals, actuals, renamings); - - result = computeReturn(e, renamings, dynamicRenamings); - storeMemoizedResult(actuals, keyArgValues, result); - if (eval.getCallTracing()) { - printEndTrace(result.getValue()); + // backtrack to other function body + throw new MatchFailed(); } - return result; - } - catch (Throwable e) { - if (eval.getCallTracing()) { - printExcept(e); + catch (RestartFrameException rfe) { + // Handle frame restart at this level + // Restore environment state and retry execution + // Continue the while loop to retry the call + restartFrame = handleRestartFrame(rfe); + continue; } - if(e instanceof Exception){ - eval.notifyAboutSuspensionException((Exception)e); // Force call here so we do not loose the current env + catch (Return e) { + checkReturnTypeIsNotVoid(formals, actuals, renamings); + + result = computeReturn(e, renamings, dynamicRenamings); + storeMemoizedResult(actuals, keyArgValues, result); + if (eval.getCallTracing()) { + printEndTrace(result.getValue()); + } + return result; + } + catch (Throwable e) { + if (eval.getCallTracing()) { + printExcept(e); + } + if(e instanceof Exception){ + try { + eval.notifyAboutSuspensionException((Exception)e); + } catch (RestartFrameException rfe){ + // Special case to handle restart frame on the exception breakpoint + restartFrame = handleRestartFrame(rfe); + continue; + } + } + throw e; } - throw e; - } - finally { - if (eval.getCallTracing()) { - eval.decCallNesting(); + finally { + if (eval.getCallTracing()) { + eval.decCallNesting(); + } + ctx.setCurrentEnvt(old); + ctx.setAccumulators(oldAccus); + ctx.setCurrentAST(oldAST); } - ctx.setCurrentEnvt(old); - ctx.setAccumulators(oldAccus); - ctx.setCurrentAST(oldAST); } + throw new ImplementationError("Unreachable code reached after frame restart handling"); } private Result runBody() {