}
}
- /**
- * A GOSUB return address: the statement list the GOSUB sits in and the
- * pc of the statement after it. RETURN only resolves when it executes in
- * that same block — a GOSUB whose label lives in an outer block unwinds
- * the originating block, making its continuation unreachable.
- */
- /**
- * Signals an EXIT FOR/DO/WHILE/SUB/FUNCTION: unwinds to the innermost
- * matching construct, which catches it; everything else rethrows.
- */
- private static final class ExitJump extends RuntimeException {
- private final String what;
-
- ExitJump(final String construct) {
- super(null, null, false, false);
- what = construct;
- }
- }
-
- private record GosubFrame(List<Ast.Stmt> block, int pc) {
- }
-
- /**
- * Signals a GOTO jump. Each enclosing block that does not contain the
- * target label rethrows it, which gives procedure-wide label scope.
- */
- private static class GotoJump extends RuntimeException {
- private final String label;
- private final int line;
-
- GotoJump(final String targetLabel, final int sourceLine) {
- super(null, null, false, false);
- label = targetLabel;
- line = sourceLine;
- }
- }
-
- /**
- * A GOSUB in flight: a GOTO that additionally collects its origin
- * context chain as it unwinds. Every execBlockFrom the jump passes
- * through prepends its (block, pc-after-current-statement) frame, so
- * when the label resolves the chain reads outermost → innermost and
- * RETURN can rebuild the exact nested execution state (truth.bas:
- * GOSUB mkneg inside FOR → SELECT → IF, label at SUB body level).
- */
- private static final class GosubJump extends GotoJump {
- private final List<GosubFrame> chain = new ArrayList<>();
-
- GosubJump(final String targetLabel, final int sourceLine) {
- super(targetLabel, sourceLine);
- }
- }
-
- /**
- * A RETURN heading for a GOSUB origin in an ENCLOSING block of the
- * same procedure. Block pc loops rethrow it until the block that
- * resolved the GOSUB label (frame 0 of the chain) catches it and
- * rebuilds the origin inline — keeping its own pc loop on the stack
- * so jumps issued by the continued execution still resolve.
- */
- private static final class GosubReturnJump extends RuntimeException {
- private final List<GosubFrame> frames;
- private final int line;
-
- GosubReturnJump(final List<GosubFrame> frames, final int line) {
- super(null, null, false, false);
- this.frames = frames;
- this.line = line;
- }
- }
-
private final VgaDevice vga;
private final KeyboardQueue keys;
private final SoundQueue sound;
/** Scope stack: bottom is the global scope, top is the current SUB. */
private final Deque<Map<String, Cell>> scopes = new ArrayDeque<>();
/** GOSUB return addresses: context chain snapshots for RETURN. */
- private final Deque<List<GosubFrame>> gosubStack = new ArrayDeque<>();
+ private final Deque<List<Jumps.GosubFrame>> gosubStack = new ArrayDeque<>();
/**
* While a cross-block GOSUB origin is being rebuilt in a block's pc
* loop: that block, and the pc its frame should carry in any new GOSUB
}
}
execBlock(main);
- } catch (final ProgramEnd e) {
+ } catch (final Jumps.ProgramEnd e) {
// END statement: normal termination.
- } catch (final GotoJump jump) {
+ } catch (final Jumps.GotoJump jump) {
throw error("Label not found: " + jump.label, jump.line);
}
}
Map<String, Integer> labels = null;
int pc = startPc;
int stmtLine = 0;
- List<GosubFrame> pendingGosubResume = null;
+ List<Jumps.GosubFrame> pendingGosubResume = null;
while (pc < stmts.size() || pendingGosubResume != null) {
try {
if (pendingGosubResume != null) {
// try: jumps from the continued execution (e.g. the
// FOR's next iterations hitting GOSUB again) are
// caught by this pc loop, which stays on the stack.
- final List<GosubFrame> frames = pendingGosubResume;
+ final List<Jumps.GosubFrame> frames = pendingGosubResume;
pendingGosubResume = null;
if (frames.size() > 1) {
// While the rebuild runs, a new GOSUB chain's frame
final Ast.Stmt stmt = stmts.get(pc++);
stmtLine = stmt.line();
if (stmt instanceof Ast.GotoStmt g) {
- throw new GotoJump(g.label(), g.line());
+ throw new Jumps.GotoJump(g.label(), g.line());
}
if (stmt instanceof Ast.GosubStmt g) {
// The jump collects the origin context chain as it
// unwinds (each block's catch prepends its frame,
// including this one); the snapshot is pushed where
// the label resolves, and RETURN rebuilds from it.
- throw new GosubJump(g.label(), g.line());
+ throw new Jumps.GosubJump(g.label(), g.line());
}
if (stmt instanceof Ast.ReturnStmt r) {
if (++executed > maxStatements) {
throw new StepsLimitException(executed);
}
- final List<GosubFrame> snapshot = gosubStack.pollLast();
+ final List<Jumps.GosubFrame> snapshot = gosubStack.pollLast();
if (snapshot == null) {
throw error("RETURN without GOSUB", r.line());
}
if (snapshot.get(0).block() != stmts) {
// The RETURN sits in a block nested inside the
// subroutine stretch: unwind to the resolving block.
- throw new GosubReturnJump(snapshot, r.line());
+ throw new Jumps.GosubReturnJump(snapshot, r.line());
}
// Resume at the top of the loop, inside the try.
pendingGosubResume = snapshot;
continue;
}
exec(stmt);
- } catch (final GotoJump caught) {
+ } catch (final Jumps.GotoJump caught) {
if (++executed > maxStatements) {
throw new StepsLimitException(executed);
}
}
}
}
- GotoJump jump = caught;
+ Jumps.GotoJump jump = caught;
while (true) {
// A GOSUB issued while this block was rebuilding a
// RETURN origin: this block's frame belongs to the
}
final Integer target = labels.get(jump.label.toLowerCase());
if (target != null) {
- if (jump instanceof GosubJump gosub) {
+ if (jump instanceof Jumps.GosubJump gosub) {
// Label resolved at this level: this block's
// frame is the outermost of the return chain.
- gosub.chain.add(0, new GosubFrame(stmts, originPc));
+ gosub.chain.add(0, new Jumps.GosubFrame(stmts, originPc));
gosubStack.addLast(gosub.chain);
}
pc = target;
}
// Flat QBasic semantics: the label may be nested inside
// IF/FOR/DO bodies of this block — jump INTO it.
- final List<NestedLabelStep> path = findNestedLabel(stmts, jump.label);
+ final List<LabelSearch.NestedLabelStep> path = LabelSearch.findNestedLabel(stmts, jump.label);
if (path == null) {
- if (jump instanceof GosubJump gosub) {
+ if (jump instanceof Jumps.GosubJump gosub) {
// Not here either: record this level's frame and
// keep unwinding toward the enclosing block.
- gosub.chain.add(0, new GosubFrame(stmts, originPc));
+ gosub.chain.add(0, new Jumps.GosubFrame(stmts, originPc));
}
throw jump;
}
- if (jump instanceof GosubJump) {
+ if (jump instanceof Jumps.GosubJump) {
// RETURN from a nested-resume stretch can't rebuild
// the origin: mark the snapshot unsupported.
gosubStack.addLast(List.of());
try {
pc = resumeNestedLabel(stmts, path);
break;
- } catch (final GotoJump again) {
+ } catch (final Jumps.GotoJump again) {
// A GOTO issued from inside the resumed stretch:
// resolve it against this same block.
jump = again;
}
}
- } catch (final GosubReturnJump ret) {
+ } catch (final Jumps.GosubReturnJump ret) {
// A RETURN aiming for an origin that passes through an
// ENCLOSING block. Only the block that resolved the GOSUB
// label (frame 0) may resume the chain — everyone else
}
resumePc = pc - 1;
pc = target;
- } catch (final ResumeJump resume) {
+ } catch (final Jumps.ResumeJump resume) {
if (++executed > maxStatements) {
throw new StepsLimitException(executed);
}
}
final Integer target = labels.get(resume.label.toLowerCase());
if (target == null) {
- throw new GotoJump(resume.label, 0);
+ throw new Jumps.GotoJump(resume.label, 0);
}
pc = target;
} else if (resumePc < 0) {
}
}
- /**
- * One step on the path to a label nested inside a block statement:
- * the statement of the enclosing list whose child list leads to the
- * label. In the path's last step, {@code labelIndex} is the label's
- * position inside {@code childList} (-1 for the other steps).
- */
- private record NestedLabelStep(Ast.Stmt stmt, List<Ast.Stmt> childList, int labelIndex) {
- }
-
- /**
- * Finds a label nested inside the statements of {@code stmts} (any
- * depth), returning the chain of enclosing statements from this
- * block down to the list that directly holds the label — or null
- * when the label is nowhere below this block.
- */
- private List<NestedLabelStep> findNestedLabel(final List<Ast.Stmt> stmts, final String label) {
- for (final Ast.Stmt stmt : stmts) {
- for (final List<Ast.Stmt> child : childListsOf(stmt)) {
- for (int i = 0; i < child.size(); i++) {
- if (child.get(i) instanceof Ast.LabelStmt l
- && l.label().equalsIgnoreCase(label)) {
- final List<NestedLabelStep> path = new ArrayList<>();
- path.add(new NestedLabelStep(stmt, child, i));
- return path;
- }
- }
- final List<NestedLabelStep> sub = findNestedLabel(child, label);
- if (sub != null) {
- sub.add(0, new NestedLabelStep(stmt, child, -1));
- return sub;
- }
- }
- }
- return null;
- }
-
- /** Statement lists nested directly inside a statement. */
- private static List<List<Ast.Stmt>> childListsOf(final Ast.Stmt stmt) {
- return switch (stmt) {
- case Ast.IfStmt s -> s.elseBody().isEmpty()
- ? List.of(s.thenBody()) : List.of(s.thenBody(), s.elseBody());
- case Ast.ForStmt s -> List.of(s.body());
- case Ast.WhileStmt s -> List.of(s.body());
- case Ast.DoStmt s -> List.of(s.body());
- case Ast.SelectStmt s -> {
- final List<List<Ast.Stmt>> lists = new ArrayList<>();
- for (final Ast.CaseBranch branch : s.cases()) {
- lists.add(branch.body());
- }
- if (!s.elseBody().isEmpty()) {
- lists.add(s.elseBody());
- }
- yield lists;
- }
- default -> List.of();
- };
- }
-
/**
* Executes a GOTO whose target label is nested inside block
* statements of {@code stmts}, with flat QBasic semantics: enclosing
* @return the pc at which this block continues once the outermost
* enclosing statement finishes
*/
- private int resumeNestedLabel(final List<Ast.Stmt> stmts, final List<NestedLabelStep> path) {
- final NestedLabelStep last = path.get(path.size() - 1);
+ private int resumeNestedLabel(final List<Ast.Stmt> stmts, final List<LabelSearch.NestedLabelStep> path) {
+ final LabelSearch.NestedLabelStep last = path.get(path.size() - 1);
execBlockFrom(last.childList(), last.labelIndex());
for (int level = path.size() - 1; level >= 0; level--) {
final Ast.Stmt enclosing = path.get(level).stmt();
continueLoopAfterResume(enclosing);
final List<Ast.Stmt> parentList = level == 0 ? stmts : path.get(level - 1).childList();
- final int enclosingIndex = indexOfIdentity(parentList, enclosing);
+ final int enclosingIndex = LabelSearch.indexOfIdentity(parentList, enclosing);
if (level == 0) {
return enclosingIndex + 1;
}
/**
* Rebuilds a cross-block GOSUB origin on RETURN, starting at the given
- * chain level (1 from the resolving block's GosubReturnJump catch —
+ * chain level (1 from the resolving block's Jumps.GosubReturnJump catch —
* frame 0 is that block itself, resumed by its pc loop). The chain
* (outermost → innermost) was captured while the GOSUB jump unwound.
* The innermost block resumes at the statement after the GOSUB; each
* iterating via {@link #continueLoopAfterResume}) and runs the rest
* of its block — mirroring {@link #resumeNestedLabel}.
*/
- private void resumeChain(final List<GosubFrame> chain, final int level) {
- final GosubFrame frame = chain.get(level);
+ private void resumeChain(final List<Jumps.GosubFrame> chain, final int level) {
+ final Jumps.GosubFrame frame = chain.get(level);
if (level == chain.size() - 1) {
execBlockFrom(frame.block(), frame.pc());
return;
}
}
- private static int indexOfIdentity(final List<Ast.Stmt> stmts, final Ast.Stmt needle) {
- for (int i = 0; i < stmts.size(); i++) {
- if (stmts.get(i) == needle) {
- return i;
- }
- }
- throw new IllegalStateException("statement not found in its own block");
- }
+
private void exec(final Ast.Stmt stmt) {
if (++executed > maxStatements) {
// ON ERROR GOTO 0 disables trapping.
errorHandler = "0".equals(s.label()) ? null : s.label().toLowerCase();
}
- case Ast.ResumeStmt s -> throw new ResumeJump(s.label(), s.next());
+ case Ast.ResumeStmt s -> throw new Jumps.ResumeJump(s.label(), s.next());
case Ast.ColorStmt s -> {
final int fg = toInt(eval(s.foreground()), s.line());
final int bg = s.background() == null ? currentBackground()
}
}
case Ast.SwapStmt s -> execSwap(s);
- case Ast.ExitStmt s -> throw new ExitJump(s.what());
+ case Ast.ExitStmt s -> throw new Jumps.ExitJump(s.what());
case Ast.StaticStmt s -> {
// STATIC name, ...: these locals persist across calls of
// the current procedure.
case Ast.IgnoredStmt s -> {
// Parsed but intentionally not modeled.
}
- case Ast.GotoStmt s -> throw new GotoJump(s.label(), s.line());
+ case Ast.GotoStmt s -> throw new Jumps.GotoJump(s.label(), s.line());
case Ast.GosubStmt s ->
// Reached only through paths that bypass execBlock's pc loop.
throw error("GOSUB is only supported as a standalone statement", s.line());
}
execBlock(s.body());
}
- } catch (final ExitJump exit) {
+ } catch (final Jumps.ExitJump exit) {
if (!exit.what.equals("WHILE")) {
throw exit;
}
break;
}
}
- } catch (final ExitJump exit) {
+ } catch (final Jumps.ExitJump exit) {
if (!exit.what.equals("DO")) {
throw exit;
}
}
case Ast.EndStmt s -> {
// Program end: run() simply returns after this.
- throw new ProgramEnd();
+ throw new Jumps.ProgramEnd();
}
}
}
execBlock(s.body());
cell.set(num(cell.value, s.line()) + step);
}
- } catch (final ExitJump exit) {
+ } catch (final Jumps.ExitJump exit) {
if (!exit.what.equals("FOR")) {
throw exit;
}
pushProcScope(sub.name(), sub.isStatic(), scope);
try {
execBlock(sub.body());
- } catch (final ExitJump exit) {
+ } catch (final Jumps.ExitJump exit) {
if (!exit.what.equals("SUB")) {
throw exit;
}
- } catch (final GotoJump jump) {
+ } catch (final Jumps.GotoJump jump) {
throw error("Label not found: " + jump.label, jump.line);
} finally {
popProcScope();
pushProcScope(function.name(), function.isStatic(), scope);
try {
execBlock(function.body());
- } catch (final ExitJump exit) {
+ } catch (final Jumps.ExitJump exit) {
if (!exit.what.equals("FUNCTION")) {
throw exit;
}
- } catch (final GotoJump jump) {
+ } catch (final Jumps.GotoJump jump) {
throw error("Label not found: " + jump.label, jump.line);
} finally {
popProcScope();
}
}
- /**
- * Signals a RESUME jump out of an error handler: retry the trapped
- * statement (label null, next false), continue after it (next true),
- * or jump to a label.
- */
- private static final class ResumeJump extends RuntimeException {
- private final String label;
- private final boolean next;
-
- ResumeJump(final String targetLabel, final boolean resumeNext) {
- super(null, null, false, false);
- label = targetLabel;
- next = resumeNext;
- }
- }
-
- /** Control-flow signal for END. */
- private static final class ProgramEnd extends RuntimeException {
- ProgramEnd() {
- super(null, null, false, false);
- }
- }
}
--- /dev/null
+package eu.svjatoslav.crtbasic.interp;
+
+import eu.svjatoslav.crtbasic.ast.Ast;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Control-flow signals. Classic BASIC control flow (GOTO into nested
+ * blocks, GOSUB/RETURN across blocks, EXIT, RESUME, END) unwinds the
+ * interpreter's Java call stack with these exceptions; each enclosing
+ * block pc loop catches the ones it resolves and rethrows the rest.
+ * All are stackless (no fillInStackTrace) — they are control flow, not
+ * failures. Grouped here so Interpreter.java reads as statement
+ * semantics, not jump plumbing.
+ */
+final class Jumps {
+
+ private Jumps() {
+ // Holder class; see the nested signals.
+ }
+
+ /**
+ * Signals an EXIT FOR/DO/WHILE/SUB/FUNCTION: unwinds to the innermost
+ * matching construct, which catches it; everything else rethrows.
+ */
+ static final class ExitJump extends RuntimeException {
+ final String what;
+
+ ExitJump(final String construct) {
+ super(null, null, false, false);
+ what = construct;
+ }
+ }
+
+ /**
+ * A GOSUB return address: the statement list the GOSUB sits in and the
+ * pc of the statement after it. RETURN only resolves when it executes in
+ * that same block — a GOSUB whose label lives in an outer block unwinds
+ * the originating block, making its continuation unreachable.
+ */
+ record GosubFrame(List<Ast.Stmt> block, int pc) {
+ }
+
+ /**
+ * Signals a GOTO jump. Each enclosing block that does not contain the
+ * target label rethrows it, which gives procedure-wide label scope.
+ */
+ static class GotoJump extends RuntimeException {
+ final String label;
+ final int line;
+
+ GotoJump(final String targetLabel, final int sourceLine) {
+ super(null, null, false, false);
+ label = targetLabel;
+ line = sourceLine;
+ }
+ }
+
+ /**
+ * A GOSUB in flight: a GOTO that additionally collects its origin
+ * context chain as it unwinds. Every execBlockFrom the jump passes
+ * through prepends its (block, pc-after-current-statement) frame, so
+ * when the label resolves the chain reads outermost → innermost and
+ * RETURN can rebuild the exact nested execution state (truth.bas:
+ * GOSUB mkneg inside FOR → SELECT → IF, label at SUB body level).
+ */
+ static final class GosubJump extends GotoJump {
+ final List<GosubFrame> chain = new ArrayList<>();
+
+ GosubJump(final String targetLabel, final int sourceLine) {
+ super(targetLabel, sourceLine);
+ }
+ }
+
+ /**
+ * A RETURN heading for a GOSUB origin in an ENCLOSING block of the
+ * same procedure. Block pc loops rethrow it until the block that
+ * resolved the GOSUB label (frame 0 of the chain) catches it and
+ * rebuilds the origin inline — keeping its own pc loop on the stack
+ * so jumps issued by the continued execution still resolve.
+ */
+ static final class GosubReturnJump extends RuntimeException {
+ final List<GosubFrame> frames;
+ final int line;
+
+ GosubReturnJump(final List<GosubFrame> frames, final int line) {
+ super(null, null, false, false);
+ this.frames = frames;
+ this.line = line;
+ }
+ }
+
+ /**
+ * Signals a RESUME jump out of an error handler: retry the trapped
+ * statement (label null, next false), continue after it (next true),
+ * or jump to a label.
+ */
+ static final class ResumeJump extends RuntimeException {
+ final String label;
+ final boolean next;
+
+ ResumeJump(final String targetLabel, final boolean resumeNext) {
+ super(null, null, false, false);
+ label = targetLabel;
+ next = resumeNext;
+ }
+ }
+
+ /** Control-flow signal for END. */
+ static final class ProgramEnd extends RuntimeException {
+ ProgramEnd() {
+ super(null, null, false, false);
+ }
+ }
+}