+ /**
+ * 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
+ * IF/SELECT headers are not re-evaluated, loops resume with the
+ * variable values the program left behind (the FOR variable is NOT
+ * re-initialized), and once the innermost list runs off its end the
+ * enclosing loops keep iterating and the enclosing lists continue.
+ *
+ * @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);
+ 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);
+ if (level == 0) {
+ return enclosingIndex + 1;
+ }
+ execBlockFrom(parentList, enclosingIndex + 1);
+ }
+ throw new IllegalStateException("empty label path");
+ }
+
+ /**
+ * Continues a loop statement whose current body pass just finished
+ * after an inward GOTO resume: remaining iterations run normally.
+ * Non-loop statements (IF/SELECT) have nothing to continue.
+ */
+ private void continueLoopAfterResume(final Ast.Stmt stmt) {
+ switch (stmt) {
+ case Ast.ForStmt s -> {
+ final Cell cell = cellFor(s.variable());
+ final double to = num(eval(s.to()), s.line());
+ final double step = s.step() == null ? 1 : num(eval(s.step()), s.line());
+ if (step == 0) {
+ throw error("STEP cannot be 0", s.line());
+ }
+ cell.set(num(cell.value, s.line()) + step);
+ while (step > 0 ? num(cell.value, s.line()) <= to
+ : num(cell.value, s.line()) >= to) {
+ execBlock(s.body());
+ cell.set(num(cell.value, s.line()) + step);
+ }
+ }
+ case Ast.WhileStmt s -> {
+ while (truthy(eval(s.condition()))) {
+ execBlock(s.body());
+ }
+ }
+ case Ast.DoStmt s -> {
+ while (true) {
+ if (s.postCondition() != null
+ && truthy(eval(s.postCondition())) == s.postIsUntil()) {
+ break;
+ }
+ if (s.preCondition() != null
+ && truthy(eval(s.preCondition())) == s.preIsUntil()) {
+ break;
+ }
+ execBlock(s.body());
+ }
+ }
+ default -> {
+ // IF / SELECT: the resumed branch simply ends.
+ }
+ }
+ }
+
+ 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");
+ }
+