diff --git a/compiler/plc_diagnostics/src/diagnostics.rs b/compiler/plc_diagnostics/src/diagnostics.rs index 7f0593819a..db9b57cbd8 100644 --- a/compiler/plc_diagnostics/src/diagnostics.rs +++ b/compiler/plc_diagnostics/src/diagnostics.rs @@ -744,6 +744,13 @@ impl Diagnostic { err_no: ErrNo::var__invalid_enum_variant, } } + + pub fn assignment_instead_of_equal(range: SourceLocation) -> Diagnostic { + Diagnostic::ImprovementSuggestion { + message: "This statement has no effect, did you mean to use `:=`?".to_string(), + range: vec![range], + } + } } // CFC related diagnostics diff --git a/src/codegen/tests/debug_tests/snapshots/rusty__codegen__tests__debug_tests__expression_debugging__repeat_conditions_location_marked.snap b/src/codegen/tests/debug_tests/snapshots/rusty__codegen__tests__debug_tests__expression_debugging__repeat_conditions_location_marked.snap index f8b406a4e5..9f92bd5c61 100644 --- a/src/codegen/tests/debug_tests/snapshots/rusty__codegen__tests__debug_tests__expression_debugging__repeat_conditions_location_marked.snap +++ b/src/codegen/tests/debug_tests/snapshots/rusty__codegen__tests__debug_tests__expression_debugging__repeat_conditions_location_marked.snap @@ -15,8 +15,12 @@ entry: condition_check: ; preds = %while_body %load_myFunc = load i32, i32* %myFunc, align 4, !dbg !12 %tmpVar = icmp sgt i32 %load_myFunc, 10, !dbg !12 - %tmpVar1 = xor i1 %tmpVar, true, !dbg !12 - br i1 %tmpVar1, label %while_body, label %continue, !dbg !12 + %0 = zext i1 %tmpVar to i8, !dbg !12 + %1 = icmp ne i8 %0, 0, !dbg !12 + %tmpVar1 = xor i1 %1, true, !dbg !12 + %2 = zext i1 %tmpVar1 to i8, !dbg !12 + %3 = icmp ne i8 %2, 0, !dbg !12 + br i1 %3, label %while_body, label %continue, !dbg !12 while_body: ; preds = %entry, %condition_check store i32 1, i32* %myFunc, align 4, !dbg !11 diff --git a/src/codegen/tests/debug_tests/snapshots/rusty__codegen__tests__debug_tests__expression_debugging__while_conditions_location_marked.snap b/src/codegen/tests/debug_tests/snapshots/rusty__codegen__tests__debug_tests__expression_debugging__while_conditions_location_marked.snap index 088019ca7f..210e5d113c 100644 --- a/src/codegen/tests/debug_tests/snapshots/rusty__codegen__tests__debug_tests__expression_debugging__while_conditions_location_marked.snap +++ b/src/codegen/tests/debug_tests/snapshots/rusty__codegen__tests__debug_tests__expression_debugging__while_conditions_location_marked.snap @@ -15,7 +15,9 @@ entry: condition_check: ; preds = %entry, %while_body %load_myFunc = load i32, i32* %myFunc, align 4, !dbg !12 %tmpVar = icmp sgt i32 %load_myFunc, 1, !dbg !12 - br i1 %tmpVar, label %while_body, label %continue, !dbg !12 + %0 = zext i1 %tmpVar to i8, !dbg !12 + %1 = icmp ne i8 %0, 0, !dbg !12 + br i1 %1, label %while_body, label %continue, !dbg !12 while_body: ; preds = %condition_check store i32 1, i32* %myFunc, align 4, !dbg !11 diff --git a/src/codegen/tests/snapshots/rusty__codegen__tests__code_gen_tests__if_with_expression_generator_test.snap b/src/codegen/tests/snapshots/rusty__codegen__tests__code_gen_tests__if_with_expression_generator_test.snap index d91b86c7c0..485cce3a48 100644 --- a/src/codegen/tests/snapshots/rusty__codegen__tests__code_gen_tests__if_with_expression_generator_test.snap +++ b/src/codegen/tests/snapshots/rusty__codegen__tests__code_gen_tests__if_with_expression_generator_test.snap @@ -15,22 +15,26 @@ entry: %b1 = getelementptr inbounds %prg, %prg* %0, i32 0, i32 1 %load_x = load i32, i32* %x, align 4 %tmpVar = icmp sgt i32 %load_x, 1 - br i1 %tmpVar, label %3, label %1 + %1 = zext i1 %tmpVar to i8 + %2 = icmp ne i8 %1, 0 + br i1 %2, label %5, label %3 -condition_body: ; preds = %3 +condition_body: ; preds = %5 %load_x1 = load i32, i32* %x, align 4 br label %continue -continue: ; preds = %condition_body, %3 +continue: ; preds = %condition_body, %5 ret void -1: ; preds = %entry +3: ; preds = %entry %load_b1 = load i8, i8* %b1, align 1 - %2 = icmp ne i8 %load_b1, 0 - br label %3 - -3: ; preds = %1, %entry - %4 = phi i1 [ %tmpVar, %entry ], [ %2, %1 ] - br i1 %4, label %condition_body, label %continue + %4 = icmp ne i8 %load_b1, 0 + br label %5 + +5: ; preds = %3, %entry + %6 = phi i1 [ %2, %entry ], [ %4, %3 ] + %7 = zext i1 %6 to i8 + %8 = icmp ne i8 %7, 0 + br i1 %8, label %condition_body, label %continue } diff --git a/src/codegen/tests/snapshots/rusty__codegen__tests__code_gen_tests__returning_early_in_function.snap b/src/codegen/tests/snapshots/rusty__codegen__tests__code_gen_tests__returning_early_in_function.snap index 3681b4b66b..d6be89e902 100644 --- a/src/codegen/tests/snapshots/rusty__codegen__tests__code_gen_tests__returning_early_in_function.snap +++ b/src/codegen/tests/snapshots/rusty__codegen__tests__code_gen_tests__returning_early_in_function.snap @@ -14,7 +14,9 @@ entry: %load_n = load i8, i8* %n, align 1 %1 = sext i8 %load_n to i32 %tmpVar = icmp slt i32 %1, 10 - br i1 %tmpVar, label %condition_body, label %continue + %2 = zext i1 %tmpVar to i8 + %3 = icmp ne i8 %2, 0 + br i1 %3, label %condition_body, label %continue condition_body: ; preds = %entry %smaller_than_ten_ret = load i16, i16* %smaller_than_ten, align 2 diff --git a/src/codegen/tests/snapshots/rusty__codegen__tests__code_gen_tests__returning_early_in_function_block.snap b/src/codegen/tests/snapshots/rusty__codegen__tests__code_gen_tests__returning_early_in_function_block.snap index e26fa8c863..10f321a84d 100644 --- a/src/codegen/tests/snapshots/rusty__codegen__tests__code_gen_tests__returning_early_in_function_block.snap +++ b/src/codegen/tests/snapshots/rusty__codegen__tests__code_gen_tests__returning_early_in_function_block.snap @@ -15,7 +15,9 @@ entry: %load_n = load i8, i8* %n, align 1 %1 = sext i8 %load_n to i32 %tmpVar = icmp slt i32 %1, 10 - br i1 %tmpVar, label %condition_body, label %continue + %2 = zext i1 %tmpVar to i8 + %3 = icmp ne i8 %2, 0 + br i1 %3, label %condition_body, label %continue condition_body: ; preds = %entry ret void diff --git a/src/codegen/tests/snapshots/rusty__codegen__tests__code_gen_tests__while_loop_with_if_exit.snap b/src/codegen/tests/snapshots/rusty__codegen__tests__code_gen_tests__while_loop_with_if_exit.snap index 169623959a..b4c246c497 100644 --- a/src/codegen/tests/snapshots/rusty__codegen__tests__code_gen_tests__while_loop_with_if_exit.snap +++ b/src/codegen/tests/snapshots/rusty__codegen__tests__code_gen_tests__while_loop_with_if_exit.snap @@ -17,7 +17,9 @@ entry: condition_check: ; preds = %entry, %continue3 %load_x = load i32, i32* %x, align 4 %tmpVar = icmp slt i32 %load_x, 20 - br i1 %tmpVar, label %while_body, label %continue + %1 = zext i1 %tmpVar to i8 + %2 = icmp ne i8 %1, 0 + br i1 %2, label %while_body, label %continue while_body: ; preds = %condition_check %load_x1 = load i32, i32* %x, align 4 @@ -25,7 +27,9 @@ while_body: ; preds = %condition_check store i32 %tmpVar2, i32* %x, align 4 %load_x4 = load i32, i32* %x, align 4 %tmpVar5 = icmp sge i32 %load_x4, 10 - br i1 %tmpVar5, label %condition_body, label %continue3 + %3 = zext i1 %tmpVar5 to i8 + %4 = icmp ne i8 %3, 0 + br i1 %4, label %condition_body, label %continue3 continue: ; preds = %condition_body, %condition_check ret void diff --git a/src/codegen/tests/snapshots/rusty__codegen__tests__code_gen_tests__while_with_expression_statement.snap b/src/codegen/tests/snapshots/rusty__codegen__tests__code_gen_tests__while_with_expression_statement.snap index 7301464b95..93829a14df 100644 --- a/src/codegen/tests/snapshots/rusty__codegen__tests__code_gen_tests__while_with_expression_statement.snap +++ b/src/codegen/tests/snapshots/rusty__codegen__tests__code_gen_tests__while_with_expression_statement.snap @@ -18,7 +18,9 @@ condition_check: ; preds = %entry, %while_body %load_x = load i8, i8* %x, align 1 %1 = zext i8 %load_x to i32 %tmpVar = icmp eq i32 %1, 0 - br i1 %tmpVar, label %while_body, label %continue + %2 = zext i1 %tmpVar to i8 + %3 = icmp ne i8 %2, 0 + br i1 %3, label %while_body, label %continue while_body: ; preds = %condition_check %load_x1 = load i8, i8* %x, align 1 diff --git a/src/resolver.rs b/src/resolver.rs index d20f2c5606..82fe52b63e 100644 --- a/src/resolver.rs +++ b/src/resolver.rs @@ -75,9 +75,12 @@ pub struct VisitorContext<'s> { /// e.g. true for `a.b.c` if either a,b or c is declared in a constant block constant: bool, - /// true the visitor entered a body (so no declarations) + /// true if the visitor entered a body (so no declarations) in_body: bool, + /// true if the visitor entered a control statement + in_control: bool, + pub id_provider: IdProvider, // what's the current strategy for resolving @@ -87,80 +90,54 @@ pub struct VisitorContext<'s> { impl<'s> VisitorContext<'s> { /// returns a copy of the current context and changes the `current_qualifier` to the given qualifier fn with_qualifier(&self, qualifier: String) -> VisitorContext<'s> { - VisitorContext { - pou: self.pou, - qualifier: Some(qualifier), - lhs: self.lhs, - constant: false, - in_body: self.in_body, - id_provider: self.id_provider.clone(), - resolve_strategy: self.resolve_strategy.clone(), - } + let mut ctx = self.clone(); + ctx.qualifier = Some(qualifier); + ctx.constant = false; + ctx } /// returns a copy of the current context and changes the `current_pou` to the given pou fn with_pou(&self, pou: &'s str) -> VisitorContext<'s> { - VisitorContext { - pou: Some(pou), - qualifier: self.qualifier.clone(), - lhs: self.lhs, - constant: false, - in_body: self.in_body, - id_provider: self.id_provider.clone(), - resolve_strategy: self.resolve_strategy.clone(), - } + let mut ctx = self.clone(); + ctx.pou = Some(pou); + ctx.constant = false; + ctx } /// returns a copy of the current context and changes the `lhs_pou` to the given pou fn with_lhs(&self, lhs_pou: &'s str) -> VisitorContext<'s> { - VisitorContext { - pou: self.pou, - qualifier: self.qualifier.clone(), - lhs: Some(lhs_pou), - constant: false, - in_body: self.in_body, - id_provider: self.id_provider.clone(), - resolve_strategy: self.resolve_strategy.clone(), - } + let mut ctx = self.clone(); + ctx.lhs = Some(lhs_pou); + ctx.constant = false; + ctx } /// returns a copy of the current context and changes the `is_call` to true fn with_const(&self, const_state: bool) -> VisitorContext<'s> { - VisitorContext { - pou: self.pou, - qualifier: self.qualifier.clone(), - lhs: self.lhs, - constant: const_state, - in_body: self.in_body, - id_provider: self.id_provider.clone(), - resolve_strategy: self.resolve_strategy.clone(), - } + let mut ctx = self.clone(); + ctx.constant = const_state; + ctx } // returns a copy of the current context and sets the in_body field to true fn enter_body(&self) -> Self { - VisitorContext { - pou: self.pou, - qualifier: self.qualifier.clone(), - lhs: self.lhs, - constant: self.constant, - in_body: true, - id_provider: self.id_provider.clone(), - resolve_strategy: self.resolve_strategy.clone(), - } + let mut ctx = self.clone(); + ctx.in_body = true; + ctx + } + + fn enter_control(&self) -> Self { + let mut ctx = self.clone(); + ctx.in_control = true; + ctx } // returns a copy of the current context and sets the resolve_strategy field to the given strategies fn with_resolving_strategy(&self, resolve_strategy: Vec) -> Self { - VisitorContext { - pou: self.pou, - qualifier: self.qualifier.clone(), - lhs: self.lhs, - constant: self.constant, - in_body: true, - id_provider: self.id_provider.clone(), - resolve_strategy, - } + let mut ctx = self.clone(); + ctx.in_body = true; + ctx.resolve_strategy = resolve_strategy; + ctx } fn is_in_a_body(&self) -> bool { @@ -762,6 +739,7 @@ impl<'i> TypeAnnotator<'i> { in_body: false, id_provider, resolve_strategy: ResolvingScope::default_scopes(), + in_control: false, }; for global_variable in unit.global_vars.iter().flat_map(|it| it.variables.iter()) { @@ -1244,54 +1222,56 @@ impl<'i> TypeAnnotator<'i> { self.visit_statement(ctx, expr); self.inherit_annotations(statement, expr); } - AstStatement::ControlStatement(AstControlStatement::If(stmt), ..) => { - stmt.blocks.iter().for_each(|b| { - self.visit_statement(ctx, b.condition.as_ref()); - b.body.iter().for_each(|s| self.visit_statement(ctx, s)); - }); - stmt.else_block.iter().for_each(|e| self.visit_statement(ctx, e)); - } - AstStatement::ControlStatement(AstControlStatement::ForLoop(stmt), ..) => { - visit_all_statements!(self, ctx, &stmt.counter, &stmt.start, &stmt.end); - if let Some(by_step) = &stmt.by_step { - self.visit_statement(ctx, by_step); - } - //Hint annotate start, end and step with the counter's real type - if let Some(type_name) = self - .annotation_map - .get_type(&stmt.counter, self.index) - .map(typesystem::DataType::get_name) - { - let annotation = StatementAnnotation::value(type_name); - self.annotation_map.annotate_type_hint(&stmt.start, annotation.clone()); - self.annotation_map.annotate_type_hint(&stmt.end, annotation.clone()); - if let Some(by_step) = &stmt.by_step { - self.annotation_map.annotate_type_hint(by_step, annotation); + AstStatement::ControlStatement(control) => { + match control { + AstControlStatement::If(stmt) => { + stmt.blocks.iter().for_each(|b| { + self.visit_statement(&ctx.enter_control(), b.condition.as_ref()); + b.body.iter().for_each(|s| self.visit_statement(ctx, s)); + }); + stmt.else_block.iter().for_each(|e| self.visit_statement(ctx, e)); } - } - stmt.body.iter().for_each(|s| self.visit_statement(ctx, s)); - } - AstStatement::ControlStatement(AstControlStatement::WhileLoop(stmt), ..) - | AstStatement::ControlStatement(AstControlStatement::RepeatLoop(stmt), ..) => { - self.visit_statement(ctx, &stmt.condition); - stmt.body.iter().for_each(|s| self.visit_statement(ctx, s)); - } - AstStatement::ControlStatement(AstControlStatement::Case(stmt), ..) => { - self.visit_statement(ctx, &stmt.selector); - let selector_type = self.annotation_map.get_type(&stmt.selector, self.index).cloned(); - stmt.case_blocks.iter().for_each(|b| { - self.visit_statement(ctx, b.condition.as_ref()); - if let Some(selector_type) = &selector_type { - self.update_expected_types(selector_type, b.condition.as_ref()); + AstControlStatement::ForLoop(stmt) => { + visit_all_statements!(self, ctx, &stmt.counter, &stmt.start, &stmt.end); + if let Some(by_step) = &stmt.by_step { + self.visit_statement(ctx, by_step); + } + //Hint annotate start, end and step with the counter's real type + if let Some(type_name) = self + .annotation_map + .get_type(&stmt.counter, self.index) + .map(typesystem::DataType::get_name) + { + let annotation = StatementAnnotation::value(type_name); + self.annotation_map.annotate_type_hint(&stmt.start, annotation.clone()); + self.annotation_map.annotate_type_hint(&stmt.end, annotation.clone()); + if let Some(by_step) = &stmt.by_step { + self.annotation_map.annotate_type_hint(by_step, annotation); + } + } + stmt.body.iter().for_each(|s| self.visit_statement(ctx, s)); + } + AstControlStatement::WhileLoop(stmt) | AstControlStatement::RepeatLoop(stmt) => { + self.visit_statement(&ctx.enter_control(), &stmt.condition); + stmt.body.iter().for_each(|s| self.visit_statement(ctx, s)); + } + AstControlStatement::Case(stmt) => { + self.visit_statement(ctx, &stmt.selector); + let selector_type = self.annotation_map.get_type(&stmt.selector, self.index).cloned(); + stmt.case_blocks.iter().for_each(|b| { + self.visit_statement(ctx, b.condition.as_ref()); + if let Some(selector_type) = &selector_type { + self.update_expected_types(selector_type, b.condition.as_ref()); + } + b.body.iter().for_each(|s| self.visit_statement(ctx, s)); + }); + stmt.else_block.iter().for_each(|s| self.visit_statement(ctx, s)); } - b.body.iter().for_each(|s| self.visit_statement(ctx, s)); - }); - stmt.else_block.iter().for_each(|s| self.visit_statement(ctx, s)); + } } + AstStatement::CaseCondition(condition, ..) => self.visit_statement(ctx, condition), - _ => { - self.visit_statement_expression(ctx, statement); - } + _ => self.visit_statement_expression(ctx, statement), } } @@ -1399,7 +1379,15 @@ impl<'i> TypeAnnotator<'i> { }; if let Some(statement_type) = statement_type { - self.annotate(statement, StatementAnnotation::value(statement_type)); + self.annotate(statement, StatementAnnotation::value(statement_type.clone())); + + // https://github.com/PLC-lang/rusty/issues/939: We rely on type-hints in order + // to identify `=` operations that have no effect (e.g. `foo = bar;`) hence + // type-hint the conditions of control statements to eliminate false-positives. + if ctx.in_control { + self.annotation_map + .annotate_type_hint(statement, StatementAnnotation::value(statement_type)) + } } } AstStatement::UnaryExpression(data, ..) => { @@ -1435,7 +1423,7 @@ impl<'i> TypeAnnotator<'i> { visit_all_statements!(self, ctx, &data.start, &data.end); } AstStatement::Assignment(data, ..) => { - self.visit_statement(ctx, &data.right); + self.visit_statement(&ctx.enter_control(), &data.right); if let Some(lhs) = ctx.lhs { //special context for left hand side self.visit_statement(&ctx.with_pou(lhs).with_lhs(lhs), &data.left); diff --git a/src/resolver/tests/resolve_expressions_tests.rs b/src/resolver/tests/resolve_expressions_tests.rs index 4997f48006..c9b39f9fd0 100644 --- a/src/resolver/tests/resolve_expressions_tests.rs +++ b/src/resolver/tests/resolve_expressions_tests.rs @@ -400,7 +400,7 @@ fn addition_subtraction_expression_with_pointers_resolves_to_pointer_type() { if let AstNode { stmt: AstStatement::BinaryExpression(BinaryExpression { left, .. }), .. } = &**addition { - assert_type_and_hint!(&annotations, &index, left, "__PRG_a", None); + assert_type_and_hint!(&annotations, &index, left, "__PRG_a", Some("__PRG_a")); } } if let AstNode { stmt: AstStatement::Assignment(Assignment { right: addition, .. }), .. } = &statements[2] @@ -462,7 +462,7 @@ fn complex_expressions_resolves_types_for_literals_directly() { // c assert_type_and_hint!(&annotations, &index, c, INT_TYPE, Some(DINT_TYPE)); // (b + USINT#7) - assert_type_and_hint!(&annotations, &index, left, DINT_TYPE, None); + assert_type_and_hint!(&annotations, &index, left, DINT_TYPE, Some(DINT_TYPE)); let AstStatement::ParenExpression(left) = left.get_stmt() else { panic!() }; if let AstNode { diff --git a/src/validation/statement.rs b/src/validation/statement.rs index a1b1d83b17..5ee8202ed4 100644 --- a/src/validation/statement.rs +++ b/src/validation/statement.rs @@ -524,6 +524,13 @@ fn visit_binary_expression( context: &ValidationContext, ) { match operator { + Operator::Equal => { + if context.annotations.get_type_hint(statement, context.index).is_none() { + validator.push_diagnostic(Diagnostic::assignment_instead_of_equal(statement.get_location())); + } + + validate_binary_expression(validator, statement, operator, left, right, context) + } Operator::NotEqual => { validate_binary_expression(validator, statement, &Operator::Equal, left, right, context) } diff --git a/src/validation/tests/snapshots/rusty__validation__tests__variable_validation_tests__assignment_suggestion_for_equal_operation_with_no_effect.snap b/src/validation/tests/snapshots/rusty__validation__tests__variable_validation_tests__assignment_suggestion_for_equal_operation_with_no_effect.snap new file mode 100644 index 0000000000..b3b3e13c1b --- /dev/null +++ b/src/validation/tests/snapshots/rusty__validation__tests__variable_validation_tests__assignment_suggestion_for_equal_operation_with_no_effect.snap @@ -0,0 +1,41 @@ +--- +source: src/validation/tests/variable_validation_tests.rs +expression: diagnostics +--- +warning: This statement has no effect, did you mean to use `:=`? + ┌─ :24:13 + │ +24 │ value = 1; + │ ^^^^^^^^^ This statement has no effect, did you mean to use `:=`? + +warning: This statement has no effect, did you mean to use `:=`? + ┌─ :25:13 + │ +25 │ value = condition AND condition; + │ ^^^^^^^^^^^^^^^^^ This statement has no effect, did you mean to use `:=`? + +warning: This statement has no effect, did you mean to use `:=`? + ┌─ :26:13 + │ +26 │ value = condition AND (condition = TRUE); + │ ^^^^^^^^^^^^^^^^^ This statement has no effect, did you mean to use `:=`? + +warning: This statement has no effect, did you mean to use `:=`? + ┌─ :26:36 + │ +26 │ value = condition AND (condition = TRUE); + │ ^^^^^^^^^^^^^^^^ This statement has no effect, did you mean to use `:=`? + +warning: This statement has no effect, did you mean to use `:=`? + ┌─ :28:26 + │ +28 │ IF TRUE THEN value = 1; END_IF + │ ^^^^^^^^^ This statement has no effect, did you mean to use `:=`? + +warning: This statement has no effect, did you mean to use `:=`? + ┌─ :29:27 + │ +29 │ WHILE TRUE DO value = 1; END_WHILE + │ ^^^^^^^^^ This statement has no effect, did you mean to use `:=`? + + diff --git a/src/validation/tests/variable_validation_tests.rs b/src/validation/tests/variable_validation_tests.rs index a054c7d922..1ac87a2004 100644 --- a/src/validation/tests/variable_validation_tests.rs +++ b/src/validation/tests/variable_validation_tests.rs @@ -380,3 +380,42 @@ fn type_initializers_in_structs_are_validated() { assert_snapshot!(diagnostics); } + +#[test] +fn assignment_suggestion_for_equal_operation_with_no_effect() { + let diagnostics = parse_and_validate_buffered( + " + PROGRAM main + VAR + value : DINT; + condition : BOOL; + + // These Should work + arr_dint : ARRAY[0..5] OF DINT := [1 = 1, 2, 3, 4, 5 = 5]; + arr_bool : ARRAY[1..5] OF BOOL := [1 = 1, 2 = 2, 3 = 3, 4 = 4, 5 = 10]; + END_VAR + + // These should work + value := (condition = TRUE); + + IF condition = TRUE THEN (* ... *) END_IF + IF (condition = TRUE) THEN (* ... *) END_IF + IF ((condition = TRUE)) THEN (* ... *) END_IF + + IF condition = TRUE AND condition = TRUE THEN (* ... *) END_IF + IF (condition = TRUE) AND (condition = TRUE) THEN (* ... *) END_IF + IF ((condition = TRUE) AND (condition = TRUE)) THEN (* ... *) END_IF + + // These should NOT work + value = 1; + value = condition AND condition; + value = condition AND (condition = TRUE); + + IF TRUE THEN value = 1; END_IF + WHILE TRUE DO value = 1; END_WHILE + END_PROGRAM + ", + ); + + assert_snapshot!(diagnostics); +}