From 57624a3ad08cbb264059f8b305ed0c4c453c25be Mon Sep 17 00:00:00 2001 From: Tim Chevalier Date: Thu, 18 Oct 2012 12:20:18 -0700 Subject: [PATCH] Preliminary support for labeled break/continue for `loop`s This patch adds preliminary middle-end support (liveness and trans) for breaks and `loop`s to `loop` constructs that have labels. while and for loops can't have labels yet. Progress on #2216 --- src/libsyntax/print/pprust.rs | 5 +- src/rustc/middle/liveness.rs | 192 ++++++++++++++++++-------- src/rustc/middle/trans/base.rs | 7 +- src/rustc/middle/trans/common.rs | 1 + src/rustc/middle/trans/controlflow.rs | 32 +++-- src/rustc/middle/trans/expr.rs | 14 +- src/rustc/middle/typeck.rs | 2 +- src/rustc/middle/typeck/check.rs | 26 +++- src/rustc/util/common.rs | 29 ++-- src/test/run-pass/issue-2216.rs | 22 +++ 10 files changed, 232 insertions(+), 98 deletions(-) create mode 100644 src/test/run-pass/issue-2216.rs diff --git a/src/libsyntax/print/pprust.rs b/src/libsyntax/print/pprust.rs index 3fc2bcda90182..8b579e8998433 100644 --- a/src/libsyntax/print/pprust.rs +++ b/src/libsyntax/print/pprust.rs @@ -1168,7 +1168,10 @@ fn print_expr(s: ps, &&expr: @ast::expr) { ast::expr_loop(blk, opt_ident) => { head(s, ~"loop"); space(s.s); - opt_ident.iter(|ident| {print_ident(s, *ident); space(s.s)}); + opt_ident.iter(|ident| { + print_ident(s, *ident); + word_space(s, ~":"); + }); print_block(s, blk); } ast::expr_match(expr, arms) => { diff --git a/src/rustc/middle/liveness.rs b/src/rustc/middle/liveness.rs index 89d5c842a9f62..184d694a50c5c 100644 --- a/src/rustc/middle/liveness.rs +++ b/src/rustc/middle/liveness.rs @@ -95,9 +95,9 @@ use dvec::DVec; use std::map::HashMap; use syntax::{visit, ast_util}; -use syntax::print::pprust::{expr_to_str}; +use syntax::print::pprust::{expr_to_str, block_to_str}; use visit::vt; -use syntax::codemap::span; +use syntax::codemap::{span, span_to_str}; use syntax::ast::*; use io::WriterUtil; use capture::{cap_move, cap_drop, cap_copy, cap_ref}; @@ -167,6 +167,16 @@ impl LiveNodeKind : cmp::Eq { pure fn ne(other: &LiveNodeKind) -> bool { !self.eq(other) } } +fn live_node_kind_to_str(lnk: LiveNodeKind, cx: ty::ctxt) -> ~str { + let cm = cx.sess.codemap; + match lnk { + FreeVarNode(s) => fmt!("Free var node [%s]", span_to_str(s, cm)), + ExprNode(s) => fmt!("Expr node [%s]", span_to_str(s, cm)), + VarDefNode(s) => fmt!("Var def node [%s]", span_to_str(s, cm)), + ExitNode => ~"Exit node" + } +} + fn check_crate(tcx: ty::ctxt, method_map: typeck::method_map, crate: @crate) -> last_use_map { @@ -277,8 +287,8 @@ fn IrMaps(tcx: ty::ctxt, method_map: typeck::method_map, tcx: tcx, method_map: method_map, last_use_map: last_use_map, - num_live_nodes: 0u, - num_vars: 0u, + num_live_nodes: 0, + num_vars: 0, live_node_map: HashMap(), variable_map: HashMap(), capture_map: HashMap(), @@ -291,9 +301,10 @@ impl IrMaps { fn add_live_node(lnk: LiveNodeKind) -> LiveNode { let ln = LiveNode(self.num_live_nodes); self.lnks.push(lnk); - self.num_live_nodes += 1u; + self.num_live_nodes += 1; - debug!("%s is of kind %?", ln.to_str(), lnk); + debug!("%s is of kind %s", ln.to_str(), + live_node_kind_to_str(lnk, self.tcx)); ln } @@ -308,7 +319,7 @@ impl IrMaps { fn add_variable(vk: VarKind) -> Variable { let v = Variable(self.num_vars); self.var_kinds.push(vk); - self.num_vars += 1u; + self.num_vars += 1; match vk { Local(LocalInfo {id:node_id, _}) | @@ -491,6 +502,10 @@ fn visit_expr(expr: @expr, &&self: @IrMaps, vt: vt<@IrMaps>) { } expr_fn(_, _, _, cap_clause) | expr_fn_block(_, _, cap_clause) => { + // Interesting control flow (for loops can contain labeled + // breaks or continues) + self.add_live_node_for_node(expr.id, ExprNode(expr.span)); + // Make a live_node for each captured variable, with the span // being the location that the variable is used. This results // in better error messages than just pointing at the closure @@ -571,14 +586,22 @@ const ACC_READ: uint = 1u; const ACC_WRITE: uint = 2u; const ACC_USE: uint = 4u; +type LiveNodeMap = HashMap; + struct Liveness { tcx: ty::ctxt, ir: @IrMaps, s: Specials, successors: ~[mut LiveNode], users: ~[mut users], - mut break_ln: LiveNode, - mut cont_ln: LiveNode, + // The list of node IDs for the nested loop scopes + // we're in. + mut loop_scope: @DVec, + // mappings from loop node ID to LiveNode + // ("break" label should map to loop node ID, + // it probably doesn't now) + break_ln: LiveNodeMap, + cont_ln: LiveNodeMap } fn Liveness(ir: @IrMaps, specials: Specials) -> Liveness { @@ -594,8 +617,9 @@ fn Liveness(ir: @IrMaps, specials: Specials) -> Liveness { vec::to_mut( vec::from_elem(ir.num_live_nodes * ir.num_vars, invalid_users())), - break_ln: invalid_node(), - cont_ln: invalid_node() + loop_scope: @DVec(), + break_ln: HashMap(), + cont_ln: HashMap() } } @@ -691,6 +715,9 @@ impl Liveness { if reader.is_valid() {Some((*self.ir).lnk(reader))} else {None} } + /* + Is this variable live on entry to any of its successor nodes? + */ fn live_on_exit(ln: LiveNode, var: Variable) -> Option { @@ -717,8 +744,8 @@ impl Liveness { } fn indices(ln: LiveNode, op: fn(uint)) { - let node_base_idx = self.idx(ln, Variable(0u)); - for uint::range(0u, self.ir.num_vars) |var_idx| { + let node_base_idx = self.idx(ln, Variable(0)); + for uint::range(0, self.ir.num_vars) |var_idx| { op(node_base_idx + var_idx) } } @@ -735,8 +762,8 @@ impl Liveness { fn write_vars(wr: io::Writer, ln: LiveNode, test: fn(uint) -> LiveNode) { - let node_base_idx = self.idx(ln, Variable(0u)); - for uint::range(0u, self.ir.num_vars) |var_idx| { + let node_base_idx = self.idx(ln, Variable(0)); + for uint::range(0, self.ir.num_vars) |var_idx| { let idx = node_base_idx + var_idx; if test(idx).is_valid() { wr.write_str(~" "); @@ -745,6 +772,28 @@ impl Liveness { } } + fn find_loop_scope(opt_label: Option, id: node_id, sp: span) + -> node_id { + match opt_label { + Some(_) => // Refers to a labeled loop. Use the results of resolve + // to find with one + match self.tcx.def_map.find(id) { + Some(def_label(loop_id)) => loop_id, + _ => self.tcx.sess.span_bug(sp, ~"Label on break/loop \ + doesn't refer to a loop") + }, + None => + // Vanilla 'break' or 'loop', so use the enclosing + // loop scope + if self.loop_scope.len() == 0 { + self.tcx.sess.span_bug(sp, ~"break outside loop"); + } + else { + self.loop_scope.last() + } + } + } + fn ln_str(ln: LiveNode) -> ~str { do io::with_str_writer |wr| { wr.write_str(~"[ln("); @@ -833,18 +882,18 @@ impl Liveness { let idx = self.idx(ln, var); let user = &mut self.users[idx]; - if (acc & ACC_WRITE) != 0u { + if (acc & ACC_WRITE) != 0 { user.reader = invalid_node(); user.writer = ln; } // Important: if we both read/write, must do read second // or else the write will override. - if (acc & ACC_READ) != 0u { + if (acc & ACC_READ) != 0 { user.reader = ln; } - if (acc & ACC_USE) != 0u { + if (acc & ACC_USE) != 0 { self.users[idx].used = true; } @@ -858,10 +907,13 @@ impl Liveness { // if there is a `break` or `again` at the top level, then it's // effectively a return---this only occurs in `for` loops, // where the body is really a closure. + + debug!("compute: using id for block, %s", block_to_str(body, + self.tcx.sess.intr())); + let entry_ln: LiveNode = - self.with_loop_nodes(self.s.exit_ln, self.s.exit_ln, || { - self.propagate_through_fn_block(decl, body) - }); + self.with_loop_nodes(body.node.id, self.s.exit_ln, self.s.exit_ln, + || { self.propagate_through_fn_block(decl, body) }); // hack to skip the loop unless debug! is enabled: debug!("^^ liveness computation results for body %d (entry=%s)", @@ -972,6 +1024,9 @@ impl Liveness { } fn propagate_through_expr(expr: @expr, succ: LiveNode) -> LiveNode { + debug!("propagate_through_expr: %s", + expr_to_str(expr, self.tcx.sess.intr())); + match expr.node { // Interesting cases with control flow or which gen/kill @@ -983,16 +1038,27 @@ impl Liveness { self.propagate_through_expr(e, succ) } - expr_fn(*) | expr_fn_block(*) => { - // the construction of a closure itself is not important, - // but we have to consider the closed over variables. - let caps = (*self.ir).captures(expr); - do (*caps).foldr(succ) |cap, succ| { - self.init_from_succ(cap.ln, succ); - let var = self.variable(cap.var_nid, expr.span); - self.acc(cap.ln, var, ACC_READ | ACC_USE); - cap.ln - } + expr_fn(_, _, blk, _) | expr_fn_block(_, blk, _) => { + debug!("%s is an expr_fn or expr_fn_block", + expr_to_str(expr, self.tcx.sess.intr())); + + /* + The next-node for a break is the successor of the entire + loop. The next-node for a continue is the top of this loop. + */ + self.with_loop_nodes(blk.node.id, succ, + self.live_node(expr.id, expr.span), || { + + // the construction of a closure itself is not important, + // but we have to consider the closed over variables. + let caps = (*self.ir).captures(expr); + do (*caps).foldr(succ) |cap, succ| { + self.init_from_succ(cap.ln, succ); + let var = self.variable(cap.var_nid, expr.span); + self.acc(cap.ln, var, ACC_READ | ACC_USE); + cap.ln + } + }) } expr_if(cond, then, els) => { @@ -1021,6 +1087,8 @@ impl Liveness { self.propagate_through_loop(expr, Some(cond), blk, succ) } + // Note that labels have been resolved, so we don't need to look + // at the label ident expr_loop(blk, _) => { self.propagate_through_loop(expr, None, blk, succ) } @@ -1062,29 +1130,31 @@ impl Liveness { } expr_break(opt_label) => { - if !self.break_ln.is_valid() { - self.tcx.sess.span_bug( - expr.span, ~"break with invalid break_ln"); - } + // Find which label this break jumps to + let sc = self.find_loop_scope(opt_label, expr.id, expr.span); - if opt_label.is_some() { - self.tcx.sess.span_unimpl(expr.span, ~"labeled break"); - } + // Now that we know the label we're going to, + // look it up in the break loop nodes table - self.break_ln + match self.break_ln.find(sc) { + Some(b) => b, + None => self.tcx.sess.span_bug(expr.span, + ~"Break to unknown label") + } } expr_again(opt_label) => { - if !self.cont_ln.is_valid() { - self.tcx.sess.span_bug( - expr.span, ~"cont with invalid cont_ln"); - } + // Find which label this expr continues to to + let sc = self.find_loop_scope(opt_label, expr.id, expr.span); - if opt_label.is_some() { - self.tcx.sess.span_unimpl(expr.span, ~"labeled again"); - } + // Now that we know the label we're going to, + // look it up in the continue loop nodes table - self.cont_ln + match self.cont_ln.find(sc) { + Some(b) => b, + None => self.tcx.sess.span_bug(expr.span, + ~"Loop to unknown label") + } } expr_move(l, r) | expr_assign(l, r) => { @@ -1314,6 +1384,7 @@ impl Liveness { */ + // first iteration: let mut first_merge = true; let ln = self.live_node(expr.id, expr.span); @@ -1325,8 +1396,11 @@ impl Liveness { self.merge_from_succ(ln, succ, first_merge); first_merge = false; } + debug!("propagate_through_loop: using id for loop body %d %s", + expr.id, block_to_str(body, self.tcx.sess.intr())); + let cond_ln = self.propagate_through_opt_expr(cond, ln); - let body_ln = self.with_loop_nodes(succ, ln, || { + let body_ln = self.with_loop_nodes(expr.id, succ, ln, || { self.propagate_through_block(body, cond_ln) }); @@ -1334,7 +1408,8 @@ impl Liveness { while self.merge_from_succ(ln, body_ln, first_merge) { first_merge = false; assert cond_ln == self.propagate_through_opt_expr(cond, ln); - assert body_ln == self.with_loop_nodes(succ, ln, || { + assert body_ln == self.with_loop_nodes(expr.id, succ, ln, + || { self.propagate_through_block(body, cond_ln) }); } @@ -1342,15 +1417,16 @@ impl Liveness { cond_ln } - fn with_loop_nodes(break_ln: LiveNode, + fn with_loop_nodes(loop_node_id: node_id, + break_ln: LiveNode, cont_ln: LiveNode, f: fn() -> R) -> R { - let bl = self.break_ln, cl = self.cont_ln; - self.break_ln = break_ln; - self.cont_ln = cont_ln; - let r <- f(); - self.break_ln = bl; - self.cont_ln = cl; + debug!("with_loop_nodes: %d %u", loop_node_id, *break_ln); + self.loop_scope.push(loop_node_id); + self.break_ln.insert(loop_node_id, break_ln); + self.cont_ln.insert(loop_node_id, cont_ln); + let r = f(); + self.loop_scope.pop(); move r } } @@ -1526,6 +1602,10 @@ impl @Liveness { } } + /* + Checks whether is live on entry to any of the successors of . + If it is, report an error. + */ fn check_move_from_var(span: span, ln: LiveNode, var: Variable) { debug!("check_move_from_var(%s, %s)", ln.to_str(), var.to_str()); diff --git a/src/rustc/middle/trans/base.rs b/src/rustc/middle/trans/base.rs index 4c9a006007e75..d2cde0420fb64 100644 --- a/src/rustc/middle/trans/base.rs +++ b/src/rustc/middle/trans/base.rs @@ -1050,7 +1050,7 @@ fn new_block(cx: fn_ctxt, parent: Option, +kind: block_kind, } fn simple_block_scope() -> block_kind { - block_scope({loop_break: None, mut cleanups: ~[], + block_scope({loop_break: None, loop_label: None, mut cleanups: ~[], mut cleanup_paths: ~[], mut landing_pad: None}) } @@ -1067,10 +1067,11 @@ fn scope_block(bcx: block, n, opt_node_info); } -fn loop_scope_block(bcx: block, loop_break: block, n: ~str, - opt_node_info: Option) -> block { +fn loop_scope_block(bcx: block, loop_break: block, loop_label: Option, + n: ~str, opt_node_info: Option) -> block { return new_block(bcx.fcx, Some(bcx), block_scope({ loop_break: Some(loop_break), + loop_label: loop_label, mut cleanups: ~[], mut cleanup_paths: ~[], mut landing_pad: None diff --git a/src/rustc/middle/trans/common.rs b/src/rustc/middle/trans/common.rs index 931e82d5be985..be8a4e4418e3c 100644 --- a/src/rustc/middle/trans/common.rs +++ b/src/rustc/middle/trans/common.rs @@ -445,6 +445,7 @@ enum block_kind { type scope_info = { loop_break: Option, + loop_label: Option, // A list of functions that must be run at when leaving this // block, cleaning up any variables that were introduced in the // block. diff --git a/src/rustc/middle/trans/controlflow.rs b/src/rustc/middle/trans/controlflow.rs index ce32cd0a2dd1e..b1ca93f8f8a00 100644 --- a/src/rustc/middle/trans/controlflow.rs +++ b/src/rustc/middle/trans/controlflow.rs @@ -113,7 +113,9 @@ fn trans_while(bcx: block, cond: @ast::expr, body: ast::blk) // | body_bcx_out --+ // next_bcx - let loop_bcx = loop_scope_block(bcx, next_bcx, ~"`while`", body.info()); + // tjc: while should have labels... + let loop_bcx = loop_scope_block(bcx, next_bcx, None, ~"`while`", + body.info()); let cond_bcx_in = scope_block(loop_bcx, cond.info(), ~"while loop cond"); let body_bcx_in = scope_block(loop_bcx, body.info(), ~"while loop body"); Br(bcx, loop_bcx.llbb); @@ -133,10 +135,11 @@ fn trans_while(bcx: block, cond: @ast::expr, body: ast::blk) return next_bcx; } -fn trans_loop(bcx:block, body: ast::blk) -> block { +fn trans_loop(bcx:block, body: ast::blk, opt_label: Option) -> block { let _icx = bcx.insn_ctxt("trans_loop"); let next_bcx = sub_block(bcx, ~"next"); - let body_bcx_in = loop_scope_block(bcx, next_bcx, ~"`loop`", body.info()); + let body_bcx_in = loop_scope_block(bcx, next_bcx, opt_label, ~"`loop`", + body.info()); Br(bcx, body_bcx_in.llbb); let body_bcx_out = trans_block(body_bcx_in, body, expr::Ignore); cleanup_and_Br(body_bcx_out, body_bcx_in, body_bcx_in.llbb); @@ -201,7 +204,7 @@ fn trans_log(log_ex: @ast::expr, } } -fn trans_break_cont(bcx: block, to_end: bool) +fn trans_break_cont(bcx: block, opt_label: Option, to_end: bool) -> block { let _icx = bcx.insn_ctxt("trans_break_cont"); // Locate closest loop block, outputting cleanup as we go. @@ -209,13 +212,22 @@ fn trans_break_cont(bcx: block, to_end: bool) let mut target; loop { match unwind.kind { - block_scope({loop_break: Some(brk), _}) => { + block_scope({loop_break: Some(brk), loop_label: l, _}) => { + // If we're looking for a labeled loop, check the label... target = if to_end { brk } else { unwind }; - break; + match opt_label { + Some(desired) => match l { + Some(actual) if actual == desired => break, + // If it doesn't match the one we want, + // don't break + _ => () + }, + None => break + } } _ => () } @@ -235,12 +247,12 @@ fn trans_break_cont(bcx: block, to_end: bool) return bcx; } -fn trans_break(bcx: block) -> block { - return trans_break_cont(bcx, true); +fn trans_break(bcx: block, label_opt: Option) -> block { + return trans_break_cont(bcx, label_opt, true); } -fn trans_cont(bcx: block) -> block { - return trans_break_cont(bcx, false); +fn trans_cont(bcx: block, label_opt: Option) -> block { + return trans_break_cont(bcx, label_opt, false); } fn trans_ret(bcx: block, e: Option<@ast::expr>) -> block { diff --git a/src/rustc/middle/trans/expr.rs b/src/rustc/middle/trans/expr.rs index 30bea1376c000..b0702c80b39bf 100644 --- a/src/rustc/middle/trans/expr.rs +++ b/src/rustc/middle/trans/expr.rs @@ -410,16 +410,10 @@ fn trans_rvalue_stmt_unadjusted(bcx: block, expr: @ast::expr) -> block { match expr.node { ast::expr_break(label_opt) => { - if label_opt.is_some() { - bcx.tcx().sess.span_unimpl(expr.span, ~"labeled break"); - } - return controlflow::trans_break(bcx); + return controlflow::trans_break(bcx, label_opt); } ast::expr_again(label_opt) => { - if label_opt.is_some() { - bcx.tcx().sess.span_unimpl(expr.span, ~"labeled again"); - } - return controlflow::trans_cont(bcx); + return controlflow::trans_cont(bcx, label_opt); } ast::expr_ret(ex) => { return controlflow::trans_ret(bcx, ex); @@ -436,8 +430,8 @@ fn trans_rvalue_stmt_unadjusted(bcx: block, expr: @ast::expr) -> block { ast::expr_while(cond, body) => { return controlflow::trans_while(bcx, cond, body); } - ast::expr_loop(body, _) => { - return controlflow::trans_loop(bcx, body); + ast::expr_loop(body, opt_label) => { + return controlflow::trans_loop(bcx, body, opt_label); } ast::expr_assign(dst, src) => { let src_datum = unpack_datum!(bcx, trans_to_datum(bcx, src)); diff --git a/src/rustc/middle/typeck.rs b/src/rustc/middle/typeck.rs index 8d10343d78ee0..90aacb4b7ce61 100644 --- a/src/rustc/middle/typeck.rs +++ b/src/rustc/middle/typeck.rs @@ -46,7 +46,7 @@ use syntax::ast_map::node_id_to_str; use syntax::ast_util::{local_def, respan, split_trait_methods}; use syntax::visit; use metadata::csearch; -use util::common::may_break; +use util::common::{block_query, loop_query}; use syntax::codemap::span; use pat_util::{pat_is_variant, pat_id_map, PatIdMap}; use middle::ty; diff --git a/src/rustc/middle/typeck/check.rs b/src/rustc/middle/typeck/check.rs index 6de249ebc68f2..c68f87020a709 100644 --- a/src/rustc/middle/typeck/check.rs +++ b/src/rustc/middle/typeck/check.rs @@ -1665,7 +1665,7 @@ fn check_expr_with_unifier(fcx: @fn_ctxt, ast::expr_loop(body, _) => { check_block_no_value(fcx, body); fcx.write_ty(id, ty::mk_nil(tcx)); - bot = !may_break(body); + bot = !may_break(tcx, expr.id, body); } ast::expr_match(discrim, arms) => { bot = alt::check_alt(fcx, expr, discrim, arms); @@ -2544,6 +2544,30 @@ fn ast_expr_vstore_to_vstore(fcx: @fn_ctxt, e: @ast::expr, n: uint, } } +// Returns true if b contains a break that can exit from b +fn may_break(cx: ty::ctxt, id: ast::node_id, b: ast::blk) -> bool { + // First: is there an unlabeled break immediately + // inside the loop? + (loop_query(b, |e| { + match e { + ast::expr_break(_) => true, + _ => false + } + })) || + // Second: is there a labeled break with label + // nested anywhere inside the loop? + (block_query(b, |e| { + match e.node { + ast::expr_break(Some(_)) => + match cx.def_map.find(e.id) { + Some(ast::def_label(loop_id)) if id == loop_id => true, + _ => false, + }, + _ => false + } + })) +} + fn check_bounds_are_used(ccx: @crate_ctxt, span: span, tps: ~[ast::ty_param], diff --git a/src/rustc/util/common.rs b/src/rustc/util/common.rs index 123905adba11b..0448b022602a6 100644 --- a/src/rustc/util/common.rs +++ b/src/rustc/util/common.rs @@ -58,22 +58,19 @@ fn loop_query(b: ast::blk, p: fn@(ast::expr_) -> bool) -> bool { return *rs; } -fn has_nonlocal_exits(b: ast::blk) -> bool { - do loop_query(b) |e| { - match e { - ast::expr_break(_) | ast::expr_again(_) => true, - _ => false - } - } -} - -fn may_break(b: ast::blk) -> bool { - do loop_query(b) |e| { - match e { - ast::expr_break(_) => true, - _ => false - } - } +// Takes a predicate p, returns true iff p is true for any subexpressions +// of b -- skipping any inner loops (loop, while, loop_body) +fn block_query(b: ast::blk, p: fn@(@ast::expr) -> bool) -> bool { + let rs = @mut false; + let visit_expr = + |e: @ast::expr, &&flag: @mut bool, v: visit::vt<@mut bool>| { + *flag |= p(e); + visit::visit_expr(e, flag, v) + }; + let v = visit::mk_vt(@{visit_expr: visit_expr + ,.. *visit::default_visitor()}); + visit::visit_block(b, rs, v); + return *rs; } fn local_rhs_span(l: @ast::local, def: span) -> span { diff --git a/src/test/run-pass/issue-2216.rs b/src/test/run-pass/issue-2216.rs new file mode 100644 index 0000000000000..11815db176327 --- /dev/null +++ b/src/test/run-pass/issue-2216.rs @@ -0,0 +1,22 @@ +fn main() { + let mut x = 0; + + loop foo: { + loop bar: { + loop quux: { + if 1 == 2 { + break foo; + } + else { + break bar; + } + } + loop foo; + } + x = 42; + break; + } + + error!("%?", x); + assert(x == 42); +} \ No newline at end of file