diff options
-rw-r--r-- | .drone.yml | 1 | ||||
-rw-r--r-- | .drone.yml.sig | 2 | ||||
-rw-r--r-- | .gitignore | 1 | ||||
-rwxr-xr-x | bin/dotr | 17 | ||||
-rw-r--r-- | compiler/src/dotty/tools/dotc/ast/Desugar.scala | 4 | ||||
-rwxr-xr-x | compiler/test/debug/Gen | 13 | ||||
-rwxr-xr-x | compiler/test/debug/Gen.scala | 171 | ||||
-rwxr-xr-x | compiler/test/debug/test | 21 | ||||
-rw-r--r-- | docs/docs/contributing/debug-tests.md | 124 | ||||
-rw-r--r-- | tests/debug/for.scala | 16 | ||||
-rw-r--r-- | tests/debug/function.scala | 14 | ||||
-rw-r--r-- | tests/debug/if.scala | 20 | ||||
-rw-r--r-- | tests/debug/method.scala | 14 | ||||
-rw-r--r-- | tests/debug/nested-method.scala | 15 | ||||
-rw-r--r-- | tests/debug/sequence.scala | 11 | ||||
-rw-r--r-- | tests/debug/tailrec.scala | 17 | ||||
-rw-r--r-- | tests/debug/while.scala | 14 |
17 files changed, 471 insertions, 4 deletions
diff --git a/.drone.yml b/.drone.yml index 83309ef4e..935297a5b 100644 --- a/.drone.yml +++ b/.drone.yml @@ -7,6 +7,7 @@ pipeline: - ln -s /var/cache/drone/ivy2 "$HOME/.ivy2" - ./scripts/update-scala-library - sbt -J-Xmx4096m -J-XX:ReservedCodeCacheSize=512m -J-XX:MaxMetaspaceSize=1024m -Ddotty.drone.mem=4096m "${TEST}" + - ./compiler/test/debug/test when: branch: exclude: gh-pages diff --git a/.drone.yml.sig b/.drone.yml.sig index 1093928f0..63609e5c8 100644 --- a/.drone.yml.sig +++ b/.drone.yml.sig @@ -1 +1 @@ -eyJhbGciOiJIUzI1NiJ9.cGlwZWxpbmU6CiAgdGVzdDoKICAgIGltYWdlOiBsYW1wZXBmbC9kb3R0eTpsYXRlc3QKICAgIHB1bGw6IHRydWUKICAgIGNvbW1hbmRzOgogICAgICAtIGxuIC1zIC92YXIvY2FjaGUvZHJvbmUvc2NhbGEtc2NhbGEgc2NhbGEtc2NhbGEKICAgICAgLSBsbiAtcyAvdmFyL2NhY2hlL2Ryb25lL2l2eTIgIiRIT01FLy5pdnkyIgogICAgICAtIC4vc2NyaXB0cy91cGRhdGUtc2NhbGEtbGlicmFyeQogICAgICAtIHNidCAtSi1YbXg0MDk2bSAtSi1YWDpSZXNlcnZlZENvZGVDYWNoZVNpemU9NTEybSAtSi1YWDpNYXhNZXRhc3BhY2VTaXplPTEwMjRtIC1EZG90dHkuZHJvbmUubWVtPTQwOTZtICIke1RFU1R9IgogICAgd2hlbjoKICAgICAgYnJhbmNoOgogICAgICAgIGV4Y2x1ZGU6IGdoLXBhZ2VzCgogIGRvY3VtZW50YXRpb246CiAgICBpbWFnZTogbGFtcGVwZmwvZG90dHk6bGF0ZXN0CiAgICBwdWxsOiB0cnVlCiAgICBjb21tYW5kczoKICAgICAgLSAuL3Byb2plY3Qvc2NyaXB0cy9nZW5Eb2NzICIke1RFU1R9IiAkQk9UX1BBU1MKICAgIHdoZW46CiAgICAgIGJyYW5jaDogbWFzdGVyCgogIGdpdHRlcjoKICAgIGltYWdlOiBwbHVnaW5zL2dpdHRlcgogICAgd2hlbjoKICAgICAgYnJhbmNoOiBtYXN0ZXIKICAgICAgc3RhdHVzOiBjaGFuZ2VkCgogIHNsYWNrOgogICAgaW1hZ2U6IHBsdWdpbnMvc2xhY2sKICAgIGNoYW5uZWw6IGRvdHR5CiAgICB3aGVuOgogICAgICBicmFuY2g6IG1hc3RlcgogICAgICBzdGF0dXM6IGNoYW5nZWQKCm1hdHJpeDoKICBURVNUOgogICAgLSA7dGVzdDtkb3R0eS1iaW4tdGVzdHMvdGVzdAogICAgLSA7cHVibGlzaExvY2FsO2RvdHR5LWJvb3RzdHJhcHBlZC90ZXN0CiAgICAtIHBhcnRlc3Qtb25seS1uby1ib290c3RyYXAgLS1zaG93LWRpZmYgLS12ZXJib3NlCiAgICAtIHBhcnRlc3Qtb25seSAtLXNob3ctZGlmZiAtLXZlcmJvc2UK.VRqZiSgeE6OumPlEvs4TWfxIHNOEVjR_ZmyBmapxZ-U
\ No newline at end of file +eyJhbGciOiJIUzI1NiJ9.cGlwZWxpbmU6CiAgdGVzdDoKICAgIGltYWdlOiBsYW1wZXBmbC9kb3R0eTpsYXRlc3QKICAgIHB1bGw6IHRydWUKICAgIGNvbW1hbmRzOgogICAgICAtIGxuIC1zIC92YXIvY2FjaGUvZHJvbmUvc2NhbGEtc2NhbGEgc2NhbGEtc2NhbGEKICAgICAgLSBsbiAtcyAvdmFyL2NhY2hlL2Ryb25lL2l2eTIgIiRIT01FLy5pdnkyIgogICAgICAtIC4vc2NyaXB0cy91cGRhdGUtc2NhbGEtbGlicmFyeQogICAgICAtIHNidCAtSi1YbXg0MDk2bSAtSi1YWDpSZXNlcnZlZENvZGVDYWNoZVNpemU9NTEybSAtSi1YWDpNYXhNZXRhc3BhY2VTaXplPTEwMjRtIC1EZG90dHkuZHJvbmUubWVtPTQwOTZtICIke1RFU1R9IgogICAgICAtIC4vY29tcGlsZXIvdGVzdC9kZWJ1Zy90ZXN0CiAgICB3aGVuOgogICAgICBicmFuY2g6CiAgICAgICAgZXhjbHVkZTogZ2gtcGFnZXMKCiAgZG9jdW1lbnRhdGlvbjoKICAgIGltYWdlOiBsYW1wZXBmbC9kb3R0eTpsYXRlc3QKICAgIHB1bGw6IHRydWUKICAgIGNvbW1hbmRzOgogICAgICAtIC4vcHJvamVjdC9zY3JpcHRzL2dlbkRvY3MgIiR7VEVTVH0iICRCT1RfUEFTUwogICAgd2hlbjoKICAgICAgYnJhbmNoOiBtYXN0ZXIKCiAgZ2l0dGVyOgogICAgaW1hZ2U6IHBsdWdpbnMvZ2l0dGVyCiAgICB3aGVuOgogICAgICBicmFuY2g6IG1hc3RlcgogICAgICBzdGF0dXM6IGNoYW5nZWQKCiAgc2xhY2s6CiAgICBpbWFnZTogcGx1Z2lucy9zbGFjawogICAgY2hhbm5lbDogZG90dHkKICAgIHdoZW46CiAgICAgIGJyYW5jaDogbWFzdGVyCiAgICAgIHN0YXR1czogY2hhbmdlZAoKbWF0cml4OgogIFRFU1Q6CiAgICAtIDt0ZXN0O2RvdHR5LWJpbi10ZXN0cy90ZXN0CiAgICAtIDtwdWJsaXNoTG9jYWw7ZG90dHktYm9vdHN0cmFwcGVkL3Rlc3QKICAgIC0gcGFydGVzdC1vbmx5LW5vLWJvb3RzdHJhcCAtLXNob3ctZGlmZiAtLXZlcmJvc2UKICAgIC0gcGFydGVzdC1vbmx5IC0tc2hvdy1kaWZmIC0tdmVyYm9zZQo.NgZCPjkR9Z1A96-ryvHIVJOsL9GI6aIvU6tGC_hv5Lo
\ No newline at end of file diff --git a/.gitignore b/.gitignore index 1a0b1b6a7..9842e0c6b 100644 --- a/.gitignore +++ b/.gitignore @@ -52,4 +52,5 @@ build/ # Put local stuff here local/ +compiler/test/debug/Gen.jar @@ -8,6 +8,10 @@ fi DOTTY_ROOT="$(dirname "$DOTTY_ROOT")" DOTTY_ROOT="$( cd "$DOTTY_ROOT" >& /dev/null && pwd )/.." # absolute +# debug +DEBUG_STR=-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005 +DEBUG= + # Load common functions and variables source "$DOTTY_ROOT"/bin/common @@ -24,10 +28,21 @@ function runMain { echo "java bin not detected - please specify with \$JAVA_BIN or install java to a default location" exit 1 else - eval "$jbin $CLASS_PATH $@" + eval "$jbin $DEBUG $CLASS_PATH $@" fi } +# parse command line params -d to enable debugging +while getopts "dx" opt; do + case "$opt" in + d) + DEBUG="$DEBUG_STR" + ;; + esac +done + +shift $((OPTIND-1)) + first_arg="$1" if [ -z "$1" ]; then diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index eda4a12dc..e3102fda2 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -991,12 +991,12 @@ object desugar { else Apply(ref(tupleTypeRef.classSymbol.companionModule.valRef), ts) case WhileDo(cond, body) => // { <label> def while$(): Unit = if (cond) { body; while$() } ; while$() } - val call = Apply(Ident(nme.WHILE_PREFIX), Nil) + val call = Apply(Ident(nme.WHILE_PREFIX), Nil).withPos(tree.pos) val rhs = If(cond, Block(body, call), unitLiteral) labelDefAndCall(nme.WHILE_PREFIX, rhs, call) case DoWhile(body, cond) => // { label def doWhile$(): Unit = { body; if (cond) doWhile$() } ; doWhile$() } - val call = Apply(Ident(nme.DO_WHILE_PREFIX), Nil) + val call = Apply(Ident(nme.DO_WHILE_PREFIX), Nil).withPos(tree.pos) val rhs = Block(body, If(cond, call, unitLiteral)) labelDefAndCall(nme.DO_WHILE_PREFIX, rhs, call) case ForDo(enums, body) => diff --git a/compiler/test/debug/Gen b/compiler/test/debug/Gen new file mode 100755 index 000000000..c5c4d6ec6 --- /dev/null +++ b/compiler/test/debug/Gen @@ -0,0 +1,13 @@ +#! /usr/bin/env bash + +DIR="$( cd "$( dirname "$0" )" && pwd )" + +SOURCE=$DIR/Gen.scala +CLASS=./Gen.class + +if [ ! -e $CLASS ] || [ $SOURCE -nt $CLASS ]; then + ./bin/dotc $DIR/Gen.scala +fi + +./bin/dotr Gen $@ + diff --git a/compiler/test/debug/Gen.scala b/compiler/test/debug/Gen.scala new file mode 100755 index 000000000..f7a5cc432 --- /dev/null +++ b/compiler/test/debug/Gen.scala @@ -0,0 +1,171 @@ +import scala.io.Source +import scala.collection.mutable.ListBuffer + +/** Automate testing debuggability of generated code using JDB and expect + * + * The debugging information is annotated as comments to the code in brackets: + * + * val x = f(3) // [break] [next: line=5] + * val y = 5 + * + * 1. A jdb command must be wrapped in brackets, like `[step]`. All jdb commands can be used. + * 2. To check output of jdb for a command, use `[cmd: expect]`. + * 3. If `expect` is wrapped in double quotes, regex is supported. + * 4. Break commands are collected and set globally. + * 5. Other commands will be send to jdb in the order they appear in the source file + * + * Note: jdb uses line number starts from 1 + */ + +object Gen { + val MainObject = "Test" + val CommandWait = 0.5 + + sealed trait Tree + + case class Break(line: Int) extends Tree + + case class Command(val name: String, val expect: Expect = EmptyExpect) extends Tree + + sealed trait Expect + + case object EmptyExpect extends Expect + + case class LitExpect(lit: String) extends Expect + + case class PatExpect(pat: String) extends Expect + + case class Program(breaks: Seq[Break], commands: Seq[Command]) + + def error(msg: String): Nothing = { + throw new Exception(msg) + } + + def parseCommand(command: String, lineNo: Int): Tree = { + val index = command.indexOf(':') + if (index == -1) { + // simple command + if (command == "break") Break(lineNo) + else Command(command) + } else { + val Seq(cmd, rhs) = command.split(":", 2).toSeq.map(_.trim) + if (rhs.startsWith("\"")) { + // regex match + val content = "\"(.+)\"".r + rhs match { + case content(expect) => Command(cmd, PatExpect(expect)) + case _ => error(s"""incorrect specification: `$rhs` for `$cmd` at line $lineNo. Ending " expected.""") + } + } else { + // literal match + Command(cmd, LitExpect(rhs)) + } + } + } + + def parse(file: String): Program = { + val lines = Source.fromFile(file).getLines.toBuffer + + val breaks = new ListBuffer[Break]() + val cmds = new ListBuffer[Command]() + lines.zipWithIndex.map { case (code, line) => + val comment = if (code.indexOf("//") != -1) code.split("//").last else "" + val regex = """(?<=\[).*?(?=\])""".r + for (p <- regex findAllIn comment) parseCommand(p.trim, line + 1) match { // jdb index from 0 + case b: Break => breaks += b + case c: Command => cmds += c + } + } + + Program(breaks, cmds) + } + + def generate(program: Program, source: String = "tests/debug/"): String = { + val Program(breaks, cmds) = program + val breakpoints = (breaks.map { + case Break(point) => + s"""|send "stop at $MainObject$$:$point\\r" + |sleep $CommandWait + |expect "breakpoint $MainObject$$:$point" + |expect -re $$ + """.stripMargin + }).mkString("\n\n") + + val commands = (cmds.map { + case Command(cmd, EmptyExpect) => + s"""|# send_user "send command `$cmd`\\n" + |send "$cmd\\r" + |sleep $CommandWait + |expect -re $$ + """.stripMargin + case Command(cmd, LitExpect(lit)) => + s"""|# send_user "send command `$cmd`\\n" + |send "$cmd\\r" + |sleep $CommandWait + |expect { + | "*$lit*" { send_user "success - $cmd : $lit \\n" } + | timeout { + | send_user "timeout while waiting for response: $cmd : $lit\\n" + | exit 1 + | } + |} + |expect -re $$ + |""".stripMargin + case Command(cmd, PatExpect(pat)) => + s"""|# send_user "send command `$cmd`\\n" + |send "$cmd\\r" + |sleep $CommandWait + |expect { + | -re {$pat} { send_user "success - $cmd : $pat \\n" } + | timeout { + | send_user "timeout while waiting for response: $cmd : $pat\\n" + | exit 1 + | } + |} + |expect -re $$ + |""".stripMargin + }).mkString("\n\n") + +s"""|#!/usr/bin/expect + | + |# log_user 1 + |# exp_internal 1 + |# set timeout 5 + | + |send_user "spawning job...\\n" + | + |spawn jdb -attach 5005 -sourcepath $source + | + |send_user "interacting...\\n" + | + |expect { + | "*VM Started*" { send_user "success - connected to server \\n" } + | timeout { + | send_user "timeout while waiting for: *VM Started*\\n" + | exit 1 + | } + |} + | + |send_user "setting breakpoints...\\n" + | + |# breakpoints + |$breakpoints + | + |# run + |send_user "run program...\\n" + |send "run\\r" + |expect "Breakpoint hit" + | + |# interactions + |$commands""".stripMargin + } + + def main(args: Array[String]): Unit = { + val prog = Gen.parse(args(0)) + // println("--------------------------------") + // println("prog:" + prog) + // println("\n\n\n scrip:") + // println("--------------------------------") + println(Gen.generate(prog)) + } +} diff --git a/compiler/test/debug/test b/compiler/test/debug/test new file mode 100755 index 000000000..603e3c153 --- /dev/null +++ b/compiler/test/debug/test @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +DIR="$( cd "$( dirname "$0" )" && pwd )" + +echo "start debug test..." +for file in tests/debug/*.scala; do + ./bin/dotc $file || exit 1 + ./bin/dotr -d Test > /dev/null & + $DIR/Gen $file > robot + expect robot 2>&1 > /dev/null + + if [[ $? != 0 ]]; then + echo "debug test failed for file $file" + exit 1 + fi + + echo "$file -- success" +done + +echo "debug test success!" + diff --git a/docs/docs/contributing/debug-tests.md b/docs/docs/contributing/debug-tests.md new file mode 100644 index 000000000..b3d65c937 --- /dev/null +++ b/docs/docs/contributing/debug-tests.md @@ -0,0 +1,124 @@ +--- +layout: doc-page +title: Tests for Debuggability +--- + +## Tools Requires + +- JDB +- expect + +Both are usually pre-installed on Mac OS and linux distributions. + +## Debug Manually with JDB + +First, compile the file `tests/debug/while.scala`: + +``` +bin/dotc tests/debug/while.scala +``` + +Second, run the compiled class with debugging enabled (suppose the main class is `Test`): + +``` +bin/dotr -d Test +``` + +Third, start JDB: + +``` +jdb -attach 5005 -sourcepath tests/debug/ +``` + +You can run `help` for commands that supported by JDB. + +## Debug Automatically with Expect + +### 1. Annotate the source code with debug information. + +Following file (`tests/debug/while.scala`) is an example of annoated source code: + +```Scala +object Test { + + def main(args: Array[String]): Unit = { + var a = 1 + 2 + a = a + 3 + a = 4 + 5 // [break] [step: while] + + while (a * 8 < 100) { // [step: a += 1] + a += 1 // [step: while] [cont: print] + } + + print(a) // [break] [cont] + } +} +``` + +The debugging information is annotated as comments to the code in brackets: + +```Scala +val x = f(3) // [break] [next: line=5] +val y = 5 +``` + +1. A JDB command must be wrapped in brackets, like `[step]`. All JDB commands can be used. +2. To check output of JDB for a command, use `[cmd: expect]`. +3. If `expect` is wrapped in double quotes, regex is supported. +4. Break commands are collected and set globally. +5. Other commands will be send to jdb in the order they appear in the source file + +Note that JDB uses line number starts from 1. + +### 2. Generate Expect File + +Now we can run the following command to generate an expect file: + +``` +compiler/test/debug/Gen tests/debug/while.scala > robot +``` + +### 3. Run the Test + +First, compile the file `tests/debug/while.scala`: + +``` +bin/dotc tests/debug/while.scala +``` + +Second, run the compiled class with debugging enabled: + +``` +bin/dotr -d Test +``` + +Finally, run the expect script: + +``` +expect robot +``` + +## Other Tips + +### Adding a New Test + +Just put the annotated source file under `tests/debug/`, it will be automatically +run by the test infrastructure. + +### Run All Debug Tests + +``` +./compiler/test/debug/test +``` + +### Debug a Debug Test + +If there is any problem with a debug test, first check if the problematic +test work correctly with JDB without automation. + +Then, uncomment the following line in the generated expect file to check the +output of expect: + +``` +# exp_internal 1 +``` diff --git a/tests/debug/for.scala b/tests/debug/for.scala new file mode 100644 index 000000000..b2287a988 --- /dev/null +++ b/tests/debug/for.scala @@ -0,0 +1,16 @@ +object Test { + def main(args: Array[String]): Unit = { + val b = 8 * 9 // [break] [step: f()] + f() // [step: val a] + 20 + b + print(b) + } + + def f(): Unit = { + val a = for (i <- 1 to 5; j <- 10 to 20) // [cont] + yield (i, j) // Error: incorrect reaching this line + + for (i <- 1 to 5; j <- 10 to 20) + println(i + j) // TODO: i is renamed to i$2 --> reduce debuggability + } +}
\ No newline at end of file diff --git a/tests/debug/function.scala b/tests/debug/function.scala new file mode 100644 index 000000000..644344414 --- /dev/null +++ b/tests/debug/function.scala @@ -0,0 +1,14 @@ +object Test { + def main(args: Array[String]): Unit = { + val a = 1 + 2 + val b = a * 9 // [break] [step: plus] [step: c = plus] + val plus = (x: Int, y: Int) => { // [cont: x * x] + val a = x * x // [break] [step: y * y] + val b = y * y // [step: a + b] + a + b // [next] [next] + } + val c = plus(a, b) // [next: print] + print(c) // [cont] + } + +} diff --git a/tests/debug/if.scala b/tests/debug/if.scala new file mode 100644 index 000000000..af598c1cd --- /dev/null +++ b/tests/debug/if.scala @@ -0,0 +1,20 @@ +object Test { + + def main(args: Array[String]): Unit = { + var a = 1 + 2 // [break] [step: a + 3] + a = a + 3 // [step: 4 + 5] + a = 4 + 5 // [step: if] + + if (a * 8 > 20) // [step: 9 * 9] + a = 9 * 9 // [step: if] + else + a = 34 * 23 + + if (a * 8 < 20) // [step: 34 * 23] + a = 9 * 9 + else + a = 34 * 23 // [step: print] + + print(a) + } +} diff --git a/tests/debug/method.scala b/tests/debug/method.scala new file mode 100644 index 000000000..9489b0088 --- /dev/null +++ b/tests/debug/method.scala @@ -0,0 +1,14 @@ +object Test { + def main(args: Array[String]): Unit = { + val a = 1 + 2 // [break] [step: a * 9] + val b = a * 9 // [step: plus] + val c = plus(a, b) // [step: x * x] + print(c) + } + + def plus(x: Int, y: Int) = { + val a = x * x // [step: y * y] + val b = y * y // [step: a + b] + a + b // [step: plus] [step: print] [cont] + } +} diff --git a/tests/debug/nested-method.scala b/tests/debug/nested-method.scala new file mode 100644 index 000000000..fcc326ccb --- /dev/null +++ b/tests/debug/nested-method.scala @@ -0,0 +1,15 @@ +object Test { + def main(args: Array[String]): Unit = { + val a = 1 + 2 // [break] [step: a * 9] + val b = a * 9 // [step: plus] [step: x * x] + + def plus(x: Int, y: Int) = { + val a = x * x // [step: y * y] + val b = y * y // [step: a + b] + a + b // [step: plus] + } + + val c = plus(a, b) // [step: print] [cont] + print(c) + } +}
\ No newline at end of file diff --git a/tests/debug/sequence.scala b/tests/debug/sequence.scala new file mode 100644 index 000000000..a6c1e9018 --- /dev/null +++ b/tests/debug/sequence.scala @@ -0,0 +1,11 @@ +object Test { + def main(args: Array[String]): Unit = { + var a = 1 + 2 // [break] [step: a + 3] + a = a + 3 // [step: 4 + 5] + a = 4 + 5 // [step: a * 8] + a = a * 8 // [step: 9 * 9] + a = 9 * 9 // [step: 34 * 23] + a = 34 * 23 // [step: print] + print(a) // [cont] + } +}
\ No newline at end of file diff --git a/tests/debug/tailrec.scala b/tests/debug/tailrec.scala new file mode 100644 index 000000000..f79514fa3 --- /dev/null +++ b/tests/debug/tailrec.scala @@ -0,0 +1,17 @@ +object Test { + def fact(x: Int): Int = { + if (x == 0) + 1 + else + x * fact(x - 1) // TODO: incorrect this line when x = 0 + } + + + def main(args: Array[String]): Unit = { + val a = 1 + 2 + val b = a * 9 // [break] [step: fact] + val c = fact(a) // [step: x == 0] [step: fact(x - 1)] [step: x == 0] [cont] + fact(0) // [break] [step: x == 0] [step: 1] [step: fact(x - 1)] [step: print] + print(c) // [cont] + } +}
\ No newline at end of file diff --git a/tests/debug/while.scala b/tests/debug/while.scala new file mode 100644 index 000000000..0e5f8f8b0 --- /dev/null +++ b/tests/debug/while.scala @@ -0,0 +1,14 @@ +object Test { + + def main(args: Array[String]): Unit = { + var a = 1 + 2 + a = a + 3 + a = 4 + 5 // [break] [step: while] + + while (a * 8 < 100) { // [step: a += 1] + a += 1 // [step: while] [cont: print] + } + + print(a) // [break] [cont] + } +} |