From d78fea147c5502bd6dd35d97f2bb81388d05ea9d Mon Sep 17 00:00:00 2001 From: Svjatoslav Agejenko Date: Thu, 3 Sep 2026 23:55:40 +0300 Subject: [PATCH] refactor(interp): move control-flow signals and label search out of Interpreter MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit The GOTO/GOSUB plumbing (jump exception types, the GosubFrame chain, nested-label search) sat inline in Interpreter between the variable cell model and the statement dispatch, where it read as noise around statement semantics. - Jumps.java groups the seven stackless control-flow signals (ExitJump, GotoJump, GosubJump, GosubReturnJump, ResumeJump, ProgramEnd, GosubFrame) with their semantics documented once, up front - LabelSearch.java holds the pure Ast utilities (findNestedLabel, childListsOf, indexOfIdentity) — no interpreter state involved resumeNestedLabel/resumeChain/continueLoopAfterResume stay in Interpreter: they are interlocked with execBlockFrom's pc loop and the GOSUB rebuild state, and moving them would distort rather than clarify. Also fixes a doubled/orphan javadoc above the old GosubFrame. Verification: 93/93 tests green (the suite covers nested-label GOTO and cross-block GOSUB/RETURN); EDPAT (EXIT DO) and BAR re-run pixel-identical. --- .../crtbasic/interp/Interpreter.java | 236 +++--------------- .../eu/svjatoslav/crtbasic/interp/Jumps.java | 115 +++++++++ .../crtbasic/interp/LabelSearch.java | 86 +++++++ 3 files changed, 240 insertions(+), 197 deletions(-) create mode 100644 src/main/java/eu/svjatoslav/crtbasic/interp/Jumps.java create mode 100644 src/main/java/eu/svjatoslav/crtbasic/interp/LabelSearch.java diff --git a/src/main/java/eu/svjatoslav/crtbasic/interp/Interpreter.java b/src/main/java/eu/svjatoslav/crtbasic/interp/Interpreter.java index 81e6900..454fabb 100644 --- a/src/main/java/eu/svjatoslav/crtbasic/interp/Interpreter.java +++ b/src/main/java/eu/svjatoslav/crtbasic/interp/Interpreter.java @@ -131,77 +131,6 @@ public final class Interpreter { } } - /** - * 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 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 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 frames; - private final int line; - - GosubReturnJump(final List 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; @@ -215,7 +144,7 @@ public final class Interpreter { /** Scope stack: bottom is the global scope, top is the current SUB. */ private final Deque> scopes = new ArrayDeque<>(); /** GOSUB return addresses: context chain snapshots for RETURN. */ - private final Deque> gosubStack = new ArrayDeque<>(); + private final Deque> 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 @@ -498,9 +427,9 @@ public final class Interpreter { } } 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); } } @@ -533,7 +462,7 @@ public final class Interpreter { Map labels = null; int pc = startPc; int stmtLine = 0; - List pendingGosubResume = null; + List pendingGosubResume = null; while (pc < stmts.size() || pendingGosubResume != null) { try { if (pendingGosubResume != null) { @@ -542,7 +471,7 @@ public final class Interpreter { // 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 frames = pendingGosubResume; + final List frames = pendingGosubResume; pendingGosubResume = null; if (frames.size() > 1) { // While the rebuild runs, a new GOSUB chain's frame @@ -564,20 +493,20 @@ public final class Interpreter { 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 snapshot = gosubStack.pollLast(); + final List snapshot = gosubStack.pollLast(); if (snapshot == null) { throw error("RETURN without GOSUB", r.line()); } @@ -589,14 +518,14 @@ public final class Interpreter { 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); } @@ -608,7 +537,7 @@ public final class Interpreter { } } } - 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 @@ -623,10 +552,10 @@ public final class Interpreter { } 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; @@ -634,16 +563,16 @@ public final class Interpreter { } // Flat QBasic semantics: the label may be nested inside // IF/FOR/DO bodies of this block — jump INTO it. - final List path = findNestedLabel(stmts, jump.label); + final List 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()); @@ -651,13 +580,13 @@ public final class Interpreter { 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 @@ -694,7 +623,7 @@ public final class Interpreter { } resumePc = pc - 1; pc = target; - } catch (final ResumeJump resume) { + } catch (final Jumps.ResumeJump resume) { if (++executed > maxStatements) { throw new StepsLimitException(executed); } @@ -709,7 +638,7 @@ public final class Interpreter { } 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) { @@ -722,64 +651,6 @@ public final class Interpreter { } } - /** - * 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 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 findNestedLabel(final List stmts, final String label) { - for (final Ast.Stmt stmt : stmts) { - for (final List 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 path = new ArrayList<>(); - path.add(new NestedLabelStep(stmt, child, i)); - return path; - } - } - final List 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> 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> 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 @@ -791,14 +662,14 @@ public final class Interpreter { * @return the pc at which this block continues once the outermost * enclosing statement finishes */ - private int resumeNestedLabel(final List stmts, final List path) { - final NestedLabelStep last = path.get(path.size() - 1); + private int resumeNestedLabel(final List stmts, final List 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 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; } @@ -809,7 +680,7 @@ public final class Interpreter { /** * 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 @@ -817,8 +688,8 @@ public final class Interpreter { * iterating via {@link #continueLoopAfterResume}) and runs the rest * of its block — mirroring {@link #resumeNestedLabel}. */ - private void resumeChain(final List chain, final int level) { - final GosubFrame frame = chain.get(level); + private void resumeChain(final List chain, final int level) { + final Jumps.GosubFrame frame = chain.get(level); if (level == chain.size() - 1) { execBlockFrom(frame.block(), frame.pc()); return; @@ -876,14 +747,7 @@ public final class Interpreter { } } - private static int indexOfIdentity(final List 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) { @@ -974,7 +838,7 @@ public final class Interpreter { // 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() @@ -999,7 +863,7 @@ public final class Interpreter { } } 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. @@ -1217,7 +1081,7 @@ public final class Interpreter { 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()); @@ -1312,7 +1176,7 @@ public final class Interpreter { } execBlock(s.body()); } - } catch (final ExitJump exit) { + } catch (final Jumps.ExitJump exit) { if (!exit.what.equals("WHILE")) { throw exit; } @@ -1336,7 +1200,7 @@ public final class Interpreter { break; } } - } catch (final ExitJump exit) { + } catch (final Jumps.ExitJump exit) { if (!exit.what.equals("DO")) { throw exit; } @@ -1344,7 +1208,7 @@ public final class Interpreter { } case Ast.EndStmt s -> { // Program end: run() simply returns after this. - throw new ProgramEnd(); + throw new Jumps.ProgramEnd(); } } } @@ -1554,7 +1418,7 @@ public final class Interpreter { 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; } @@ -1575,11 +1439,11 @@ public final class Interpreter { 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(); @@ -1690,11 +1554,11 @@ public final class Interpreter { 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(); @@ -3227,26 +3091,4 @@ public final class Interpreter { } } - /** - * 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); - } - } } diff --git a/src/main/java/eu/svjatoslav/crtbasic/interp/Jumps.java b/src/main/java/eu/svjatoslav/crtbasic/interp/Jumps.java new file mode 100644 index 0000000..cc70907 --- /dev/null +++ b/src/main/java/eu/svjatoslav/crtbasic/interp/Jumps.java @@ -0,0 +1,115 @@ +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 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 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 frames; + final int line; + + GosubReturnJump(final List 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); + } + } +} diff --git a/src/main/java/eu/svjatoslav/crtbasic/interp/LabelSearch.java b/src/main/java/eu/svjatoslav/crtbasic/interp/LabelSearch.java new file mode 100644 index 0000000..5ca0fa8 --- /dev/null +++ b/src/main/java/eu/svjatoslav/crtbasic/interp/LabelSearch.java @@ -0,0 +1,86 @@ +package eu.svjatoslav.crtbasic.interp; + +import eu.svjatoslav.crtbasic.ast.Ast; +import java.util.ArrayList; +import java.util.List; + +/** + * Label search over the statement tree: a GOTO/GOSUB target may live in + * the current block or nested inside IF/FOR/WHILE/DO/SELECT bodies of + * the same procedure (flat QBasic line semantics). Pure functions over + * the Ast — no interpreter state. + */ +final class LabelSearch { + + private LabelSearch() { + // Utility class. + } + + /** + * 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). + */ + record NestedLabelStep(Ast.Stmt stmt, List 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. + */ + static List findNestedLabel(final List stmts, final String label) { + for (final Ast.Stmt stmt : stmts) { + for (final List 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 path = new ArrayList<>(); + path.add(new NestedLabelStep(stmt, child, i)); + return path; + } + } + final List 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. */ + static List> 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> 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(); + }; + } + + /** Identity-based index lookup (records never implement equals). */ + static int indexOfIdentity(final List 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"); + } +} -- 2.20.1