From c8e858481aeec9b98fe60008bf612c67cc5d18dc Mon Sep 17 00:00:00 2001 From: Simon Ochsenreither Date: Tue, 23 Aug 2022 01:10:01 +0200 Subject: [PATCH] lang: unified condition expressions - Allow multiple if-cases. - Conditions can be split into a common discriminator and continuations in individual if-cases. Though not strictly required, we use a token (`...`) to explicitly indicate this construct. --- dora-parser/src/ast.rs | 18 +++-- dora-parser/src/ast/dump.rs | 24 +++--- dora-parser/src/ast/visit.rs | 9 ++- dora-parser/src/parser.rs | 110 +++++++++++++++++++++++++-- dora/src/language/fctbodyck/body.rs | 100 +++++++++++++++++------- dora/src/language/fctbodyck/tests.rs | 2 +- dora/src/language/generator.rs | 71 ++++++++++++----- dora/src/language/generator_tests.rs | 16 ++-- dora/src/language/returnck.rs | 4 +- tests/{ => if}/if1.dora | 0 tests/if/if2.dora | 7 ++ tests/if/if3.dora | 7 ++ tests/if/if4.dora | 8 ++ tests/if/if5.dora | 8 ++ tests/if/if6.dora | 13 ++++ tests/if/if7.dora | 14 ++++ tests/if/if8.dora | 18 +++++ 17 files changed, 351 insertions(+), 78 deletions(-) rename tests/{ => if}/if1.dora (100%) create mode 100644 tests/if/if2.dora create mode 100644 tests/if/if3.dora create mode 100644 tests/if/if4.dora create mode 100644 tests/if/if5.dora create mode 100644 tests/if/if6.dora create mode 100644 tests/if/if7.dora create mode 100644 tests/if/if8.dora diff --git a/dora-parser/src/ast.rs b/dora-parser/src/ast.rs index d57176efd..307a59514 100644 --- a/dora-parser/src/ast.rs +++ b/dora-parser/src/ast.rs @@ -1257,8 +1257,8 @@ impl Expr { id: NodeId, pos: Position, span: Span, - cond: Box, - then_block: Box, + cond_head: Box, + branches: Vec, else_block: Option>, ) -> Expr { Expr::If(ExprIfType { @@ -1266,8 +1266,8 @@ impl Expr { pos, span, - cond, - then_block, + cond_head, + branches, else_block, }) } @@ -1897,11 +1897,17 @@ pub struct ExprIfType { pub pos: Position, pub span: Span, - pub cond: Box, - pub then_block: Box, + pub cond_head: Box, + pub branches: Vec, pub else_block: Option>, } +#[derive(Clone, Debug)] +pub struct Branch { + pub cond_tail: Option>, + pub then_block: Box, +} + #[derive(Clone, Debug)] pub struct ExprTupleType { pub id: NodeId, diff --git a/dora-parser/src/ast/dump.rs b/dora-parser/src/ast/dump.rs index a21a9edda..cd28d287f 100644 --- a/dora-parser/src/ast/dump.rs +++ b/dora-parser/src/ast/dump.rs @@ -516,16 +516,22 @@ impl<'a> AstDumper<'a> { self.indent(|d| { d.indent(|d| { - d.dump_expr(&expr.cond); - }); - dump!(d, "then"); - d.indent(|d| { - d.dump_expr(&expr.then_block); - }); - dump!(d, "else"); - d.indent(|d| { - d.dump_expr(&expr.then_block); + d.dump_expr(&expr.cond_head.expr.as_ref().unwrap()); }); + for branch in &expr.branches { + dump!(d, "then"); + d.indent(|d| { + if let Some(cond_tail) = &branch.cond_tail { + d.dump_expr(cond_tail); + } + }); + } + if let Some(else_block) = &expr.else_block { + dump!(d, "else"); + d.indent(|d| { + d.dump_expr(else_block); + }); + } }); } diff --git a/dora-parser/src/ast/visit.rs b/dora-parser/src/ast/visit.rs index 5a16efc26..d75c5e8d2 100644 --- a/dora-parser/src/ast/visit.rs +++ b/dora-parser/src/ast/visit.rs @@ -318,8 +318,13 @@ pub fn walk_expr(v: &mut V, e: &Expr) { } Expr::If(ref value) => { - v.visit_expr(&value.cond); - v.visit_expr(&value.then_block); + v.visit_stmt(&Stmt::Let(value.cond_head.as_ref().clone())); + for branch in &value.branches { + if let Some(cond_tail) = &branch.cond_tail { + v.visit_expr(cond_tail); + } + v.visit_expr(&branch.then_block); + } if let Some(ref b) = value.else_block { v.visit_expr(b); diff --git a/dora-parser/src/parser.rs b/dora-parser/src/parser.rs index 6397c4ca7..cb90e8b76 100644 --- a/dora-parser/src/parser.rs +++ b/dora-parser/src/parser.rs @@ -1278,7 +1278,7 @@ impl<'a> Parser<'a> { let cond = self.parse_expression()?; - let then_block = self.parse_block()?; + let branches = self.parse_branches()?; let else_block = if self.token.is(TokenKind::Else) { self.advance_token()?; @@ -1294,16 +1294,52 @@ impl<'a> Parser<'a> { let span = self.span_from(start); + let let_pattern = LetPattern::Ident(LetIdentType { + id: self.generate_id(), + pos, + span, + mutable: false, + name: self.interner.intern(TokenKind::DotDotDot.name()), + }); + let let_condition = StmtLetType { + id: self.generate_id(), + pos, + span, + pattern: Box::from(let_pattern), + data_type: None, + expr: Some(cond), + }; Ok(Box::new(Expr::create_if( self.generate_id(), pos, span, - cond, - then_block, + Box::from(let_condition), + branches, else_block, ))) } + fn parse_branches(&mut self) -> Result, ParseErrorAndPos> { + let mut branches = Vec::new(); + if self.token.is(TokenKind::DotDotDot) { + while self.token.is(TokenKind::DotDotDot) { + let cond_tail = Some(self.parse_expression()?); + let then_block = self.parse_block()?; + branches.push(Branch { + cond_tail, + then_block, + }) + } + } else { + let then_block = self.parse_block()?; + branches.push(Branch { + cond_tail: None, + then_block, + }) + } + Ok(branches) + } + fn parse_match(&mut self) -> ExprResult { let start = self.token.span.start(); let pos = self.expect_token(TokenKind::Match)?.position; @@ -1718,6 +1754,7 @@ impl<'a> Parser<'a> { TokenKind::LParen => self.parse_parentheses(), TokenKind::LBrace => self.parse_block(), TokenKind::If => self.parse_if(), + TokenKind::DotDotDot => self.parse_dotdotdot(), TokenKind::LitChar(_) => self.parse_lit_char(), TokenKind::LitInt(_, _, _) => self.parse_lit_int(), TokenKind::LitFloat(_, _) => self.parse_lit_float(), @@ -1734,6 +1771,20 @@ impl<'a> Parser<'a> { } } + fn parse_dotdotdot(&mut self) -> ExprResult { + let pos = self.token.position; + let span = self.token.span; + self.expect_token(TokenKind::DotDotDot)?; + + Ok(Box::new(Expr::create_ident( + self.generate_id(), + pos, + span, + self.interner.intern(TokenKind::DotDotDot.name()), + None, + ))) + } + fn parse_identifier(&mut self) -> ExprResult { let pos = self.token.position; let span = self.token.span; @@ -2783,7 +2834,7 @@ mod tests { let (expr, _) = parse_expr("if true { 2; } else { 3; }"); let ifexpr = expr.to_if().unwrap(); - assert!(ifexpr.cond.is_lit_bool()); + assert!(ifexpr.cond_head.expr.as_ref().unwrap().is_lit_bool()); assert!(ifexpr.else_block.is_some()); } @@ -2792,7 +2843,7 @@ mod tests { let (expr, _) = parse_expr("if true { 2; }"); let ifexpr = expr.to_if().unwrap(); - assert!(ifexpr.cond.is_lit_bool()); + assert!(ifexpr.cond_head.expr.as_ref().unwrap().is_lit_bool()); assert!(ifexpr.else_block.is_none()); } @@ -3148,7 +3199,7 @@ mod tests { fn parse_struct_lit_if() { let (expr, _) = parse_expr("if i < n { }"); let ifexpr = expr.to_if().unwrap(); - let bin = ifexpr.cond.to_bin().unwrap(); + let bin = ifexpr.cond_head.expr.as_ref().unwrap().to_bin().unwrap(); assert!(bin.lhs.is_ident()); assert!(bin.rhs.is_ident()); @@ -3481,6 +3532,53 @@ mod tests { ); } + #[test] + fn parse_if_pattern() { + let (expr, _) = parse_expr( + "if true + ... == 5 { \"\" } + ... == 6.0 { '2' } + else { 4 }", + ); + + let expr = expr.to_if().unwrap(); + + let branch0 = &expr.branches[0]; + let branch0_cond_tail = &branch0.cond_tail.as_ref().unwrap().to_bin().unwrap(); + assert!(branch0_cond_tail.lhs.is_ident()); + assert!(branch0_cond_tail.rhs.is_lit_int()); + assert!(branch0 + .then_block + .to_block() + .unwrap() + .expr + .as_ref() + .unwrap() + .is_lit_str()); + + let branch1 = &expr.branches[1]; + let branch1_cond_tail = &branch1.cond_tail.as_ref().unwrap().to_bin().unwrap(); + assert!(branch1_cond_tail.lhs.is_ident()); + assert!(branch1_cond_tail.rhs.is_lit_float()); + assert!(branch1 + .then_block + .to_block() + .unwrap() + .expr + .as_ref() + .unwrap() + .is_lit_char()); + + let else_block = expr.else_block.as_ref().unwrap(); + assert!(else_block + .to_block() + .unwrap() + .expr + .as_ref() + .unwrap() + .is_lit_int()) + } + #[test] fn parse_tuple() { let (expr, _) = parse_expr("(1,)"); diff --git a/dora/src/language/fctbodyck/body.rs b/dora/src/language/fctbodyck/body.rs index d4fbaf413..7270352e9 100644 --- a/dora/src/language/fctbodyck/body.rs +++ b/dora/src/language/fctbodyck/body.rs @@ -28,6 +28,7 @@ use crate::language::{report_sym_shadow, TypeParamContext}; use dora_parser::ast; use dora_parser::ast::visit::Visitor; +use dora_parser::ast::Expr; use dora_parser::interner::Name; use dora_parser::lexer::position::Position; use dora_parser::lexer::token::{FloatSuffix, IntBase, IntSuffix}; @@ -809,36 +810,46 @@ impl<'a> TypeCheck<'a> { } fn check_expr_if(&mut self, expr: &ast::ExprIfType, expected_ty: SourceType) -> SourceType { - let expr_type = self.check_expr(&expr.cond, SourceType::Any); + let let_stmt = expr.cond_head.as_ref(); + self.check_stmt_let(let_stmt); + let mut branch_types = Vec::new(); + let mut require_cond_head_is_bool = false; + for branch in &expr.branches { + if branch.cond_tail.is_some() { + let cond_tail = branch.cond_tail.as_ref().unwrap(); + let cond_tail_type = self.check_expr(&cond_tail, expected_ty.clone()); + self.check_if_condition_is_bool(cond_tail_type, cond_tail); + } else { + // if any branch is empty, the condition head needs to be of type bool + require_cond_head_is_bool = true; + } - if !expr_type.is_bool() && !expr_type.is_error() { - let expr_type = expr_type.name_fct(self.sa, self.fct); - let msg = ErrorMessage::IfCondType(expr_type); - self.sa.diag.lock().report(self.file_id, expr.pos, msg); + branch_types.push(( + self.check_expr(&branch.then_block, SourceType::Any), + expr_always_returns(&branch.then_block), + )); + } + if require_cond_head_is_bool { + let cond_expr = let_stmt.expr.as_ref().unwrap(); + let cond_type = self.analysis.ty(cond_expr.id()); + self.check_if_condition_is_bool(cond_type, cond_expr); } + let merged_type = if expr.else_block.is_some() { + let else_block = expr.else_block.as_ref().unwrap(); + branch_types.push(( + self.check_expr(else_block, expected_ty), + expr_always_returns(else_block), + )); - let then_type = self.check_expr(&expr.then_block, expected_ty.clone()); - - let merged_type = if let Some(ref else_block) = expr.else_block { - let else_type = self.check_expr(else_block, expected_ty); - - if expr_always_returns(&expr.then_block) { - else_type - } else if expr_always_returns(else_block) { - then_type - } else if then_type.is_error() { - else_type - } else if else_type.is_error() { - then_type - } else if !then_type.allows(self.sa, else_type.clone()) { - let then_type_name = then_type.name_fct(self.sa, self.fct); - let else_type_name = else_type.name_fct(self.sa, self.fct); - let msg = ErrorMessage::IfBranchTypesIncompatible(then_type_name, else_type_name); - self.sa.diag.lock().report(self.file_id, expr.pos, msg); - then_type - } else { - then_type - } + branch_types + .iter() + .fold((SourceType::Error, true), |t1, t2| { + ( + self.merge_branch_types(expr, t1.0, t1.1, t2.clone().0, t2.1), + false, + ) + }) + .0 } else { SourceType::Unit }; @@ -848,6 +859,41 @@ impl<'a> TypeCheck<'a> { merged_type } + fn check_if_condition_is_bool(&mut self, cond_type: SourceType, cond: &Box) { + if !cond_type.is_bool() && !cond_type.is_error() { + let expr_type = cond_type.name_fct(self.sa, self.fct); + let msg = ErrorMessage::IfCondType(expr_type); + self.sa.diag.lock().report(self.file_id, cond.pos(), msg); + } + } + + fn merge_branch_types( + &mut self, + expr: &ast::ExprIfType, + type1: SourceType, + always_returns1: bool, + type2: SourceType, + always_returns2: bool, + ) -> SourceType { + if always_returns1 { + type2 + } else if always_returns2 { + type1 + } else if type1.is_error() { + type2 + } else if type2.is_error() { + type1 + } else if !type1.allows(self.sa, type2.clone()) { + let then_type_name = type1.name_fct(self.sa, self.fct); + let else_type_name = type2.name_fct(self.sa, self.fct); + let msg = ErrorMessage::IfBranchTypesIncompatible(then_type_name, else_type_name); + self.sa.diag.lock().report(self.file_id, expr.pos, msg); + type1 + } else { + type1 + } + } + fn check_expr_ident(&mut self, e: &ast::ExprIdentType, expected_ty: SourceType) -> SourceType { let sym = self.symtable.get(e.name); diff --git a/dora/src/language/fctbodyck/tests.rs b/dora/src/language/fctbodyck/tests.rs index f7e93b98b..2ca5c4d2a 100644 --- a/dora/src/language/fctbodyck/tests.rs +++ b/dora/src/language/fctbodyck/tests.rs @@ -256,7 +256,7 @@ fn type_if() { ok("fun x() { if false { } }"); err( "fun x() { if 4i32 { } }", - pos(1, 11), + pos(1, 14), ErrorMessage::IfCondType("Int32".into()), ); } diff --git a/dora/src/language/generator.rs b/dora/src/language/generator.rs index 98dd10345..bbb5a9f05 100644 --- a/dora/src/language/generator.rs +++ b/dora/src/language/generator.rs @@ -215,7 +215,9 @@ impl<'a> AstBytecodeGen<'a> { ast::Stmt::Break(ref stmt) => self.visit_stmt_break(stmt), ast::Stmt::Continue(ref stmt) => self.visit_stmt_continue(stmt), ast::Stmt::Expr(ref expr) => self.visit_stmt_expr(expr), - ast::Stmt::Let(ref stmt) => self.visit_stmt_let(stmt), + ast::Stmt::Let(ref stmt) => { + self.visit_stmt_let(stmt); + } ast::Stmt::While(ref stmt) => self.visit_stmt_while(stmt), ast::Stmt::For(ref stmt) => self.visit_stmt_for(stmt), } @@ -475,23 +477,27 @@ impl<'a> AstBytecodeGen<'a> { self.free_if_temp(object_reg); } - fn visit_stmt_let(&mut self, stmt: &ast::StmtLetType) { + fn visit_stmt_let(&mut self, stmt: &ast::StmtLetType) -> Option { match &*stmt.pattern { - ast::LetPattern::Ident(ref ident) => { - self.visit_stmt_let_ident(stmt, ident); - } + ast::LetPattern::Ident(ref ident) => self.visit_stmt_let_ident(stmt, ident), ast::LetPattern::Underscore(_) => { self.visit_stmt_let_underscore(stmt); + None } ast::LetPattern::Tuple(ref tuple) => { self.visit_stmt_let_pattern(stmt, tuple); + None } } } - fn visit_stmt_let_ident(&mut self, stmt: &ast::StmtLetType, ident: &ast::LetIdentType) { + fn visit_stmt_let_ident( + &mut self, + stmt: &ast::StmtLetType, + ident: &ast::LetIdentType, + ) -> Option { let var_id = *self.analysis.map_vars.get(ident.id).unwrap(); let var = self.analysis.vars.get_var(var_id); @@ -504,6 +510,7 @@ impl<'a> AstBytecodeGen<'a> { self.store_in_context(value_reg, context_idx, ident.pos); self.free_if_temp(value_reg); } + None } VarLocation::Stack => { @@ -518,7 +525,9 @@ impl<'a> AstBytecodeGen<'a> { }; if let Some(ref expr) = stmt.expr { - self.visit_expr(expr, dest); + Some(self.visit_expr(expr, dest)) + } else { + None } } } @@ -963,40 +972,64 @@ impl<'a> AstBytecodeGen<'a> { self.ensure_register(dest, register_bty_from_ty(ty)) }; - let else_lbl = self.builder.create_label(); + let mut next_lbl; let end_lbl = self.builder.create_label(); - let cond_reg = self.visit_expr(&expr.cond, DataDest::Alloc); - self.builder.emit_jump_if_false(cond_reg, else_lbl); - self.free_if_temp(cond_reg); - - self.visit_expr(&expr.then_block, DataDest::Reg(dest)); + let cond_head_reg = self.visit_stmt_let(&expr.cond_head).unwrap(); - if !expr_always_returns(&expr.then_block) { - self.builder.emit_jump(end_lbl); + for branch in &expr.branches { + next_lbl = self.builder.create_label(); + self.visit_if_branch(cond_head_reg, branch, next_lbl, end_lbl, dest); } - self.builder.bind_label(else_lbl); self.visit_expr(else_block, DataDest::Reg(dest)); self.builder.bind_label(end_lbl); dest } else { - // Without else-branch there can't be return value + // Without else-branch there can't be return value. + // This will assumption will change when we start considering exhaustive types, i. e. + // `cond_head` is an enum and all enum members have been covered in preceding branches. assert!(ty.is_unit()); let end_lbl = self.builder.create_label(); - let cond_reg = self.visit_expr(&expr.cond, DataDest::Alloc); + let cond_reg = self.visit_expr(&expr.cond_head.expr.as_ref().unwrap(), DataDest::Alloc); self.builder.emit_jump_if_false(cond_reg, end_lbl); self.free_if_temp(cond_reg); - self.emit_expr_for_effect(&expr.then_block); + for branch in expr.branches.iter() { + self.emit_expr_for_effect(&branch.then_block); + } self.builder.bind_label(end_lbl); Register::invalid() } } + fn visit_if_branch( + &mut self, + cond_head: Register, + branch: &ast::Branch, + next_lbl: Label, + end_lbl: Label, + dest: Register, + ) { + let cond_reg = if branch.cond_tail.is_some() { + let cond = branch.cond_tail.as_ref().unwrap(); + self.visit_expr(cond, DataDest::Alloc) + } else { + cond_head + }; + self.builder.emit_jump_if_false(cond_reg, next_lbl); + self.free_if_temp(cond_reg); + + self.visit_expr(&branch.then_block, DataDest::Reg(dest)); + if !expr_always_returns(&branch.then_block) { + self.builder.emit_jump(end_lbl); + } + self.builder.bind_label(next_lbl); + } + fn visit_expr_block(&mut self, block: &ast::ExprBlockType, dest: DataDest) -> Register { self.push_scope(); diff --git a/dora/src/language/generator_tests.rs b/dora/src/language/generator_tests.rs index 870594b8f..240c9cfdb 100644 --- a/dora/src/language/generator_tests.rs +++ b/dora/src/language/generator_tests.rs @@ -495,11 +495,12 @@ fn gen_stmt_if() { fn gen_stmt_if_else_with_return() { let result = code("fun f(a: Bool): Int32 { if a { return 1; } else { return 2; } }"); let expected = vec![ - JumpIfFalse(r(0), 3), - ConstInt32(r(1), 1), - Ret(r(1)), - ConstInt32(r(1), 2), - Ret(r(1)), + Mov(Register(1), Register(0)), + JumpIfFalse(r(1), 4), + ConstInt32(r(2), 1), + Ret(r(2)), + ConstInt32(r(2), 2), + Ret(r(2)), ]; assert_eq!(expected, result); } @@ -515,9 +516,10 @@ fn gen_stmt_if_else_without_return() { ); let expected = vec![ Mov(r(1), r(0)), - JumpIfFalse(r(1), 4), + Mov(r(2), r(1)), + JumpIfFalse(r(2), 5), ConstFalse(r(1)), - Jump(5), + Jump(6), ConstTrue(r(1)), Ret(r(1)), ]; diff --git a/dora/src/language/returnck.rs b/dora/src/language/returnck.rs index b2e4529d5..2bcd4bf94 100644 --- a/dora/src/language/returnck.rs +++ b/dora/src/language/returnck.rs @@ -39,7 +39,9 @@ pub fn expr_block_returns_value(e: &ExprBlockType) -> Result<(), Position> { } fn expr_if_returns_value(e: &ExprIfType) -> Result<(), Position> { - expr_returns_value(&e.then_block)?; + for branch in &e.branches { + expr_returns_value(&branch.then_block)?; + } match e.else_block { Some(ref block) => expr_returns_value(block), diff --git a/tests/if1.dora b/tests/if/if1.dora similarity index 100% rename from tests/if1.dora rename to tests/if/if1.dora diff --git a/tests/if/if2.dora b/tests/if/if2.dora new file mode 100644 index 000000000..07e2592db --- /dev/null +++ b/tests/if/if2.dora @@ -0,0 +1,7 @@ +fun main() { + let x = if true + ... { 1 } + ... { 2 } + else { -1 }; + assert(x == 1); +} diff --git a/tests/if/if3.dora b/tests/if/if3.dora new file mode 100644 index 000000000..4635a9b3c --- /dev/null +++ b/tests/if/if3.dora @@ -0,0 +1,7 @@ +fun main() { + let x = if true + ... == false { 1 } + ... == true { 2 } + else { -1 }; + assert(x == 2); +} diff --git a/tests/if/if4.dora b/tests/if/if4.dora new file mode 100644 index 000000000..396f646c6 --- /dev/null +++ b/tests/if/if4.dora @@ -0,0 +1,8 @@ +fun main() { + let x = if "abc" + ... == "bcd" { 1 } + ... != "abc" { 2 } + ... .size() == 3 { 3 } + else { -1 }; + assert(x == 3); +} diff --git a/tests/if/if5.dora b/tests/if/if5.dora new file mode 100644 index 000000000..c74c1d6f7 --- /dev/null +++ b/tests/if/if5.dora @@ -0,0 +1,8 @@ +fun main() { + let x = if "abc" + ... == "bcd" { 1 } + ... != "abc" { 2 } + ... .size() == 4 { 3 } + else { -1 }; + assert(x == -1); +} diff --git a/tests/if/if6.dora b/tests/if/if6.dora new file mode 100644 index 000000000..3cc00ea9c --- /dev/null +++ b/tests/if/if6.dora @@ -0,0 +1,13 @@ +fun main() { + let x = if "abc" + ... == "bcd" { 1 } + ... != "abc" { 2 } + ... .size() == 3 { + if "def" + ... == "bcd" { 3 } + ... == "def" { 4 } + else { 5 } + } + else { -1 }; + assert(x == 4); +} diff --git a/tests/if/if7.dora b/tests/if/if7.dora new file mode 100644 index 000000000..484a75b24 --- /dev/null +++ b/tests/if/if7.dora @@ -0,0 +1,14 @@ +fun main() { + let x = if "abc" + ... == "bcd" { 1 } + ... != "abc" { 2 } + ... .size() == 3 { 3 } + else { -1 }; + assert(x == 3); + let x = if "abc" + ... == "bcd" { 1 } + ... == "abc" { 2 } + ... .size() == 4 { 3 } + else { -1 }; + assert(x == 2); +} diff --git a/tests/if/if8.dora b/tests/if/if8.dora new file mode 100644 index 000000000..449edcc53 --- /dev/null +++ b/tests/if/if8.dora @@ -0,0 +1,18 @@ +fun main() { + let x = if check() + ... == false { 1 } + ... == true { 2 } + else { -1 }; + assert(x == 2); +} + +let mut checked: Int64 = 0; + +fun check(): Bool { + if checked != 0 { + std::fatalError("fun ran " + checked.toString() + " times!"); + } else { + checked = checked + 1; + } + true +}