From c64eeadaeea02eb060f75af96c10af5e8a154170 Mon Sep 17 00:00:00 2001 From: Ghaith Hachem Date: Thu, 17 Sep 2026 07:01:31 +0000 Subject: [PATCH 1/6] fix(validation): type-check variable initializers Problem: An initializer whose type cannot be assigned to the declared variable type, such as `x : STRING := 3`, passes `--check` and aborts later in code generation. Only body assignments ran the assignment type check; declarations did not. Solution: Run the same assignment validation on every variable initializer. `REFERENCE TO` and alias declarations bind a reference and keep their own check, and the lowered copies of initializers are skipped so each mismatch is reported once at its declaration. Inline pointer types no longer include their initializer in the location used for type names in diagnostics. Refs: PRG-4728 Co-Authored-By: Claude Fable 5.1 --- src/parser.rs | 4 +- src/validation/statement.rs | 7 +- .../tests/pointer_validation_tests.rs | 448 +++++++++++++++++- ...overflows__overflows_with_expressions.snap | 30 ++ ...ts__overflows__overflows_with_globals.snap | 6 + ...s__overflows__overflows_with_literals.snap | 54 +++ .../tests/variable_validation_tests.rs | 108 ++++- src/validation/variable.rs | 15 +- .../single/init/initializer_type_mismatch.st | 47 ++ 9 files changed, 688 insertions(+), 31 deletions(-) create mode 100644 tests/lit/single/init/initializer_type_mismatch.st diff --git a/src/parser.rs b/src/parser.rs index 62ac7bbd636..6304471eb6e 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1169,6 +1169,7 @@ fn parse_pointer_definition( is_function: bool, ) -> Option<(DataTypeDeclaration, Option)> { parse_data_type_definition(lexer, None).map(|(decl, initializer)| { + let end = decl.get_location().to_range().map_or(lexer.last_range.end, |range| range.end); ( DataTypeDeclaration::Definition { data_type: Box::new(DataType::PointerType { @@ -1178,8 +1179,7 @@ fn parse_pointer_definition( type_safe, is_function, }), - // FIXME: this currently includes the initializer in the sourcelocation, resulting in 'REF_TO A := B' when creating a slice - location: lexer.source_range_factory.create_range(start_pos..lexer.last_range.end), + location: lexer.source_range_factory.create_range(start_pos..end), scope: lexer.scope.clone(), }, initializer, diff --git a/src/validation/statement.rs b/src/validation/statement.rs index 2ec912a7c15..0d633ec988a 100644 --- a/src/validation/statement.rs +++ b/src/validation/statement.rs @@ -137,7 +137,10 @@ pub fn visit_statement( visit_statement(validator, &data.left, context); visit_statement(validator, &data.right, context); - validate_assignment(validator, &data.right, Some(&data.left), &statement.location, context); + // lowered copies of variable initializers are validated at their declaration + if !statement.location.is_internal() { + validate_assignment(validator, &data.right, Some(&data.left), &statement.location, context); + } validate_array_assignment(validator, context, statement); } AstStatement::OutputAssignment(data) => { @@ -1557,7 +1560,7 @@ fn validate_alias_assignment( } } -fn validate_assignment( +pub(super) fn validate_assignment( validator: &mut Validator, right: &AstNode, left: Option<&AstNode>, diff --git a/src/validation/tests/pointer_validation_tests.rs b/src/validation/tests/pointer_validation_tests.rs index ab50f6e1c5d..a31a5ca13a0 100644 --- a/src/validation/tests/pointer_validation_tests.rs +++ b/src/validation/tests/pointer_validation_tests.rs @@ -273,16 +273,280 @@ fn pointer_to_validates_assignment_when_not_dealing_with_memory_address_in_initi .filter(|diagnostic| !matches!(diagnostic.error_code, "E015" | "E065")) .collect::>(); - // TODO: Validation between variable initialization / assignment in the variable block versus body are handled differently, these - // must be unified at some point. Once done, this assertion MUST fail and be identical to the test - // `pointer_to_validates_assignment_when_not_dealing_with_memory_address_in_body`. Furthermore, we should unify these tests by - // having two source code strings, one for the variable block and one for the implementation, run validation on both of them and - // assert that the diagnostics (with exception of the location) are identical with regards to their assignment validation. - assert_eq!( - filtered_diagnostics, - Vec::new(), - "these are empty for now, but eventually should be the same as the test below" - ); + insta::assert_debug_snapshot!(filtered_diagnostics, @r#" + [ + Diagnostic { + inner: DiagnosticsInner { + message: "Invalid assignment: cannot assign 'STRING' to 'POINTER TO STRING'", + primary_location: SourceLocation { + span: Range(24:55 - 24:60), + file: Some( + "", + ), + }, + secondary_locations: None, + error_code: "E037", + sub_diagnostics: [], + internal_error: None, + }, + }, + Diagnostic { + inner: DiagnosticsInner { + message: "Invalid assignment: cannot assign 'STRING' to 'POINTER TO INT'", + primary_location: SourceLocation { + span: Range(25:55 - 25:60), + file: Some( + "", + ), + }, + secondary_locations: None, + error_code: "E037", + sub_diagnostics: [], + internal_error: None, + }, + }, + Diagnostic { + inner: DiagnosticsInner { + message: "Invalid assignment: cannot assign 'STRING' to 'POINTER TO REAL'", + primary_location: SourceLocation { + span: Range(26:55 - 26:60), + file: Some( + "", + ), + }, + secondary_locations: None, + error_code: "E037", + sub_diagnostics: [], + internal_error: None, + }, + }, + Diagnostic { + inner: DiagnosticsInner { + message: "Invalid assignment: cannot assign 'STRING' to 'POINTER TO TIME'", + primary_location: SourceLocation { + span: Range(27:55 - 27:60), + file: Some( + "", + ), + }, + secondary_locations: None, + error_code: "E037", + sub_diagnostics: [], + internal_error: None, + }, + }, + Diagnostic { + inner: DiagnosticsInner { + message: "Invalid assignment: cannot assign 'STRING' to 'POINTER TO TIME_OF_DAY'", + primary_location: SourceLocation { + span: Range(28:55 - 28:60), + file: Some( + "", + ), + }, + secondary_locations: None, + error_code: "E037", + sub_diagnostics: [], + internal_error: None, + }, + }, + Diagnostic { + inner: DiagnosticsInner { + message: "Invalid assignment: cannot assign 'STRING' to 'POINTER TO DATE'", + primary_location: SourceLocation { + span: Range(29:55 - 29:60), + file: Some( + "", + ), + }, + secondary_locations: None, + error_code: "E037", + sub_diagnostics: [], + internal_error: None, + }, + }, + Diagnostic { + inner: DiagnosticsInner { + message: "Invalid assignment: cannot assign 'STRING' to 'POINTER TO Position1D'", + primary_location: SourceLocation { + span: Range(30:55 - 30:60), + file: Some( + "", + ), + }, + secondary_locations: None, + error_code: "E037", + sub_diagnostics: [], + internal_error: None, + }, + }, + Diagnostic { + inner: DiagnosticsInner { + message: "Invalid assignment: cannot assign 'STRING' to 'POINTER TO Position2D'", + primary_location: SourceLocation { + span: Range(31:55 - 31:60), + file: Some( + "", + ), + }, + secondary_locations: None, + error_code: "E037", + sub_diagnostics: [], + internal_error: None, + }, + }, + Diagnostic { + inner: DiagnosticsInner { + message: "Invalid assignment: cannot assign 'STRING' to 'POINTER TO Position3D'", + primary_location: SourceLocation { + span: Range(32:55 - 32:60), + file: Some( + "", + ), + }, + secondary_locations: None, + error_code: "E037", + sub_diagnostics: [], + internal_error: None, + }, + }, + Diagnostic { + inner: DiagnosticsInner { + message: "Invalid assignment: cannot assign 'STRING' to 'REF_TO STRING'", + primary_location: SourceLocation { + span: Range(34:55 - 34:60), + file: Some( + "", + ), + }, + secondary_locations: None, + error_code: "E037", + sub_diagnostics: [], + internal_error: None, + }, + }, + Diagnostic { + inner: DiagnosticsInner { + message: "Invalid assignment: cannot assign 'STRING' to 'REF_TO INT'", + primary_location: SourceLocation { + span: Range(35:55 - 35:60), + file: Some( + "", + ), + }, + secondary_locations: None, + error_code: "E037", + sub_diagnostics: [], + internal_error: None, + }, + }, + Diagnostic { + inner: DiagnosticsInner { + message: "Invalid assignment: cannot assign 'STRING' to 'REF_TO REAL'", + primary_location: SourceLocation { + span: Range(36:55 - 36:60), + file: Some( + "", + ), + }, + secondary_locations: None, + error_code: "E037", + sub_diagnostics: [], + internal_error: None, + }, + }, + Diagnostic { + inner: DiagnosticsInner { + message: "Invalid assignment: cannot assign 'STRING' to 'REF_TO TIME'", + primary_location: SourceLocation { + span: Range(37:55 - 37:60), + file: Some( + "", + ), + }, + secondary_locations: None, + error_code: "E037", + sub_diagnostics: [], + internal_error: None, + }, + }, + Diagnostic { + inner: DiagnosticsInner { + message: "Invalid assignment: cannot assign 'STRING' to 'REF_TO TIME_OF_DAY'", + primary_location: SourceLocation { + span: Range(38:55 - 38:60), + file: Some( + "", + ), + }, + secondary_locations: None, + error_code: "E037", + sub_diagnostics: [], + internal_error: None, + }, + }, + Diagnostic { + inner: DiagnosticsInner { + message: "Invalid assignment: cannot assign 'STRING' to 'REF_TO DATE'", + primary_location: SourceLocation { + span: Range(39:55 - 39:60), + file: Some( + "", + ), + }, + secondary_locations: None, + error_code: "E037", + sub_diagnostics: [], + internal_error: None, + }, + }, + Diagnostic { + inner: DiagnosticsInner { + message: "Invalid assignment: cannot assign 'STRING' to 'REF_TO Position1D'", + primary_location: SourceLocation { + span: Range(40:55 - 40:60), + file: Some( + "", + ), + }, + secondary_locations: None, + error_code: "E037", + sub_diagnostics: [], + internal_error: None, + }, + }, + Diagnostic { + inner: DiagnosticsInner { + message: "Invalid assignment: cannot assign 'STRING' to 'REF_TO Position2D'", + primary_location: SourceLocation { + span: Range(41:55 - 41:60), + file: Some( + "", + ), + }, + secondary_locations: None, + error_code: "E037", + sub_diagnostics: [], + internal_error: None, + }, + }, + Diagnostic { + inner: DiagnosticsInner { + message: "Invalid assignment: cannot assign 'STRING' to 'REF_TO Position3D'", + primary_location: SourceLocation { + span: Range(42:55 - 42:60), + file: Some( + "", + ), + }, + secondary_locations: None, + error_code: "E037", + sub_diagnostics: [], + internal_error: None, + }, + }, + ] + "#); } #[test] @@ -690,16 +954,160 @@ fn pointer_to_validates_assignment_of_non_pointer_sized_integers_in_initializer( .filter(|diagnostic| !matches!(diagnostic.error_code, "E015")) .collect::>(); - // TODO: Validation between variable initialization / assignment in the variable block versus body are handled differently, these - // must be unified at some point. Once done, this assertion MUST fail and be identical to the test - // `pointer_to_validates_assignment_of_non_pointer_sized_integers`. Furthermore, we should unify these tests by - // having two source code strings, one for the variable block and one for the implementation, run validation on both of them and - // assert that the diagnostics (with exception of the location) are identical with regards to their assignment validation. - assert_eq!( - filtered_diagnostics, - Vec::new(), - "these are empty for now, but eventually should be the same as the test below" - ); + insta::assert_debug_snapshot!(filtered_diagnostics, @r#" + [ + Diagnostic { + inner: DiagnosticsInner { + message: "The type SINT 8 is too small to be stored in a Pointer", + primary_location: SourceLocation { + span: Range(8:45 - 8:53), + file: Some( + "", + ), + }, + secondary_locations: None, + error_code: "E065", + sub_diagnostics: [], + internal_error: None, + }, + }, + Diagnostic { + inner: DiagnosticsInner { + message: "The type USINT 8 is too small to be stored in a Pointer", + primary_location: SourceLocation { + span: Range(9:46 - 9:55), + file: Some( + "", + ), + }, + secondary_locations: None, + error_code: "E065", + sub_diagnostics: [], + internal_error: None, + }, + }, + Diagnostic { + inner: DiagnosticsInner { + message: "The type BYTE 8 is too small to be stored in a Pointer", + primary_location: SourceLocation { + span: Range(10:45 - 10:53), + file: Some( + "", + ), + }, + secondary_locations: None, + error_code: "E065", + sub_diagnostics: [], + internal_error: None, + }, + }, + Diagnostic { + inner: DiagnosticsInner { + message: "The type INT 16 is too small to be stored in a Pointer", + primary_location: SourceLocation { + span: Range(17:44 - 17:51), + file: Some( + "", + ), + }, + secondary_locations: None, + error_code: "E065", + sub_diagnostics: [], + internal_error: None, + }, + }, + Diagnostic { + inner: DiagnosticsInner { + message: "The type UINT 16 is too small to be stored in a Pointer", + primary_location: SourceLocation { + span: Range(18:45 - 18:53), + file: Some( + "", + ), + }, + secondary_locations: None, + error_code: "E065", + sub_diagnostics: [], + internal_error: None, + }, + }, + Diagnostic { + inner: DiagnosticsInner { + message: "The type WORD 16 is too small to be stored in a Pointer", + primary_location: SourceLocation { + span: Range(19:45 - 19:53), + file: Some( + "", + ), + }, + secondary_locations: None, + error_code: "E065", + sub_diagnostics: [], + internal_error: None, + }, + }, + Diagnostic { + inner: DiagnosticsInner { + message: "The type DINT 32 is too small to be stored in a Pointer", + primary_location: SourceLocation { + span: Range(27:45 - 27:53), + file: Some( + "", + ), + }, + secondary_locations: None, + error_code: "E065", + sub_diagnostics: [], + internal_error: None, + }, + }, + Diagnostic { + inner: DiagnosticsInner { + message: "The type UDINT 32 is too small to be stored in a Pointer", + primary_location: SourceLocation { + span: Range(28:46 - 28:55), + file: Some( + "", + ), + }, + secondary_locations: None, + error_code: "E065", + sub_diagnostics: [], + internal_error: None, + }, + }, + Diagnostic { + inner: DiagnosticsInner { + message: "The type DWORD 32 is too small to be stored in a Pointer", + primary_location: SourceLocation { + span: Range(29:46 - 29:55), + file: Some( + "", + ), + }, + secondary_locations: None, + error_code: "E065", + sub_diagnostics: [], + internal_error: None, + }, + }, + Diagnostic { + inner: DiagnosticsInner { + message: "The type REAL 32 is too small to be stored in a Pointer", + primary_location: SourceLocation { + span: Range(30:45 - 30:53), + file: Some( + "", + ), + }, + secondary_locations: None, + error_code: "E065", + sub_diagnostics: [], + internal_error: None, + }, + }, + ] + "#); } #[test] diff --git a/src/validation/tests/snapshots/rusty__validation__tests__variable_validation_tests__overflows__overflows_with_expressions.snap b/src/validation/tests/snapshots/rusty__validation__tests__variable_validation_tests__overflows__overflows_with_expressions.snap index c29cf9cc2f0..8bce8570e68 100644 --- a/src/validation/tests/snapshots/rusty__validation__tests__variable_validation_tests__overflows__overflows_with_expressions.snap +++ b/src/validation/tests/snapshots/rusty__validation__tests__variable_validation_tests__overflows__overflows_with_expressions.snap @@ -97,3 +97,33 @@ warning[E039]: This will overflow for type ULINT │ 26 │ max_ulint : ULINT := ((18_446_744_073_709_551_615 * 1) * 2); // 18_446_744_073_709_551_615 │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This will overflow for type ULINT + +warning[E067]: Implicit downcast from 'UINT' to 'USINT'. + ┌─ :8:43 + │ +8 │ max_usint : USINT := ((256 * 1) * 2); // 256 + │ ^^^ Implicit downcast from 'UINT' to 'USINT'. + +warning[E067]: Implicit downcast from 'UDINT' to 'UINT'. + ┌─ :14:42 + │ +14 │ max_uint : UINT := ((65_536 * 1) * 2); // 65536 + │ ^^^^^^ Implicit downcast from 'UDINT' to 'UINT'. + +warning[E067]: Implicit downcast from 'LINT' to 'DINT'. + ┌─ :17:42 + │ +17 │ min_dint : DINT := ((-2_147_483_649 * 1) * 2); // -2_147_483_648 + │ ^^^^^^^^^^^^^^ Implicit downcast from 'LINT' to 'DINT'. + +warning[E067]: Implicit downcast from 'LINT' to 'DINT'. + ┌─ :18:43 + │ +18 │ max_dint : DINT := (( 2_147_483_648 * 1) * 2); // 2_147_483_647 + │ ^^^^^^^^^^^^^ Implicit downcast from 'LINT' to 'DINT'. + +warning[E067]: Implicit downcast from 'ULINT' to 'UDINT'. + ┌─ :20:43 + │ +20 │ max_udint : UDINT := ((4_294_967_296 * 1) * 2); // 4_294_967_296 + │ ^^^^^^^^^^^^^ Implicit downcast from 'ULINT' to 'UDINT'. diff --git a/src/validation/tests/snapshots/rusty__validation__tests__variable_validation_tests__overflows__overflows_with_globals.snap b/src/validation/tests/snapshots/rusty__validation__tests__variable_validation_tests__overflows__overflows_with_globals.snap index f6d14a2d357..5fc6963a01a 100644 --- a/src/validation/tests/snapshots/rusty__validation__tests__variable_validation_tests__overflows__overflows_with_globals.snap +++ b/src/validation/tests/snapshots/rusty__validation__tests__variable_validation_tests__overflows__overflows_with_globals.snap @@ -13,3 +13,9 @@ warning[E039]: This will overflow for type INT │ 4 │ b : INT := 32767 + 1; │ ^^^^^^^^^ This will overflow for type INT + +warning[E067]: Implicit downcast from 'DINT' to 'INT'. + ┌─ :3:24 + │ +3 │ a : INT := 32768; + │ ^^^^^ Implicit downcast from 'DINT' to 'INT'. diff --git a/src/validation/tests/snapshots/rusty__validation__tests__variable_validation_tests__overflows__overflows_with_literals.snap b/src/validation/tests/snapshots/rusty__validation__tests__variable_validation_tests__overflows__overflows_with_literals.snap index 2c3ce2b759e..c5c3a9b5b1a 100644 --- a/src/validation/tests/snapshots/rusty__validation__tests__variable_validation_tests__overflows__overflows_with_literals.snap +++ b/src/validation/tests/snapshots/rusty__validation__tests__variable_validation_tests__overflows__overflows_with_literals.snap @@ -97,3 +97,57 @@ warning[E039]: This will overflow for type ULINT │ 26 │ max_ulint : ULINT := 18_446_744_073_709_551_616; // 18_446_744_073_709_551_615 │ ^^^^^^^^^^^^^^^^^^^^^^^^^^ This will overflow for type ULINT + +warning[E067]: Implicit downcast from 'INT' to 'SINT'. + ┌─ :5:40 + │ +5 │ min_sint : SINT := -129; // -128 + │ ^^^^ Implicit downcast from 'INT' to 'SINT'. + +warning[E067]: Implicit downcast from 'INT' to 'SINT'. + ┌─ :6:41 + │ +6 │ max_sint : SINT := 128; // 127 + │ ^^^ Implicit downcast from 'INT' to 'SINT'. + +warning[E067]: Implicit downcast from 'UINT' to 'USINT'. + ┌─ :8:41 + │ +8 │ max_usint : USINT := 257; // 256 + │ ^^^ Implicit downcast from 'UINT' to 'USINT'. + +warning[E067]: Implicit downcast from 'DINT' to 'INT'. + ┌─ :11:39 + │ +11 │ min_int : INT := -32_769; // -32768 + │ ^^^^^^^ Implicit downcast from 'DINT' to 'INT'. + +warning[E067]: Implicit downcast from 'DINT' to 'INT'. + ┌─ :12:39 + │ +12 │ max_int : INT := 32_768; // 32767 + │ ^^^^^^ Implicit downcast from 'DINT' to 'INT'. + +warning[E067]: Implicit downcast from 'UDINT' to 'UINT'. + ┌─ :14:40 + │ +14 │ max_uint : UINT := 65_537; // 65536 + │ ^^^^^^ Implicit downcast from 'UDINT' to 'UINT'. + +warning[E067]: Implicit downcast from 'LINT' to 'DINT'. + ┌─ :17:40 + │ +17 │ min_dint : DINT := -2_147_483_649; // -2_147_483_648 + │ ^^^^^^^^^^^^^^ Implicit downcast from 'LINT' to 'DINT'. + +warning[E067]: Implicit downcast from 'LINT' to 'DINT'. + ┌─ :18:41 + │ +18 │ max_dint : DINT := 2_147_483_648; // 2_147_483_647 + │ ^^^^^^^^^^^^^ Implicit downcast from 'LINT' to 'DINT'. + +warning[E067]: Implicit downcast from 'ULINT' to 'UDINT'. + ┌─ :20:41 + │ +20 │ max_udint : UDINT := 4_294_967_296; // 4_294_967_296 + │ ^^^^^^^^^^^^^ Implicit downcast from 'ULINT' to 'UDINT'. diff --git a/src/validation/tests/variable_validation_tests.rs b/src/validation/tests/variable_validation_tests.rs index 3120035f651..56f35800fd3 100644 --- a/src/validation/tests/variable_validation_tests.rs +++ b/src/validation/tests/variable_validation_tests.rs @@ -576,7 +576,13 @@ fn invalid_initial_constant_values_in_pou_variables() { "#, ); - assert_snapshot!(diagnostics, @r" + assert_snapshot!(diagnostics, @" + warning[E067]: Implicit downcast from 'DINT' to 'INT'. + ┌─ :10:28 + │ + 10 │ my_len: INT := LEN + 4; //cannot be evaluated at compile time! + │ ^^^ Implicit downcast from 'DINT' to 'INT'. + error[E033]: Unresolved constant `my_len` variable: `LEN` is no const reference ┌─ :10:28 │ @@ -899,7 +905,13 @@ fn unresolved_references_to_const_builtins_in_initializer_are_reported() { "#, ); - assert_snapshot!(diagnostics, @r" + assert_snapshot!(diagnostics, @" + warning[E090]: Pointers REF_TO BOOL and VOID have different types + ┌─ :4:38 + │ + 4 │ bar : REF_TO BOOL := REF(gb); // unresolved reference to gb + │ ^^^^^^^ Pointers REF_TO BOOL and VOID have different types + error[E048]: Could not resolve reference to gb ┌─ :4:42 │ @@ -1012,12 +1024,18 @@ fn trying_to_initialize_a_pointer_with_builtin_ref_with_type_mismatch_leads_to_e "#, ); - assert_snapshot!(diagnostics, @r" - error[E037]: Invalid assignment: cannot assign 'DINT' to 'REF_TO STRING := REF(a)' + assert_snapshot!(diagnostics, @" + warning[E090]: Pointers REF_TO STRING and DINT have different types ┌─ :7:36 │ 7 │ bar : REF_TO STRING := REF(a); - │ ^^^^^^ Invalid assignment: cannot assign 'DINT' to 'REF_TO STRING := REF(a)' + │ ^^^^^^ Pointers REF_TO STRING and DINT have different types + + error[E037]: Invalid assignment: cannot assign 'DINT' to 'REF_TO STRING' + ┌─ :7:36 + │ + 7 │ bar : REF_TO STRING := REF(a); + │ ^^^^^^ Invalid assignment: cannot assign 'DINT' to 'REF_TO STRING' "); } @@ -2017,3 +2035,83 @@ fn fb_var_temp_visible_inside_fb_body_and_actions() { assert!(diagnostics.is_empty(), "expected clean diagnostics, got:\n{diagnostics}"); } + +#[test] +fn initializers_with_incompatible_types_are_reported() { + let diagnostics = parse_and_validate_buffered( + r#" + TYPE MyStruct : STRUCT + member : STRING := 3; + END_STRUCT END_TYPE + + VAR_GLOBAL + gInt : DINT := 'abc'; + END_VAR + + FUNCTION_BLOCK MyFb + VAR + fbStr : WSTRING := 3; + END_VAR + END_FUNCTION_BLOCK + + PROGRAM mainProg + VAR + str : STRING := 3; + strFromReal : STRING := 3.5; + strFromBool : STRING := TRUE; + int : INT := 'abc'; + validStr : STRING := 'abc'; + validInt : DINT := 3; + validReal : LREAL := 3; + validStruct : MyStruct := (member := 'abc'); + validArray : ARRAY[0..1] OF DINT := [1, 2]; + validFb : MyFb; + END_VAR + END_PROGRAM + "#, + ); + + assert_snapshot!(diagnostics, @" + error[E037]: Invalid assignment: cannot assign 'DINT' to 'WSTRING' + ┌─ :12:32 + │ + 12 │ fbStr : WSTRING := 3; + │ ^ Invalid assignment: cannot assign 'DINT' to 'WSTRING' + + error[E037]: Invalid assignment: cannot assign 'DINT' to 'STRING' + ┌─ :18:29 + │ + 18 │ str : STRING := 3; + │ ^ Invalid assignment: cannot assign 'DINT' to 'STRING' + + error[E037]: Invalid assignment: cannot assign 'REAL' to 'STRING' + ┌─ :19:37 + │ + 19 │ strFromReal : STRING := 3.5; + │ ^^^ Invalid assignment: cannot assign 'REAL' to 'STRING' + + error[E037]: Invalid assignment: cannot assign 'BOOL' to 'STRING' + ┌─ :20:37 + │ + 20 │ strFromBool : STRING := TRUE; + │ ^^^^ Invalid assignment: cannot assign 'BOOL' to 'STRING' + + error[E037]: Invalid assignment: cannot assign 'STRING' to 'INT' + ┌─ :21:26 + │ + 21 │ int : INT := 'abc'; + │ ^^^^^ Invalid assignment: cannot assign 'STRING' to 'INT' + + error[E037]: Invalid assignment: cannot assign 'DINT' to 'STRING' + ┌─ :3:32 + │ + 3 │ member : STRING := 3; + │ ^ Invalid assignment: cannot assign 'DINT' to 'STRING' + + error[E037]: Invalid assignment: cannot assign 'STRING' to 'DINT' + ┌─ :7:28 + │ + 7 │ gInt : DINT := 'abc'; + │ ^^^^^ Invalid assignment: cannot assign 'STRING' to 'DINT' + "); +} diff --git a/src/validation/variable.rs b/src/validation/variable.rs index 4be80f9b047..76faf51509e 100644 --- a/src/validation/variable.rs +++ b/src/validation/variable.rs @@ -6,7 +6,7 @@ use plc_diagnostics::diagnostics::Diagnostic; use super::{ array::validate_array_assignment, - statement::{validate_assignment_mismatch, visit_statement}, + statement::{validate_assignment, validate_assignment_mismatch, visit_statement}, types::{data_type_is_fb_or_class_instance, visit_data_type_declaration}, ValidationContext, Validator, Validators, }; @@ -413,11 +413,22 @@ fn validate_variable( .as_ref() .is_some_and(|initializer| initializer.is_struct_literal_initializer()); + // `REFERENCE TO` and alias declarations bind a reference instead of assigning a value, their + // initializer is checked by `validate_reference_to_declaration` + let is_reference_declaration = context + .index + .find_effective_type_by_name(v_entry.get_type_name()) + .map(|ty| ty.get_type_information()) + .is_some_and(|ty| ty.is_reference_to() || ty.is_alias()); + if let Some(initializer) = &variable.initializer { // Assume `foo : ARRAY[1..5] OF DINT := [...]`, here the first function call validates the - // assignment as a whole whereas the second function call (`visit_statement`) validates the + // assignment as a whole whereas the last function call (`visit_statement`) validates the // initializer in case it has further sub-assignments. validate_array_assignment(validator, context, variable); + if !is_reference_declaration { + validate_assignment(validator, initializer, None, &initializer.location, context); + } visit_statement(validator, initializer, context); } diff --git a/tests/lit/single/init/initializer_type_mismatch.st b/tests/lit/single/init/initializer_type_mismatch.st new file mode 100644 index 00000000000..7fe745973f7 --- /dev/null +++ b/tests/lit/single/init/initializer_type_mismatch.st @@ -0,0 +1,47 @@ +// RUN: rm -f %t.out +// RUN: not %COMPILE --error-format clang %s 2>&1 | %CHECK --implicit-check-not='{{.*}}error[E037]{{.*}}' %s +// RUN: test ! -e %t.out + +// A variable initializer must have a type that is assignable to the declared type, no matter +// where the variable is declared. These declarations used to pass validation and then abort in +// code generation. + +TYPE MyStruct : STRUCT + // CHECK-DAG: {{.*}}initializer_type_mismatch.st:[[@LINE+1]]:{{[0-9]+}}:{{.*}}error[E037]: Invalid assignment: cannot assign 'DINT' to 'STRING' + member : STRING := 3; +END_STRUCT END_TYPE + +VAR_GLOBAL + // CHECK-DAG: {{.*}}initializer_type_mismatch.st:[[@LINE+1]]:{{[0-9]+}}:{{.*}}error[E037]: Invalid assignment: cannot assign 'STRING' to 'DINT' + gInt : DINT := 'abc'; +END_VAR + +FUNCTION_BLOCK MyFb +VAR + // CHECK-DAG: {{.*}}initializer_type_mismatch.st:[[@LINE+1]]:{{[0-9]+}}:{{.*}}error[E037]: Invalid assignment: cannot assign 'DINT' to 'WSTRING' + fbStr : WSTRING := 3; +END_VAR +END_FUNCTION_BLOCK + +PROGRAM mainProg +VAR + // CHECK-DAG: {{.*}}initializer_type_mismatch.st:[[@LINE+1]]:{{[0-9]+}}:{{.*}}error[E037]: Invalid assignment: cannot assign 'DINT' to 'STRING' + str : STRING := 3; + // CHECK-DAG: {{.*}}initializer_type_mismatch.st:[[@LINE+1]]:{{[0-9]+}}:{{.*}}error[E037]: Invalid assignment: cannot assign 'REAL' to 'STRING' + strFromReal : STRING := 3.5; + // CHECK-DAG: {{.*}}initializer_type_mismatch.st:[[@LINE+1]]:{{[0-9]+}}:{{.*}}error[E037]: Invalid assignment: cannot assign 'BOOL' to 'STRING' + strFromBool : STRING := TRUE; + // CHECK-DAG: {{.*}}initializer_type_mismatch.st:[[@LINE+1]]:{{[0-9]+}}:{{.*}}error[E037]: Invalid assignment: cannot assign 'STRING' to 'INT' + int : INT := 'abc'; + + // Compatible initializers are not reported + validStr : STRING := 'abc'; + validInt : DINT := 3; + validReal : LREAL := 3; + validStruct : MyStruct := (member := 'abc'); + validArray : ARRAY[0..1] OF DINT := [1, 2]; + validFb : MyFb; +END_VAR +END_PROGRAM + +// CHECK: error: Compilation aborted due to critical errors From 671a247790bdafd63a31508799673ffee3d00602 Mon Sep 17 00:00:00 2001 From: Ghaith Hachem Date: Thu, 17 Sep 2026 09:21:08 +0000 Subject: [PATCH 2/6] fix(codegen): cast array literal elements to the element type Problem: A BOOL or REAL literal inside an array initializer, such as `ARRAY[0..1] OF REAL := [TRUE, FALSE]`, passes `--check` and then either panics in code generation or produces an ill-typed constant that breaks the link step. Scalar initializers already convert such literals. Solution: Cast every numeric element of an array literal to the array's element type before the constant array is built, in declarations and in body assignments alike. Refs: PRG-4728 Co-Authored-By: Claude Fable 5.1 --- .../generators/expression_generator.rs | 15 ++++- .../initialization_test/pou_initializers.rs | 55 +++++++++++++++++++ ...es_in_single_dimension_array_variable.snap | 2 +- .../init/numeric_initializer_literal_casts.st | 36 ++++++++++++ 4 files changed, 105 insertions(+), 3 deletions(-) create mode 100644 tests/lit/single/init/numeric_initializer_literal_casts.st diff --git a/src/codegen/generators/expression_generator.rs b/src/codegen/generators/expression_generator.rs index d5cffbf344e..6d31f2714d1 100644 --- a/src/codegen/generators/expression_generator.rs +++ b/src/codegen/generators/expression_generator.rs @@ -2907,8 +2907,19 @@ impl<'ink, 'b> ExpressionCodeGenerator<'ink, 'b> { ); for e in elements { //generate with correct type hint using context-free generator - let value = ctx_free_gen.generate_literal(e)?; - v.push(value.get_basic_value_enum()); + let value = ctx_free_gen.generate_literal(e)?.get_basic_value_enum(); + // numeric literals take the type of the array element, e.g. `TRUE` in an ARRAY OF REAL + let value = match value { + BasicValueEnum::IntValue(_) | BasicValueEnum::FloatValue(_) => cast_if_needed!( + ctx_free_gen, + inner_type, + self.annotations.get_type_or_void(e, self.index), + value, + self.annotations.get(e) + )?, + _ => value, + }; + v.push(value); } if v.len() < expected_len { diff --git a/src/codegen/tests/initialization_test/pou_initializers.rs b/src/codegen/tests/initialization_test/pou_initializers.rs index f6dce2827eb..b0f02dacf9c 100644 --- a/src/codegen/tests/initialization_test/pou_initializers.rs +++ b/src/codegen/tests/initialization_test/pou_initializers.rs @@ -489,3 +489,58 @@ fn pou_local_constant_as_array_bound_of_a_constructed_element_type() { assert!(result.contains("icmp sgt i32 %load___prog_arr__idx0, 5"), "{result}"); assert!(!result.contains("DINT_GREATER"), "{result}"); } + +#[test] +fn numeric_array_literal_elements_are_cast_to_the_element_type() { + let result = codegen( + r#" + FUNCTION main : DINT + VAR + dints : ARRAY[0..1] OF DINT := [TRUE, FALSE]; + reals : ARRAY[0..1] OF REAL := [TRUE, FALSE]; + ints : ARRAY[0..1] OF INT := [1.5, 2]; + later : ARRAY[0..1] OF LREAL; + END_VAR + later := [TRUE, FALSE]; + END_FUNCTION + "#, + ); + + filtered_assert_snapshot!(result, @r#" + ; ModuleID = '' + source_filename = "" + target datalayout = "[filtered]" + target triple = "[filtered]" + + @__main.dints__init = unnamed_addr constant [2 x i32] [i32 1, i32 0] + @__main.reals__init = unnamed_addr constant [2 x float] [float 1.000000e+00, float 0.000000e+00] + @__main.ints__init = unnamed_addr constant [2 x i16] [i16 1, i16 2] + @.const_init = private unnamed_addr constant [2 x double] [double 1.000000e+00, double 0.000000e+00] + + define i32 @main() { + entry: + %main = alloca i32, align [filtered] + %dints = alloca [2 x i32], align [filtered] + %reals = alloca [2 x float], align [filtered] + %ints = alloca [2 x i16], align [filtered] + %later = alloca [2 x double], align [filtered] + call void @llvm.memcpy.p0.p0.i64(ptr align [filtered] %dints, ptr align [filtered] @__main.dints__init, i64 ptrtoint (ptr getelementptr ([2 x i32], ptr null, i32 1) to i64), i1 false) + call void @llvm.memcpy.p0.p0.i64(ptr align [filtered] %reals, ptr align [filtered] @__main.reals__init, i64 ptrtoint (ptr getelementptr ([2 x float], ptr null, i32 1) to i64), i1 false) + call void @llvm.memcpy.p0.p0.i64(ptr align [filtered] %ints, ptr align [filtered] @__main.ints__init, i64 ptrtoint (ptr getelementptr ([2 x i16], ptr null, i32 1) to i64), i1 false) + call void @llvm.memset.p0.i64(ptr align [filtered] %later, i8 0, i64 ptrtoint (ptr getelementptr ([2 x double], ptr null, i32 1) to i64), i1 false) + store i32 0, ptr %main, align [filtered] + call void @llvm.memcpy.p0.p0.i64(ptr align [filtered] %later, ptr align [filtered] @.const_init, i64 ptrtoint (ptr getelementptr ([2 x double], ptr null, i32 1) to i64), i1 false) + %main_ret = load i32, ptr %main, align [filtered] + ret i32 %main_ret + } + + ; Function Attrs: nocallback nofree nounwind willreturn memory(argmem: readwrite) + declare void @llvm.memcpy.p0.p0.i64(ptr noalias writeonly captures(none), ptr noalias readonly captures(none), i64, i1 immarg) #0 + + ; Function Attrs: nocallback nofree nounwind willreturn memory(argmem: write) + declare void @llvm.memset.p0.i64(ptr writeonly captures(none), i8, i64, i1 immarg) #1 + + attributes #0 = { nocallback nofree nounwind willreturn memory(argmem: readwrite) } + attributes #1 = { nocallback nofree nounwind willreturn memory(argmem: write) } + "#); +} diff --git a/src/codegen/tests/initialization_test/snapshots/rusty__codegen__tests__initialization_test__type_initializers__initial_values_in_single_dimension_array_variable.snap b/src/codegen/tests/initialization_test/snapshots/rusty__codegen__tests__initialization_test__type_initializers__initial_values_in_single_dimension_array_variable.snap index 02c79a6f049..feb2f8b3a15 100644 --- a/src/codegen/tests/initialization_test/snapshots/rusty__codegen__tests__initialization_test__type_initializers__initial_values_in_single_dimension_array_variable.snap +++ b/src/codegen/tests/initialization_test/snapshots/rusty__codegen__tests__initialization_test__type_initializers__initial_values_in_single_dimension_array_variable.snap @@ -14,4 +14,4 @@ target triple = "[filtered]" @e = global [3 x i8] c"\01\02\03" @f = global [3 x i16] [i16 1, i16 2, i16 3] @g = global [3 x i64] [i64 1, i64 2, i64 3] -@h = global [3 x i8] [i8 true, i8 false, i8 true] +@h = global [3 x i8] c"\01\00\01" diff --git a/tests/lit/single/init/numeric_initializer_literal_casts.st b/tests/lit/single/init/numeric_initializer_literal_casts.st new file mode 100644 index 00000000000..107b00d786c --- /dev/null +++ b/tests/lit/single/init/numeric_initializer_literal_casts.st @@ -0,0 +1,36 @@ +// RUN: %COMPILE %s && %RUN | %CHECK %s + +// BOOL and REAL literals are converted to the declared numeric type, both as scalar initializers +// and as elements of array literals in declarations and in the body. + +FUNCTION main : DINT +VAR + a : DINT := TRUE; + b : INT := TRUE; + c : REAL := TRUE; + d : LREAL := FALSE; + + dints : ARRAY[0..1] OF DINT := [TRUE, FALSE]; + lints : ARRAY[0..1] OF LINT := [FALSE, TRUE]; + reals : ARRAY[0..1] OF REAL := [TRUE, FALSE]; + lreals : ARRAY[0..1] OF LREAL := [FALSE, TRUE]; + ints : ARRAY[0..1] OF INT := [1.5, 2]; + later : ARRAY[0..1] OF REAL; +END_VAR + later := [TRUE, FALSE]; + + // CHECK: scalars: 1 1 1.000000 0.000000 + printf('scalars: %d %d %f %f$N', a, b, c, d); + // CHECK: dints: 1 0 + printf('dints: %d %d$N', dints[0], dints[1]); + // CHECK: lints: 0 1 + printf('lints: %d %d$N', lints[0], lints[1]); + // CHECK: reals: 1.000000 0.000000 + printf('reals: %f %f$N', reals[0], reals[1]); + // CHECK: lreals: 0.000000 1.000000 + printf('lreals: %f %f$N', lreals[0], lreals[1]); + // CHECK: ints: 1 2 + printf('ints: %d %d$N', ints[0], ints[1]); + // CHECK: later: 1.000000 0.000000 + printf('later: %f %f$N', later[0], later[1]); +END_FUNCTION From c0535b695441294b0f67b818049efe5d3f74dfcb Mon Sep 17 00:00:00 2001 From: Ghaith Hachem Date: Thu, 17 Sep 2026 09:38:30 +0000 Subject: [PATCH 3/6] fix(validation): exclude pointer initializers from the type check Problem: Running the assignment validation on pointer-typed declarations turns previously accepted initializers such as `POINTER TO INT := 'foo'` into errors, and `REF_TO STRING := REF(a)` with a different target type reports an E037 error where the body form only warns. Solution: Skip the new initializer check for pointer-typed declarations and validate address initializers with the same check the body uses, so a `REF_TO` target type mismatch reports the E090 warning and nothing else. The two pointer initializer tests keep documenting the gap. Refs: PRG-4728 Co-Authored-By: Claude Fable 5.1 --- .../tests/pointer_validation_tests.rs | 448 +----------------- .../tests/variable_validation_tests.rs | 14 +- src/validation/variable.rs | 19 +- 3 files changed, 27 insertions(+), 454 deletions(-) diff --git a/src/validation/tests/pointer_validation_tests.rs b/src/validation/tests/pointer_validation_tests.rs index a31a5ca13a0..ab50f6e1c5d 100644 --- a/src/validation/tests/pointer_validation_tests.rs +++ b/src/validation/tests/pointer_validation_tests.rs @@ -273,280 +273,16 @@ fn pointer_to_validates_assignment_when_not_dealing_with_memory_address_in_initi .filter(|diagnostic| !matches!(diagnostic.error_code, "E015" | "E065")) .collect::>(); - insta::assert_debug_snapshot!(filtered_diagnostics, @r#" - [ - Diagnostic { - inner: DiagnosticsInner { - message: "Invalid assignment: cannot assign 'STRING' to 'POINTER TO STRING'", - primary_location: SourceLocation { - span: Range(24:55 - 24:60), - file: Some( - "", - ), - }, - secondary_locations: None, - error_code: "E037", - sub_diagnostics: [], - internal_error: None, - }, - }, - Diagnostic { - inner: DiagnosticsInner { - message: "Invalid assignment: cannot assign 'STRING' to 'POINTER TO INT'", - primary_location: SourceLocation { - span: Range(25:55 - 25:60), - file: Some( - "", - ), - }, - secondary_locations: None, - error_code: "E037", - sub_diagnostics: [], - internal_error: None, - }, - }, - Diagnostic { - inner: DiagnosticsInner { - message: "Invalid assignment: cannot assign 'STRING' to 'POINTER TO REAL'", - primary_location: SourceLocation { - span: Range(26:55 - 26:60), - file: Some( - "", - ), - }, - secondary_locations: None, - error_code: "E037", - sub_diagnostics: [], - internal_error: None, - }, - }, - Diagnostic { - inner: DiagnosticsInner { - message: "Invalid assignment: cannot assign 'STRING' to 'POINTER TO TIME'", - primary_location: SourceLocation { - span: Range(27:55 - 27:60), - file: Some( - "", - ), - }, - secondary_locations: None, - error_code: "E037", - sub_diagnostics: [], - internal_error: None, - }, - }, - Diagnostic { - inner: DiagnosticsInner { - message: "Invalid assignment: cannot assign 'STRING' to 'POINTER TO TIME_OF_DAY'", - primary_location: SourceLocation { - span: Range(28:55 - 28:60), - file: Some( - "", - ), - }, - secondary_locations: None, - error_code: "E037", - sub_diagnostics: [], - internal_error: None, - }, - }, - Diagnostic { - inner: DiagnosticsInner { - message: "Invalid assignment: cannot assign 'STRING' to 'POINTER TO DATE'", - primary_location: SourceLocation { - span: Range(29:55 - 29:60), - file: Some( - "", - ), - }, - secondary_locations: None, - error_code: "E037", - sub_diagnostics: [], - internal_error: None, - }, - }, - Diagnostic { - inner: DiagnosticsInner { - message: "Invalid assignment: cannot assign 'STRING' to 'POINTER TO Position1D'", - primary_location: SourceLocation { - span: Range(30:55 - 30:60), - file: Some( - "", - ), - }, - secondary_locations: None, - error_code: "E037", - sub_diagnostics: [], - internal_error: None, - }, - }, - Diagnostic { - inner: DiagnosticsInner { - message: "Invalid assignment: cannot assign 'STRING' to 'POINTER TO Position2D'", - primary_location: SourceLocation { - span: Range(31:55 - 31:60), - file: Some( - "", - ), - }, - secondary_locations: None, - error_code: "E037", - sub_diagnostics: [], - internal_error: None, - }, - }, - Diagnostic { - inner: DiagnosticsInner { - message: "Invalid assignment: cannot assign 'STRING' to 'POINTER TO Position3D'", - primary_location: SourceLocation { - span: Range(32:55 - 32:60), - file: Some( - "", - ), - }, - secondary_locations: None, - error_code: "E037", - sub_diagnostics: [], - internal_error: None, - }, - }, - Diagnostic { - inner: DiagnosticsInner { - message: "Invalid assignment: cannot assign 'STRING' to 'REF_TO STRING'", - primary_location: SourceLocation { - span: Range(34:55 - 34:60), - file: Some( - "", - ), - }, - secondary_locations: None, - error_code: "E037", - sub_diagnostics: [], - internal_error: None, - }, - }, - Diagnostic { - inner: DiagnosticsInner { - message: "Invalid assignment: cannot assign 'STRING' to 'REF_TO INT'", - primary_location: SourceLocation { - span: Range(35:55 - 35:60), - file: Some( - "", - ), - }, - secondary_locations: None, - error_code: "E037", - sub_diagnostics: [], - internal_error: None, - }, - }, - Diagnostic { - inner: DiagnosticsInner { - message: "Invalid assignment: cannot assign 'STRING' to 'REF_TO REAL'", - primary_location: SourceLocation { - span: Range(36:55 - 36:60), - file: Some( - "", - ), - }, - secondary_locations: None, - error_code: "E037", - sub_diagnostics: [], - internal_error: None, - }, - }, - Diagnostic { - inner: DiagnosticsInner { - message: "Invalid assignment: cannot assign 'STRING' to 'REF_TO TIME'", - primary_location: SourceLocation { - span: Range(37:55 - 37:60), - file: Some( - "", - ), - }, - secondary_locations: None, - error_code: "E037", - sub_diagnostics: [], - internal_error: None, - }, - }, - Diagnostic { - inner: DiagnosticsInner { - message: "Invalid assignment: cannot assign 'STRING' to 'REF_TO TIME_OF_DAY'", - primary_location: SourceLocation { - span: Range(38:55 - 38:60), - file: Some( - "", - ), - }, - secondary_locations: None, - error_code: "E037", - sub_diagnostics: [], - internal_error: None, - }, - }, - Diagnostic { - inner: DiagnosticsInner { - message: "Invalid assignment: cannot assign 'STRING' to 'REF_TO DATE'", - primary_location: SourceLocation { - span: Range(39:55 - 39:60), - file: Some( - "", - ), - }, - secondary_locations: None, - error_code: "E037", - sub_diagnostics: [], - internal_error: None, - }, - }, - Diagnostic { - inner: DiagnosticsInner { - message: "Invalid assignment: cannot assign 'STRING' to 'REF_TO Position1D'", - primary_location: SourceLocation { - span: Range(40:55 - 40:60), - file: Some( - "", - ), - }, - secondary_locations: None, - error_code: "E037", - sub_diagnostics: [], - internal_error: None, - }, - }, - Diagnostic { - inner: DiagnosticsInner { - message: "Invalid assignment: cannot assign 'STRING' to 'REF_TO Position2D'", - primary_location: SourceLocation { - span: Range(41:55 - 41:60), - file: Some( - "", - ), - }, - secondary_locations: None, - error_code: "E037", - sub_diagnostics: [], - internal_error: None, - }, - }, - Diagnostic { - inner: DiagnosticsInner { - message: "Invalid assignment: cannot assign 'STRING' to 'REF_TO Position3D'", - primary_location: SourceLocation { - span: Range(42:55 - 42:60), - file: Some( - "", - ), - }, - secondary_locations: None, - error_code: "E037", - sub_diagnostics: [], - internal_error: None, - }, - }, - ] - "#); + // TODO: Validation between variable initialization / assignment in the variable block versus body are handled differently, these + // must be unified at some point. Once done, this assertion MUST fail and be identical to the test + // `pointer_to_validates_assignment_when_not_dealing_with_memory_address_in_body`. Furthermore, we should unify these tests by + // having two source code strings, one for the variable block and one for the implementation, run validation on both of them and + // assert that the diagnostics (with exception of the location) are identical with regards to their assignment validation. + assert_eq!( + filtered_diagnostics, + Vec::new(), + "these are empty for now, but eventually should be the same as the test below" + ); } #[test] @@ -954,160 +690,16 @@ fn pointer_to_validates_assignment_of_non_pointer_sized_integers_in_initializer( .filter(|diagnostic| !matches!(diagnostic.error_code, "E015")) .collect::>(); - insta::assert_debug_snapshot!(filtered_diagnostics, @r#" - [ - Diagnostic { - inner: DiagnosticsInner { - message: "The type SINT 8 is too small to be stored in a Pointer", - primary_location: SourceLocation { - span: Range(8:45 - 8:53), - file: Some( - "", - ), - }, - secondary_locations: None, - error_code: "E065", - sub_diagnostics: [], - internal_error: None, - }, - }, - Diagnostic { - inner: DiagnosticsInner { - message: "The type USINT 8 is too small to be stored in a Pointer", - primary_location: SourceLocation { - span: Range(9:46 - 9:55), - file: Some( - "", - ), - }, - secondary_locations: None, - error_code: "E065", - sub_diagnostics: [], - internal_error: None, - }, - }, - Diagnostic { - inner: DiagnosticsInner { - message: "The type BYTE 8 is too small to be stored in a Pointer", - primary_location: SourceLocation { - span: Range(10:45 - 10:53), - file: Some( - "", - ), - }, - secondary_locations: None, - error_code: "E065", - sub_diagnostics: [], - internal_error: None, - }, - }, - Diagnostic { - inner: DiagnosticsInner { - message: "The type INT 16 is too small to be stored in a Pointer", - primary_location: SourceLocation { - span: Range(17:44 - 17:51), - file: Some( - "", - ), - }, - secondary_locations: None, - error_code: "E065", - sub_diagnostics: [], - internal_error: None, - }, - }, - Diagnostic { - inner: DiagnosticsInner { - message: "The type UINT 16 is too small to be stored in a Pointer", - primary_location: SourceLocation { - span: Range(18:45 - 18:53), - file: Some( - "", - ), - }, - secondary_locations: None, - error_code: "E065", - sub_diagnostics: [], - internal_error: None, - }, - }, - Diagnostic { - inner: DiagnosticsInner { - message: "The type WORD 16 is too small to be stored in a Pointer", - primary_location: SourceLocation { - span: Range(19:45 - 19:53), - file: Some( - "", - ), - }, - secondary_locations: None, - error_code: "E065", - sub_diagnostics: [], - internal_error: None, - }, - }, - Diagnostic { - inner: DiagnosticsInner { - message: "The type DINT 32 is too small to be stored in a Pointer", - primary_location: SourceLocation { - span: Range(27:45 - 27:53), - file: Some( - "", - ), - }, - secondary_locations: None, - error_code: "E065", - sub_diagnostics: [], - internal_error: None, - }, - }, - Diagnostic { - inner: DiagnosticsInner { - message: "The type UDINT 32 is too small to be stored in a Pointer", - primary_location: SourceLocation { - span: Range(28:46 - 28:55), - file: Some( - "", - ), - }, - secondary_locations: None, - error_code: "E065", - sub_diagnostics: [], - internal_error: None, - }, - }, - Diagnostic { - inner: DiagnosticsInner { - message: "The type DWORD 32 is too small to be stored in a Pointer", - primary_location: SourceLocation { - span: Range(29:46 - 29:55), - file: Some( - "", - ), - }, - secondary_locations: None, - error_code: "E065", - sub_diagnostics: [], - internal_error: None, - }, - }, - Diagnostic { - inner: DiagnosticsInner { - message: "The type REAL 32 is too small to be stored in a Pointer", - primary_location: SourceLocation { - span: Range(30:45 - 30:53), - file: Some( - "", - ), - }, - secondary_locations: None, - error_code: "E065", - sub_diagnostics: [], - internal_error: None, - }, - }, - ] - "#); + // TODO: Validation between variable initialization / assignment in the variable block versus body are handled differently, these + // must be unified at some point. Once done, this assertion MUST fail and be identical to the test + // `pointer_to_validates_assignment_of_non_pointer_sized_integers`. Furthermore, we should unify these tests by + // having two source code strings, one for the variable block and one for the implementation, run validation on both of them and + // assert that the diagnostics (with exception of the location) are identical with regards to their assignment validation. + assert_eq!( + filtered_diagnostics, + Vec::new(), + "these are empty for now, but eventually should be the same as the test below" + ); } #[test] diff --git a/src/validation/tests/variable_validation_tests.rs b/src/validation/tests/variable_validation_tests.rs index 56f35800fd3..5a4df41df9e 100644 --- a/src/validation/tests/variable_validation_tests.rs +++ b/src/validation/tests/variable_validation_tests.rs @@ -906,12 +906,6 @@ fn unresolved_references_to_const_builtins_in_initializer_are_reported() { ); assert_snapshot!(diagnostics, @" - warning[E090]: Pointers REF_TO BOOL and VOID have different types - ┌─ :4:38 - │ - 4 │ bar : REF_TO BOOL := REF(gb); // unresolved reference to gb - │ ^^^^^^^ Pointers REF_TO BOOL and VOID have different types - error[E048]: Could not resolve reference to gb ┌─ :4:42 │ @@ -1010,7 +1004,7 @@ fn trying_to_initialize_a_pointer_of_unknown_type_is_reported() { } #[test] -fn trying_to_initialize_a_pointer_with_builtin_ref_with_type_mismatch_leads_to_error() { +fn trying_to_initialize_a_pointer_with_builtin_ref_with_type_mismatch_leads_to_warning() { let diagnostics = parse_and_validate_buffered( r#" VAR_GLOBAL @@ -1030,12 +1024,6 @@ fn trying_to_initialize_a_pointer_with_builtin_ref_with_type_mismatch_leads_to_e │ 7 │ bar : REF_TO STRING := REF(a); │ ^^^^^^ Pointers REF_TO STRING and DINT have different types - - error[E037]: Invalid assignment: cannot assign 'DINT' to 'REF_TO STRING' - ┌─ :7:36 - │ - 7 │ bar : REF_TO STRING := REF(a); - │ ^^^^^^ Invalid assignment: cannot assign 'DINT' to 'REF_TO STRING' "); } diff --git a/src/validation/variable.rs b/src/validation/variable.rs index 76faf51509e..20e8ed8c22f 100644 --- a/src/validation/variable.rs +++ b/src/validation/variable.rs @@ -413,20 +413,19 @@ fn validate_variable( .as_ref() .is_some_and(|initializer| initializer.is_struct_literal_initializer()); - // `REFERENCE TO` and alias declarations bind a reference instead of assigning a value, their - // initializer is checked by `validate_reference_to_declaration` - let is_reference_declaration = context + // pointer declarations are checked by `validate_reference_to_declaration` and by the + // address branch below + let is_pointer_declaration = context .index .find_effective_type_by_name(v_entry.get_type_name()) - .map(|ty| ty.get_type_information()) - .is_some_and(|ty| ty.is_reference_to() || ty.is_alias()); + .is_some_and(|ty| ty.get_type_information().is_pointer()); if let Some(initializer) = &variable.initializer { // Assume `foo : ARRAY[1..5] OF DINT := [...]`, here the first function call validates the // assignment as a whole whereas the last function call (`visit_statement`) validates the // initializer in case it has further sub-assignments. validate_array_assignment(validator, context, variable); - if !is_reference_declaration { + if !is_pointer_declaration { validate_assignment(validator, initializer, None, &initializer.location, context); } visit_statement(validator, initializer, context); @@ -470,13 +469,7 @@ fn validate_variable( validator, context, v_entry, node, ); - validate_assignment_mismatch( - context, - validator, - context.index.get_effective_type_or_void_by_name(v_entry.get_type_name()), - rhs_ty, - &node.get_location(), - ); + validate_assignment(validator, node, None, &node.get_location(), context); } }; } From 5d3b04d21fd419e420dc68bf7c1bb48997659d3a Mon Sep 17 00:00:00 2001 From: Ghaith Hachem Date: Thu, 17 Sep 2026 10:27:50 +0000 Subject: [PATCH 4/6] fix(validation): reject non-integer values as pointer initializers Problem: A string or real value stored into a pointer declaration, such as `p : POINTER TO INT := 'foo'`, is accepted, while the same assignment in a body is an error. The value is never a valid address. Solution: Pointer declarations only skip the assignment check for integer initializers; every other non-pointer value is reported like in a body. Address initializers keep going through the address branch. Refs: PRG-4728 Co-Authored-By: Claude Fable 5.1 --- .../tests/pointer_validation_tests.rs | 316 ++++++++++++++++-- src/validation/variable.rs | 10 +- 2 files changed, 303 insertions(+), 23 deletions(-) diff --git a/src/validation/tests/pointer_validation_tests.rs b/src/validation/tests/pointer_validation_tests.rs index ab50f6e1c5d..fc640b5d23b 100644 --- a/src/validation/tests/pointer_validation_tests.rs +++ b/src/validation/tests/pointer_validation_tests.rs @@ -273,16 +273,280 @@ fn pointer_to_validates_assignment_when_not_dealing_with_memory_address_in_initi .filter(|diagnostic| !matches!(diagnostic.error_code, "E015" | "E065")) .collect::>(); - // TODO: Validation between variable initialization / assignment in the variable block versus body are handled differently, these - // must be unified at some point. Once done, this assertion MUST fail and be identical to the test - // `pointer_to_validates_assignment_when_not_dealing_with_memory_address_in_body`. Furthermore, we should unify these tests by - // having two source code strings, one for the variable block and one for the implementation, run validation on both of them and - // assert that the diagnostics (with exception of the location) are identical with regards to their assignment validation. - assert_eq!( - filtered_diagnostics, - Vec::new(), - "these are empty for now, but eventually should be the same as the test below" - ); + insta::assert_debug_snapshot!(filtered_diagnostics, @r#" + [ + Diagnostic { + inner: DiagnosticsInner { + message: "Invalid assignment: cannot assign 'STRING' to 'POINTER TO STRING'", + primary_location: SourceLocation { + span: Range(24:55 - 24:60), + file: Some( + "", + ), + }, + secondary_locations: None, + error_code: "E037", + sub_diagnostics: [], + internal_error: None, + }, + }, + Diagnostic { + inner: DiagnosticsInner { + message: "Invalid assignment: cannot assign 'STRING' to 'POINTER TO INT'", + primary_location: SourceLocation { + span: Range(25:55 - 25:60), + file: Some( + "", + ), + }, + secondary_locations: None, + error_code: "E037", + sub_diagnostics: [], + internal_error: None, + }, + }, + Diagnostic { + inner: DiagnosticsInner { + message: "Invalid assignment: cannot assign 'STRING' to 'POINTER TO REAL'", + primary_location: SourceLocation { + span: Range(26:55 - 26:60), + file: Some( + "", + ), + }, + secondary_locations: None, + error_code: "E037", + sub_diagnostics: [], + internal_error: None, + }, + }, + Diagnostic { + inner: DiagnosticsInner { + message: "Invalid assignment: cannot assign 'STRING' to 'POINTER TO TIME'", + primary_location: SourceLocation { + span: Range(27:55 - 27:60), + file: Some( + "", + ), + }, + secondary_locations: None, + error_code: "E037", + sub_diagnostics: [], + internal_error: None, + }, + }, + Diagnostic { + inner: DiagnosticsInner { + message: "Invalid assignment: cannot assign 'STRING' to 'POINTER TO TIME_OF_DAY'", + primary_location: SourceLocation { + span: Range(28:55 - 28:60), + file: Some( + "", + ), + }, + secondary_locations: None, + error_code: "E037", + sub_diagnostics: [], + internal_error: None, + }, + }, + Diagnostic { + inner: DiagnosticsInner { + message: "Invalid assignment: cannot assign 'STRING' to 'POINTER TO DATE'", + primary_location: SourceLocation { + span: Range(29:55 - 29:60), + file: Some( + "", + ), + }, + secondary_locations: None, + error_code: "E037", + sub_diagnostics: [], + internal_error: None, + }, + }, + Diagnostic { + inner: DiagnosticsInner { + message: "Invalid assignment: cannot assign 'STRING' to 'POINTER TO Position1D'", + primary_location: SourceLocation { + span: Range(30:55 - 30:60), + file: Some( + "", + ), + }, + secondary_locations: None, + error_code: "E037", + sub_diagnostics: [], + internal_error: None, + }, + }, + Diagnostic { + inner: DiagnosticsInner { + message: "Invalid assignment: cannot assign 'STRING' to 'POINTER TO Position2D'", + primary_location: SourceLocation { + span: Range(31:55 - 31:60), + file: Some( + "", + ), + }, + secondary_locations: None, + error_code: "E037", + sub_diagnostics: [], + internal_error: None, + }, + }, + Diagnostic { + inner: DiagnosticsInner { + message: "Invalid assignment: cannot assign 'STRING' to 'POINTER TO Position3D'", + primary_location: SourceLocation { + span: Range(32:55 - 32:60), + file: Some( + "", + ), + }, + secondary_locations: None, + error_code: "E037", + sub_diagnostics: [], + internal_error: None, + }, + }, + Diagnostic { + inner: DiagnosticsInner { + message: "Invalid assignment: cannot assign 'STRING' to 'REF_TO STRING'", + primary_location: SourceLocation { + span: Range(34:55 - 34:60), + file: Some( + "", + ), + }, + secondary_locations: None, + error_code: "E037", + sub_diagnostics: [], + internal_error: None, + }, + }, + Diagnostic { + inner: DiagnosticsInner { + message: "Invalid assignment: cannot assign 'STRING' to 'REF_TO INT'", + primary_location: SourceLocation { + span: Range(35:55 - 35:60), + file: Some( + "", + ), + }, + secondary_locations: None, + error_code: "E037", + sub_diagnostics: [], + internal_error: None, + }, + }, + Diagnostic { + inner: DiagnosticsInner { + message: "Invalid assignment: cannot assign 'STRING' to 'REF_TO REAL'", + primary_location: SourceLocation { + span: Range(36:55 - 36:60), + file: Some( + "", + ), + }, + secondary_locations: None, + error_code: "E037", + sub_diagnostics: [], + internal_error: None, + }, + }, + Diagnostic { + inner: DiagnosticsInner { + message: "Invalid assignment: cannot assign 'STRING' to 'REF_TO TIME'", + primary_location: SourceLocation { + span: Range(37:55 - 37:60), + file: Some( + "", + ), + }, + secondary_locations: None, + error_code: "E037", + sub_diagnostics: [], + internal_error: None, + }, + }, + Diagnostic { + inner: DiagnosticsInner { + message: "Invalid assignment: cannot assign 'STRING' to 'REF_TO TIME_OF_DAY'", + primary_location: SourceLocation { + span: Range(38:55 - 38:60), + file: Some( + "", + ), + }, + secondary_locations: None, + error_code: "E037", + sub_diagnostics: [], + internal_error: None, + }, + }, + Diagnostic { + inner: DiagnosticsInner { + message: "Invalid assignment: cannot assign 'STRING' to 'REF_TO DATE'", + primary_location: SourceLocation { + span: Range(39:55 - 39:60), + file: Some( + "", + ), + }, + secondary_locations: None, + error_code: "E037", + sub_diagnostics: [], + internal_error: None, + }, + }, + Diagnostic { + inner: DiagnosticsInner { + message: "Invalid assignment: cannot assign 'STRING' to 'REF_TO Position1D'", + primary_location: SourceLocation { + span: Range(40:55 - 40:60), + file: Some( + "", + ), + }, + secondary_locations: None, + error_code: "E037", + sub_diagnostics: [], + internal_error: None, + }, + }, + Diagnostic { + inner: DiagnosticsInner { + message: "Invalid assignment: cannot assign 'STRING' to 'REF_TO Position2D'", + primary_location: SourceLocation { + span: Range(41:55 - 41:60), + file: Some( + "", + ), + }, + secondary_locations: None, + error_code: "E037", + sub_diagnostics: [], + internal_error: None, + }, + }, + Diagnostic { + inner: DiagnosticsInner { + message: "Invalid assignment: cannot assign 'STRING' to 'REF_TO Position3D'", + primary_location: SourceLocation { + span: Range(42:55 - 42:60), + file: Some( + "", + ), + }, + secondary_locations: None, + error_code: "E037", + sub_diagnostics: [], + internal_error: None, + }, + }, + ] + "#); } #[test] @@ -690,16 +954,28 @@ fn pointer_to_validates_assignment_of_non_pointer_sized_integers_in_initializer( .filter(|diagnostic| !matches!(diagnostic.error_code, "E015")) .collect::>(); - // TODO: Validation between variable initialization / assignment in the variable block versus body are handled differently, these - // must be unified at some point. Once done, this assertion MUST fail and be identical to the test - // `pointer_to_validates_assignment_of_non_pointer_sized_integers`. Furthermore, we should unify these tests by - // having two source code strings, one for the variable block and one for the implementation, run validation on both of them and - // assert that the diagnostics (with exception of the location) are identical with regards to their assignment validation. - assert_eq!( - filtered_diagnostics, - Vec::new(), - "these are empty for now, but eventually should be the same as the test below" - ); + // Integer values stored into a pointer are not reported in a declaration yet, only the REAL value + // is. Once they are, this must be identical to + // `pointer_to_validates_assignment_of_non_pointer_sized_integers` (which validates the body). + insta::assert_debug_snapshot!(filtered_diagnostics, @r#" + [ + Diagnostic { + inner: DiagnosticsInner { + message: "The type REAL 32 is too small to be stored in a Pointer", + primary_location: SourceLocation { + span: Range(30:45 - 30:53), + file: Some( + "", + ), + }, + secondary_locations: None, + error_code: "E065", + sub_diagnostics: [], + internal_error: None, + }, + }, + ] + "#); } #[test] diff --git a/src/validation/variable.rs b/src/validation/variable.rs index 20e8ed8c22f..a893976d88e 100644 --- a/src/validation/variable.rs +++ b/src/validation/variable.rs @@ -413,8 +413,6 @@ fn validate_variable( .as_ref() .is_some_and(|initializer| initializer.is_struct_literal_initializer()); - // pointer declarations are checked by `validate_reference_to_declaration` and by the - // address branch below let is_pointer_declaration = context .index .find_effective_type_by_name(v_entry.get_type_name()) @@ -425,7 +423,13 @@ fn validate_variable( // assignment as a whole whereas the last function call (`visit_statement`) validates the // initializer in case it has further sub-assignments. validate_array_assignment(validator, context, variable); - if !is_pointer_declaration { + // pointer declarations accept integer initializers; address initializers are checked by the + // address branch below and `REFERENCE TO` bindings by `validate_reference_to_declaration` + let initializer_type = + context.annotations.get_type_or_void(initializer, context.index).get_type_information(); + let is_accepted_pointer_initializer = is_pointer_declaration + && (initializer_type.is_pointer() || initializer_type.is_int() || initializer_type.is_void()); + if !is_accepted_pointer_initializer { validate_assignment(validator, initializer, None, &initializer.location, context); } visit_statement(validator, initializer, context); From ea9446839397e553612b70f4de705b10e20b89b8 Mon Sep 17 00:00:00 2001 From: Ghaith Hachem Date: Thu, 17 Sep 2026 11:09:12 +0000 Subject: [PATCH 5/6] fix(validation): type-check BOOL values and array literal elements Problem: `a : DINT := TRUE` is accepted although BOOL is not a numeric value, `b : BOOL := 5` is accepted although only 0 and 1 are BOOL literals, and the elements of an array literal are never checked against the element type, so `ARRAY[0..1] OF DINT := ['a', 'b']` passes `--check` and fails in code generation. Solution: A BOOL value assigned to another numeric type is an invalid assignment, as is an integer literal other than 0 or 1, or a real literal, assigned to a BOOL. Every element of an array literal is validated like an assignment to the element type, including nested literals; spliced array references and flat initializers of nested arrays are left to the existing length checks. The standard library's BOOL_TO_ functions use the explicit IF form already used for the bit types, and the CFC storage test on a numeric sink now asserts the rejection. Refs: PRG-4728 Co-Authored-By: Claude Fable 5.1 --- libs/stdlib/iec61131-st/bit_num_conversion.st | 48 ++++++++++--- .../generators/expression_generator.rs | 2 +- .../initialization_test/pou_initializers.rs | 24 +++---- src/validation/array.rs | 50 ++++++++++++- src/validation/statement.rs | 19 +++++ .../tests/assignment_validation_tests.rs | 71 +++++++++++++++++++ .../tests/statement_validation_tests.rs | 4 +- .../tests/variable_validation_tests.rs | 27 ++++++- .../cfc/validation/storage_numeric/README.md | 5 ++ .../cfc/validation/storage_numeric/main.st | 8 +++ .../storage_numeric/plc.json | 0 .../cfc/validation/storage_numeric/run.test | 1 + .../storage_numeric/storage_numeric.cfc | 0 .../cfc/variables/storage_numeric/README.md | 5 -- .../lit/cfc/variables/storage_numeric/main.st | 17 ----- .../cfc/variables/storage_numeric/run.test | 1 - .../builtin-positional-arguments/main.st | 33 ++++----- .../single/init/initializer_type_mismatch.st | 15 ++++ .../init/numeric_initializer_literal_casts.st | 41 +++++------ 19 files changed, 278 insertions(+), 93 deletions(-) create mode 100644 tests/lit/cfc/validation/storage_numeric/README.md create mode 100644 tests/lit/cfc/validation/storage_numeric/main.st rename tests/lit/cfc/{variables => validation}/storage_numeric/plc.json (100%) create mode 100644 tests/lit/cfc/validation/storage_numeric/run.test rename tests/lit/cfc/{variables => validation}/storage_numeric/storage_numeric.cfc (100%) delete mode 100644 tests/lit/cfc/variables/storage_numeric/README.md delete mode 100644 tests/lit/cfc/variables/storage_numeric/main.st delete mode 100644 tests/lit/cfc/variables/storage_numeric/run.test diff --git a/libs/stdlib/iec61131-st/bit_num_conversion.st b/libs/stdlib/iec61131-st/bit_num_conversion.st index ff0f83ceeec..d57302bdffa 100644 --- a/libs/stdlib/iec61131-st/bit_num_conversion.st +++ b/libs/stdlib/iec61131-st/bit_num_conversion.st @@ -415,7 +415,11 @@ FUNCTION BOOL_TO_LINT : LINT VAR_INPUT in : BOOL; END_VAR - BOOL_TO_LINT := in; + IF in THEN + BOOL_TO_LINT := 1; + ELSE + BOOL_TO_LINT := 0; + END_IF; END_FUNCTION (******************** @@ -427,7 +431,11 @@ FUNCTION BOOL_TO_DINT : DINT VAR_INPUT in : BOOL; END_VAR - BOOL_TO_DINT := in; + IF in THEN + BOOL_TO_DINT := 1; + ELSE + BOOL_TO_DINT := 0; + END_IF; END_FUNCTION (******************** @@ -439,7 +447,11 @@ FUNCTION BOOL_TO_INT : INT VAR_INPUT in : BOOL; END_VAR - BOOL_TO_INT := in; + IF in THEN + BOOL_TO_INT := 1; + ELSE + BOOL_TO_INT := 0; + END_IF; END_FUNCTION (******************** @@ -451,7 +463,11 @@ FUNCTION BOOL_TO_SINT : SINT VAR_INPUT in : BOOL; END_VAR - BOOL_TO_SINT := in; + IF in THEN + BOOL_TO_SINT := 1; + ELSE + BOOL_TO_SINT := 0; + END_IF; END_FUNCTION (******************** @@ -463,7 +479,11 @@ FUNCTION BOOL_TO_ULINT : ULINT VAR_INPUT in : BOOL; END_VAR - BOOL_TO_ULINT := in; + IF in THEN + BOOL_TO_ULINT := 1; + ELSE + BOOL_TO_ULINT := 0; + END_IF; END_FUNCTION (******************** @@ -475,7 +495,11 @@ FUNCTION BOOL_TO_UDINT : UDINT VAR_INPUT in : BOOL; END_VAR - BOOL_TO_UDINT := in; + IF in THEN + BOOL_TO_UDINT := 1; + ELSE + BOOL_TO_UDINT := 0; + END_IF; END_FUNCTION (******************** @@ -487,7 +511,11 @@ FUNCTION BOOL_TO_UINT : UINT VAR_INPUT in : BOOL; END_VAR - BOOL_TO_UINT := in; + IF in THEN + BOOL_TO_UINT := 1; + ELSE + BOOL_TO_UINT := 0; + END_IF; END_FUNCTION (******************** @@ -499,7 +527,11 @@ FUNCTION BOOL_TO_USINT : USINT VAR_INPUT in : BOOL; END_VAR - BOOL_TO_USINT := in; + IF in THEN + BOOL_TO_USINT := 1; + ELSE + BOOL_TO_USINT := 0; + END_IF; END_FUNCTION (******************** diff --git a/src/codegen/generators/expression_generator.rs b/src/codegen/generators/expression_generator.rs index 6d31f2714d1..6876541c71b 100644 --- a/src/codegen/generators/expression_generator.rs +++ b/src/codegen/generators/expression_generator.rs @@ -2908,7 +2908,7 @@ impl<'ink, 'b> ExpressionCodeGenerator<'ink, 'b> { for e in elements { //generate with correct type hint using context-free generator let value = ctx_free_gen.generate_literal(e)?.get_basic_value_enum(); - // numeric literals take the type of the array element, e.g. `TRUE` in an ARRAY OF REAL + // numeric literals take the type of the array element, e.g. `1.5` in an ARRAY OF INT let value = match value { BasicValueEnum::IntValue(_) | BasicValueEnum::FloatValue(_) => cast_if_needed!( ctx_free_gen, diff --git a/src/codegen/tests/initialization_test/pou_initializers.rs b/src/codegen/tests/initialization_test/pou_initializers.rs index b0f02dacf9c..50288fc1cee 100644 --- a/src/codegen/tests/initialization_test/pou_initializers.rs +++ b/src/codegen/tests/initialization_test/pou_initializers.rs @@ -496,12 +496,11 @@ fn numeric_array_literal_elements_are_cast_to_the_element_type() { r#" FUNCTION main : DINT VAR - dints : ARRAY[0..1] OF DINT := [TRUE, FALSE]; - reals : ARRAY[0..1] OF REAL := [TRUE, FALSE]; ints : ARRAY[0..1] OF INT := [1.5, 2]; - later : ARRAY[0..1] OF LREAL; + dints : ARRAY[0..1] OF DINT := [2.5, 3.75]; + later : ARRAY[0..1] OF INT; END_VAR - later := [TRUE, FALSE]; + later := [4.5, 5]; END_FUNCTION "#, ); @@ -512,24 +511,21 @@ fn numeric_array_literal_elements_are_cast_to_the_element_type() { target datalayout = "[filtered]" target triple = "[filtered]" - @__main.dints__init = unnamed_addr constant [2 x i32] [i32 1, i32 0] - @__main.reals__init = unnamed_addr constant [2 x float] [float 1.000000e+00, float 0.000000e+00] @__main.ints__init = unnamed_addr constant [2 x i16] [i16 1, i16 2] - @.const_init = private unnamed_addr constant [2 x double] [double 1.000000e+00, double 0.000000e+00] + @__main.dints__init = unnamed_addr constant [2 x i32] [i32 2, i32 3] + @.const_init = private unnamed_addr constant [2 x i16] [i16 4, i16 5] define i32 @main() { entry: %main = alloca i32, align [filtered] - %dints = alloca [2 x i32], align [filtered] - %reals = alloca [2 x float], align [filtered] %ints = alloca [2 x i16], align [filtered] - %later = alloca [2 x double], align [filtered] - call void @llvm.memcpy.p0.p0.i64(ptr align [filtered] %dints, ptr align [filtered] @__main.dints__init, i64 ptrtoint (ptr getelementptr ([2 x i32], ptr null, i32 1) to i64), i1 false) - call void @llvm.memcpy.p0.p0.i64(ptr align [filtered] %reals, ptr align [filtered] @__main.reals__init, i64 ptrtoint (ptr getelementptr ([2 x float], ptr null, i32 1) to i64), i1 false) + %dints = alloca [2 x i32], align [filtered] + %later = alloca [2 x i16], align [filtered] call void @llvm.memcpy.p0.p0.i64(ptr align [filtered] %ints, ptr align [filtered] @__main.ints__init, i64 ptrtoint (ptr getelementptr ([2 x i16], ptr null, i32 1) to i64), i1 false) - call void @llvm.memset.p0.i64(ptr align [filtered] %later, i8 0, i64 ptrtoint (ptr getelementptr ([2 x double], ptr null, i32 1) to i64), i1 false) + call void @llvm.memcpy.p0.p0.i64(ptr align [filtered] %dints, ptr align [filtered] @__main.dints__init, i64 ptrtoint (ptr getelementptr ([2 x i32], ptr null, i32 1) to i64), i1 false) + call void @llvm.memset.p0.i64(ptr align [filtered] %later, i8 0, i64 ptrtoint (ptr getelementptr ([2 x i16], ptr null, i32 1) to i64), i1 false) store i32 0, ptr %main, align [filtered] - call void @llvm.memcpy.p0.p0.i64(ptr align [filtered] %later, ptr align [filtered] @.const_init, i64 ptrtoint (ptr getelementptr ([2 x double], ptr null, i32 1) to i64), i1 false) + call void @llvm.memcpy.p0.p0.i64(ptr align [filtered] %later, ptr align [filtered] @.const_init, i64 ptrtoint (ptr getelementptr ([2 x i16], ptr null, i32 1) to i64), i1 false) %main_ret = load i32, ptr %main, align [filtered] ret i32 %main_ret } diff --git a/src/validation/array.rs b/src/validation/array.rs index c2b142fba5e..1f36ae1563f 100644 --- a/src/validation/array.rs +++ b/src/validation/array.rs @@ -18,7 +18,7 @@ use plc_index::GlobalContext; use crate::{resolver::AnnotationMap, typesystem::DataTypeInformation}; -use super::{ValidationContext, Validator, Validators}; +use super::{statement::validate_assignment, ValidationContext, Validator, Validators}; /// Indicates whether an array was assigned in a VAR block, a POU body, or a TYPE declaration #[derive(Debug, Clone, Copy)] @@ -92,6 +92,8 @@ fn validate_array( return; // Return here, because array size validation is error-prone with incorrect assignments } + validate_array_elements(validator, context, lhs_type, stmt_rhs); + let len_lhs = lhs_type.get_array_length(context.index).unwrap_or(0); let Some(len_rhs) = statement_to_array_length(context, stmt_rhs) else { return }; @@ -122,6 +124,52 @@ fn validate_array( } } +/// Validates every element of an array literal against the element type, e.g. `[TRUE]` is not an +/// `ARRAY OF DINT`. Struct elements are validated through their member assignments. +fn validate_array_elements( + validator: &mut Validator, + context: &ValidationContext, + array_type: &DataTypeInformation, + literal: &AstNode, +) { + let AstStatement::Literal(AstLiteral::Array(array)) = literal.get_stmt() else { return }; + let Some(elements) = array.elements() else { return }; + let Some(inner_type) = + array_type.get_inner_array_type_name().and_then(|name| context.index.find_effective_type_info(name)) + else { + return; + }; + if inner_type.is_struct() { + return; + } + + for element in array_literal_elements(elements) { + if inner_type.is_array() && element.is_literal_array() { + validate_array_elements(validator, context, inner_type, element); + continue; + } + // spliced array references and flat initializers of nested arrays carry array types + let element_is_array = context.annotations.get_type_or_void(element, context.index).is_array(); + let hint_is_array = + context.annotations.get_type_hint(element, context.index).is_some_and(|hint| hint.is_array()); + if !element_is_array && !hint_is_array { + validate_assignment(validator, element, None, &element.location, context); + } + } +} + +/// Returns the element expressions of an array literal, each multiplied element once +fn array_literal_elements(node: &AstNode) -> Vec<&AstNode> { + match node.get_stmt() { + AstStatement::ExpressionList(expressions) => { + expressions.iter().flat_map(array_literal_elements).collect() + } + AstStatement::MultipliedStatement(data) => array_literal_elements(&data.element), + AstStatement::ParenExpression(expression) => array_literal_elements(expression), + _ => vec![node], + } +} + /// Checks if an expression is a valid element in an array of structs. /// Valid elements are: /// - Parenthesized expressions (struct initializers like `(a := 1, b := 2)`) diff --git a/src/validation/statement.rs b/src/validation/statement.rs index 0d633ec988a..6f372cd531f 100644 --- a/src/validation/statement.rs +++ b/src/validation/statement.rs @@ -2070,6 +2070,8 @@ fn is_valid_assignment( // because those would fail return true; } else if is_invalid_char_assignment(left_type.get_type_information(), right_type.get_type_information()) + | is_invalid_bool_assignment(left_type.get_type_information(), right_type.get_type_information()) + | is_invalid_bool_literal_assignment(left_type.get_type_information(), right) | is_invalid_pointer_assignment( left_type.get_type_information(), right_type.get_type_information(), @@ -2174,6 +2176,23 @@ fn is_invalid_char_assignment(left_type: &DataTypeInformation, right_type: &Data false } +/// a BOOL value is not implicitly converted to another numeric type +fn is_invalid_bool_assignment(left_type: &DataTypeInformation, right_type: &DataTypeInformation) -> bool { + right_type.is_bool() && left_type.is_numerical() && !left_type.is_bool() +} + +/// only the literals 0 and 1 are BOOL values +fn is_invalid_bool_literal_assignment(left_type: &DataTypeInformation, right: &AstNode) -> bool { + if !left_type.is_bool() { + return false; + } + match right.get_stmt_peeled() { + AstStatement::Literal(AstLiteral::Integer(value)) => *value != 0 && *value != 1, + AstStatement::Literal(AstLiteral::Real(_)) => true, + _ => false, + } +} + /// aggregate types can only be assigned to aggregate types /// special case char := string_with_length_1, handled by `is_valid_string_to_char_assignment()` fn is_aggregate_to_none_aggregate_assignment(left_type: &DataType, right_type: &DataType) -> bool { diff --git a/src/validation/tests/assignment_validation_tests.rs b/src/validation/tests/assignment_validation_tests.rs index 9644dd0cf5a..28cab390ef4 100644 --- a/src/validation/tests/assignment_validation_tests.rs +++ b/src/validation/tests/assignment_validation_tests.rs @@ -345,6 +345,77 @@ fn string_assignment_validation() { assert_snapshot!(&diagnostics); } +#[test] +fn bool_assignment_validation() { + let diagnostics = parse_and_validate_buffered( + r#" + FUNCTION main : DINT + VAR + v_bool : BOOL; + v_dint : DINT; + v_real : REAL; + v_dword : DWORD; + END_VAR + v_dint := TRUE; // INVALID + v_real := v_bool; // INVALID + v_dword := FALSE; // INVALID + v_bool := 5; // INVALID + v_bool := 2.5; // INVALID + v_dword.1 := 5; // INVALID + v_bool := 1; // valid + v_bool := 0; // valid + v_bool := v_dint; // valid, reported as a downcast + v_dword.1 := 1; // valid + v_bool := v_dint > 1; // valid + END_FUNCTION + "#, + ); + + assert_snapshot!(&diagnostics, @" + error[E037]: Invalid assignment: cannot assign 'BOOL' to 'DINT' + ┌─ :9:5 + │ + 9 │ v_dint := TRUE; // INVALID + │ ^^^^^^^^^^^^^^ Invalid assignment: cannot assign 'BOOL' to 'DINT' + + error[E037]: Invalid assignment: cannot assign 'BOOL' to 'REAL' + ┌─ :10:5 + │ + 10 │ v_real := v_bool; // INVALID + │ ^^^^^^^^^^^^^^^^ Invalid assignment: cannot assign 'BOOL' to 'REAL' + + error[E037]: Invalid assignment: cannot assign 'BOOL' to 'DWORD' + ┌─ :11:5 + │ + 11 │ v_dword := FALSE; // INVALID + │ ^^^^^^^^^^^^^^^^ Invalid assignment: cannot assign 'BOOL' to 'DWORD' + + error[E037]: Invalid assignment: cannot assign 'DINT' to 'BOOL' + ┌─ :12:5 + │ + 12 │ v_bool := 5; // INVALID + │ ^^^^^^^^^^^ Invalid assignment: cannot assign 'DINT' to 'BOOL' + + error[E037]: Invalid assignment: cannot assign 'REAL' to 'BOOL' + ┌─ :13:5 + │ + 13 │ v_bool := 2.5; // INVALID + │ ^^^^^^^^^^^^^ Invalid assignment: cannot assign 'REAL' to 'BOOL' + + error[E037]: Invalid assignment: cannot assign 'DINT' to 'BOOL' + ┌─ :14:5 + │ + 14 │ v_dword.1 := 5; // INVALID + │ ^^^^^^^^^^^^^^ Invalid assignment: cannot assign 'DINT' to 'BOOL' + + warning[E067]: Implicit downcast from 'DINT' to 'BOOL'. + ┌─ :17:15 + │ + 17 │ v_bool := v_dint; // valid, reported as a downcast + │ ^^^^^^ Implicit downcast from 'DINT' to 'BOOL'. + "); +} + #[test] fn char_assignment_validation() { let diagnostics = parse_and_validate_buffered( diff --git a/src/validation/tests/statement_validation_tests.rs b/src/validation/tests/statement_validation_tests.rs index c59d493ef8c..e5944ff91a8 100644 --- a/src/validation/tests/statement_validation_tests.rs +++ b/src/validation/tests/statement_validation_tests.rs @@ -2339,9 +2339,9 @@ fn allowed_assignable_types() { v := 0; x[0] := 1; y^ := 2; - y^.1 := 3; + y^.1 := 1; z^[0] := 4; - z^[1].1 := 5; + z^[1].1 := 1; END_PROGRAM "#, ); diff --git a/src/validation/tests/variable_validation_tests.rs b/src/validation/tests/variable_validation_tests.rs index 5a4df41df9e..e49d27e3dac 100644 --- a/src/validation/tests/variable_validation_tests.rs +++ b/src/validation/tests/variable_validation_tests.rs @@ -467,11 +467,11 @@ fn assignment_suggestion_for_equal_operation_with_no_effect() { " PROGRAM main VAR - value : DINT; + value : BOOL; condition : BOOL; // These Should work - arr_dint : ARRAY[0..5] OF DINT := [1 = 1, 2, 3, 4, 5 = 5]; + arr_eq : ARRAY[0..4] OF BOOL := [1 = 1, 2 = 2, 3 = 3, 4 = 4, 5 = 5]; arr_bool : ARRAY[1..5] OF BOOL := [1 = 1, 2 = 2, 3 = 3, 4 = 4, 5 = 10]; END_VAR @@ -1608,7 +1608,7 @@ fn output_variables_must_be_assignable_within_the_scope_of_inheritance() { END_VAR out1 := 1; - out2 := 2; + out2 := TRUE; END_FUNCTION_BLOCK PROGRAM mainProg @@ -2048,7 +2048,10 @@ fn initializers_with_incompatible_types_are_reported() { strFromReal : STRING := 3.5; strFromBool : STRING := TRUE; int : INT := 'abc'; + dintFromBool : DINT := TRUE; + boolFromInt : BOOL := 5; validStr : STRING := 'abc'; + validBool : BOOL := 1; validInt : DINT := 3; validReal : LREAL := 3; validStruct : MyStruct := (member := 'abc'); @@ -2060,6 +2063,12 @@ fn initializers_with_incompatible_types_are_reported() { ); assert_snapshot!(diagnostics, @" + warning[E039]: This will overflow for type BOOL + ┌─ :23:35 + │ + 23 │ boolFromInt : BOOL := 5; + │ ^ This will overflow for type BOOL + error[E037]: Invalid assignment: cannot assign 'DINT' to 'WSTRING' ┌─ :12:32 │ @@ -2090,6 +2099,18 @@ fn initializers_with_incompatible_types_are_reported() { 21 │ int : INT := 'abc'; │ ^^^^^ Invalid assignment: cannot assign 'STRING' to 'INT' + error[E037]: Invalid assignment: cannot assign 'BOOL' to 'DINT' + ┌─ :22:36 + │ + 22 │ dintFromBool : DINT := TRUE; + │ ^^^^ Invalid assignment: cannot assign 'BOOL' to 'DINT' + + error[E037]: Invalid assignment: cannot assign 'DINT' to 'BOOL' + ┌─ :23:35 + │ + 23 │ boolFromInt : BOOL := 5; + │ ^ Invalid assignment: cannot assign 'DINT' to 'BOOL' + error[E037]: Invalid assignment: cannot assign 'DINT' to 'STRING' ┌─ :3:32 │ diff --git a/tests/lit/cfc/validation/storage_numeric/README.md b/tests/lit/cfc/validation/storage_numeric/README.md new file mode 100644 index 00000000000..e75daaf82c3 --- /dev/null +++ b/tests/lit/cfc/validation/storage_numeric/README.md @@ -0,0 +1,5 @@ +Pins that storage modes (Set/Reset) on a numeric sink are rejected: the +generated `IF a THEN ds := TRUE; END_IF` stores a BOOL into a `DINT`, which +the assignment validation reports as E037 at each sink block. One source +fans out to a Set sink and a Reset sink, so the build fails with two +diagnostics. diff --git a/tests/lit/cfc/validation/storage_numeric/main.st b/tests/lit/cfc/validation/storage_numeric/main.st new file mode 100644 index 00000000000..646af76d836 --- /dev/null +++ b/tests/lit/cfc/validation/storage_numeric/main.st @@ -0,0 +1,8 @@ +// A storage-mode sink on a numeric variable stores the BOOL source value into it. The generated +// `IF a THEN ds := TRUE; END_IF` and `IF a THEN dr := FALSE; END_IF` are invalid assignments, +// reported once per sink block. +// CHECK: {{.*}}storage_numeric.cfc.storage_numeric:0: error[E037]: Invalid assignment: cannot assign 'BOOL' to 'DINT' +// CHECK: {{.*}}storage_numeric.cfc.storage_numeric:1: error[E037]: Invalid assignment: cannot assign 'BOOL' to 'DINT' +// CHECK: error: Compilation aborted due to critical errors +FUNCTION main : DINT +END_FUNCTION diff --git a/tests/lit/cfc/variables/storage_numeric/plc.json b/tests/lit/cfc/validation/storage_numeric/plc.json similarity index 100% rename from tests/lit/cfc/variables/storage_numeric/plc.json rename to tests/lit/cfc/validation/storage_numeric/plc.json diff --git a/tests/lit/cfc/validation/storage_numeric/run.test b/tests/lit/cfc/validation/storage_numeric/run.test new file mode 100644 index 00000000000..84e8aa7d896 --- /dev/null +++ b/tests/lit/cfc/validation/storage_numeric/run.test @@ -0,0 +1 @@ +RUN: not %COMPILE --error-format clang build plc.json 2>&1 | %CHECK main.st diff --git a/tests/lit/cfc/variables/storage_numeric/storage_numeric.cfc b/tests/lit/cfc/validation/storage_numeric/storage_numeric.cfc similarity index 100% rename from tests/lit/cfc/variables/storage_numeric/storage_numeric.cfc rename to tests/lit/cfc/validation/storage_numeric/storage_numeric.cfc diff --git a/tests/lit/cfc/variables/storage_numeric/README.md b/tests/lit/cfc/variables/storage_numeric/README.md deleted file mode 100644 index 4e7582a0677..00000000000 --- a/tests/lit/cfc/variables/storage_numeric/README.md +++ /dev/null @@ -1,5 +0,0 @@ -Pins the deliberate permissiveness of storage modes on numeric sinks -(PRG-4559 deviates from its acceptance criteria here): the ST compiler -accepts ` := TRUE`, so a `DINT` sink with Set/Reset engraves the -literal `1`/`0` instead of raising a validation error. One source fans out -to a Set and a Reset sink; a FALSE input leaves both preset values alone. diff --git a/tests/lit/cfc/variables/storage_numeric/main.st b/tests/lit/cfc/variables/storage_numeric/main.st deleted file mode 100644 index 9aeb54a1e30..00000000000 --- a/tests/lit/cfc/variables/storage_numeric/main.st +++ /dev/null @@ -1,17 +0,0 @@ -FUNCTION main : DINT - // The ST compiler accepts BOOL stores into numerics, so a numeric sink - // latches the literal 1 (Set) or 0 (Reset). Preset both to prove the - // FALSE input writes nothing. - storage_numeric.ds := 40; - storage_numeric.dr := 42; - storage_numeric.a := FALSE; - storage_numeric(); - // CHECK: idle: ds = 40, dr = 42 - printf('idle: ds = %d, dr = %d$N', storage_numeric.ds, storage_numeric.dr); - - // A TRUE input engraves the constants. - storage_numeric.a := TRUE; - storage_numeric(); - // CHECK: latched: ds = 1, dr = 0 - printf('latched: ds = %d, dr = %d$N', storage_numeric.ds, storage_numeric.dr); -END_FUNCTION diff --git a/tests/lit/cfc/variables/storage_numeric/run.test b/tests/lit/cfc/variables/storage_numeric/run.test deleted file mode 100644 index 315a40cb544..00000000000 --- a/tests/lit/cfc/variables/storage_numeric/run.test +++ /dev/null @@ -1 +0,0 @@ -RUN: %COMPILE build plc.json && %RUN | %CHECK main.st diff --git a/tests/lit/single/builtin-positional-arguments/main.st b/tests/lit/single/builtin-positional-arguments/main.st index 522aabef9d3..8fdb5147ff9 100644 --- a/tests/lit/single/builtin-positional-arguments/main.st +++ b/tests/lit/single/builtin-positional-arguments/main.st @@ -22,6 +22,7 @@ FUNCTION main : DINT VAR result : ULINT; + flag : BOOL; x : DINT := 9; y : DINT := 7331; @@ -66,16 +67,16 @@ END_VAR printf('%d$N', result); // CHECK: 0 piAddress1 := ADR(iVar1); piAddress2 := ADR(IN := iVar1); - result := (piAddress1 = piAddress2); - printf('%d$N', result); // CHECK: 1 + flag := (piAddress1 = piAddress2); + printf('%d$N', flag); // CHECK: 1 // REF - test both positional and named to verify equivalence result := 0; printf('%d$N', result); // CHECK: 0 piAddress1 := REF(iVar1); // positional piAddress2 := REF(in := iVar1); // named argument - result := (piAddress1 = piAddress2); - printf('%d$N', result); // CHECK: 1 + flag := (piAddress1 = piAddress2); + printf('%d$N', flag); // CHECK: 1 // DIV result := DIV(dividend, divisor); @@ -96,28 +97,28 @@ END_VAR x := 5; y := 10; // GT - positional only > - result := GT(y, x); - printf('%d$N', result); // CHECK: 1 + flag := GT(y, x); + printf('%d$N', flag); // CHECK: 1 // LT - positional only < - result := LT(x, y); - printf('%d$N', result); // CHECK: 1 + flag := LT(x, y); + printf('%d$N', flag); // CHECK: 1 // EQ - positional only == - result := EQ(b, b); - printf('%d$N', result); // CHECK: 1 + flag := EQ(b, b); + printf('%d$N', flag); // CHECK: 1 // NE - positional only <> - result := NE(b, c); - printf('%d$N', result); // CHECK: 1 + flag := NE(b, c); + printf('%d$N', flag); // CHECK: 1 // GE - positional only >= - result := GE(y, x); - printf('%d$N', result); // CHECK: 1 + flag := GE(y, x); + printf('%d$N', flag); // CHECK: 1 // LE - positional only <= - result := LE(x, y); - printf('%d$N', result); // CHECK: 1 + flag := LE(x, y); + printf('%d$N', flag); // CHECK: 1 test_bounds(myarray); diff --git a/tests/lit/single/init/initializer_type_mismatch.st b/tests/lit/single/init/initializer_type_mismatch.st index 7fe745973f7..62405561c15 100644 --- a/tests/lit/single/init/initializer_type_mismatch.st +++ b/tests/lit/single/init/initializer_type_mismatch.st @@ -33,6 +33,18 @@ VAR strFromBool : STRING := TRUE; // CHECK-DAG: {{.*}}initializer_type_mismatch.st:[[@LINE+1]]:{{[0-9]+}}:{{.*}}error[E037]: Invalid assignment: cannot assign 'STRING' to 'INT' int : INT := 'abc'; + // CHECK-DAG: {{.*}}initializer_type_mismatch.st:[[@LINE+1]]:{{[0-9]+}}:{{.*}}error[E037]: Invalid assignment: cannot assign 'BOOL' to 'DINT' + dintFromBool : DINT := TRUE; + // CHECK-DAG: {{.*}}initializer_type_mismatch.st:[[@LINE+1]]:{{[0-9]+}}:{{.*}}error[E037]: Invalid assignment: cannot assign 'BOOL' to 'INT' + intFromBool : INT := TRUE; + // CHECK-DAG: {{.*}}initializer_type_mismatch.st:[[@LINE+1]]:{{[0-9]+}}:{{.*}}error[E037]: Invalid assignment: cannot assign 'BOOL' to 'REAL' + realFromBool : REAL := TRUE; + // CHECK-DAG: {{.*}}initializer_type_mismatch.st:[[@LINE+1]]:{{[0-9]+}}:{{.*}}error[E037]: Invalid assignment: cannot assign 'DINT' to 'BOOL' + boolFromInt : BOOL := 5; + // CHECK-DAG: {{.*}}initializer_type_mismatch.st:[[@LINE+1]]:{{[0-9]+}}:{{.*}}error[E037]: Invalid assignment: cannot assign 'BOOL' to 'DINT' + dintsFromBool : ARRAY[0..0] OF DINT := [TRUE]; + // CHECK-DAG: {{.*}}initializer_type_mismatch.st:[[@LINE+1]]:{{[0-9]+}}:{{.*}}error[E037]: Invalid assignment: cannot assign 'STRING' to 'DINT' + dintsFromStr : ARRAY[0..0] OF DINT := ['a']; // Compatible initializers are not reported validStr : STRING := 'abc'; @@ -40,6 +52,9 @@ VAR validReal : LREAL := 3; validStruct : MyStruct := (member := 'abc'); validArray : ARRAY[0..1] OF DINT := [1, 2]; + validBool : BOOL := 1; + validBools : ARRAY[0..1] OF BOOL := [TRUE, 0]; + validNested : ARRAY[0..1] OF ARRAY[0..0] OF INT := [[1], [2]]; validFb : MyFb; END_VAR END_PROGRAM diff --git a/tests/lit/single/init/numeric_initializer_literal_casts.st b/tests/lit/single/init/numeric_initializer_literal_casts.st index 107b00d786c..f3421c4611f 100644 --- a/tests/lit/single/init/numeric_initializer_literal_casts.st +++ b/tests/lit/single/init/numeric_initializer_literal_casts.st @@ -1,36 +1,27 @@ // RUN: %COMPILE %s && %RUN | %CHECK %s -// BOOL and REAL literals are converted to the declared numeric type, both as scalar initializers -// and as elements of array literals in declarations and in the body. +// REAL literals are converted to the declared integer type, both as scalar initializers and as +// elements of array literals in declarations and in the body. FUNCTION main : DINT VAR - a : DINT := TRUE; - b : INT := TRUE; - c : REAL := TRUE; - d : LREAL := FALSE; - - dints : ARRAY[0..1] OF DINT := [TRUE, FALSE]; - lints : ARRAY[0..1] OF LINT := [FALSE, TRUE]; - reals : ARRAY[0..1] OF REAL := [TRUE, FALSE]; - lreals : ARRAY[0..1] OF LREAL := [FALSE, TRUE]; + a : INT := 3.5; + b : DINT := 2.25; ints : ARRAY[0..1] OF INT := [1.5, 2]; - later : ARRAY[0..1] OF REAL; + dints : ARRAY[0..1] OF DINT := [2.5, 3.75]; + reals : ARRAY[0..1] OF REAL := [1, 2]; + later : ARRAY[0..1] OF INT; END_VAR - later := [TRUE, FALSE]; + later := [4.5, 5]; - // CHECK: scalars: 1 1 1.000000 0.000000 - printf('scalars: %d %d %f %f$N', a, b, c, d); - // CHECK: dints: 1 0 - printf('dints: %d %d$N', dints[0], dints[1]); - // CHECK: lints: 0 1 - printf('lints: %d %d$N', lints[0], lints[1]); - // CHECK: reals: 1.000000 0.000000 - printf('reals: %f %f$N', reals[0], reals[1]); - // CHECK: lreals: 0.000000 1.000000 - printf('lreals: %f %f$N', lreals[0], lreals[1]); + // CHECK: scalars: 3 2 + printf('scalars: %d %d$N', a, b); // CHECK: ints: 1 2 printf('ints: %d %d$N', ints[0], ints[1]); - // CHECK: later: 1.000000 0.000000 - printf('later: %f %f$N', later[0], later[1]); + // CHECK: dints: 2 3 + printf('dints: %d %d$N', dints[0], dints[1]); + // CHECK: reals: 1.000000 2.000000 + printf('reals: %f %f$N', reals[0], reals[1]); + // CHECK: later: 4 5 + printf('later: %d %d$N', later[0], later[1]); END_FUNCTION From 7584dbcd03dff7836c621896443025201d9937ec Mon Sep 17 00:00:00 2001 From: Ghaith Hachem Date: Fri, 18 Sep 2026 07:23:00 +0000 Subject: [PATCH 6/6] fix(validation): reject BOOL literals in numeric typed literals Problem: `DINT#TRUE` and `REAL#TRUE` pass validation although a BOOL literal is no numeric value, while `x : BOOL := BOOL#TRUE` fails with an unresolved constant because the constant evaluator expects an integer literal behind every integer type prefix. Solution: A BOOL literal behind a non-BOOL type prefix is an incompatible literal cast (E054), like `DINT#1.5` already is. The constant evaluator keeps a BOOL literal behind a BOOL prefix as it is. The equal-operation test no longer stores a BOOL comparison into a DINT variable. Refs: PRG-4728 Co-Authored-By: Claude Fable 5.1 --- src/resolver/const_evaluator.rs | 34 ++++++--- src/validation/statement.rs | 7 ++ .../tests/literals_validation_tests.rs | 38 ++++++++++ ...sts__real_literal_casts_are_validated.snap | 12 +++ ...on_for_equal_operation_with_no_effect.snap | 24 +++--- .../tests/variable_validation_tests.rs | 75 ++++++++++++++++++- 6 files changed, 162 insertions(+), 28 deletions(-) diff --git a/src/resolver/const_evaluator.rs b/src/resolver/const_evaluator.rs index da6e2e18532..64f3cfb847f 100644 --- a/src/resolver/const_evaluator.rs +++ b/src/resolver/const_evaluator.rs @@ -738,18 +738,28 @@ fn get_cast_statement_literal( ) -> Result { let dti = index.find_effective_type_info(type_name); match dti { - Some(&DataTypeInformation::Integer { .. }) => { - let evaluated_initial = - evaluate_with_target_hint(cast_statement, scope, index, Some(type_name), lhs)? - .as_ref() - .map(|v| { - if let AstStatement::Literal(AstLiteral::Integer(value)) = v.get_stmt() { - Ok(*value) - } else { - Err(UnresolvableKind::Misc(format!("Expected integer value, found {v:?}"))) - } - }) - .transpose()?; + Some(dti @ &DataTypeInformation::Integer { .. }) => { + let evaluated = evaluate_with_target_hint(cast_statement, scope, index, Some(type_name), lhs)?; + // BOOL#TRUE stays a BOOL literal + if let (true, Some(AstStatement::Literal(AstLiteral::Bool(value)))) = + (dti.is_bool(), evaluated.as_ref().map(|it| it.get_stmt())) + { + return Ok(AstNode::new( + AstStatement::Literal(AstLiteral::Bool(*value)), + cast_statement.get_id(), + cast_statement.get_location(), + )); + } + let evaluated_initial = evaluated + .as_ref() + .map(|v| { + if let AstStatement::Literal(AstLiteral::Integer(value)) = v.get_stmt() { + Ok(*value) + } else { + Err(UnresolvableKind::Misc(format!("Expected integer value, found {v:?}"))) + } + }) + .transpose()?; if let Some(value) = evaluated_initial { return Ok(AstNode::new( diff --git a/src/validation/statement.rs b/src/validation/statement.rs index 6f372cd531f..0dc647ebc7d 100644 --- a/src/validation/statement.rs +++ b/src/validation/statement.rs @@ -611,6 +611,13 @@ fn validate_cast_literal( .with_error_code("E061") .with_location(location), ) + } else if matches!(literal, AstLiteral::Bool(_)) && !cast_type.is_bool() { + // a BOOL literal is no numeric value, e.g. DINT#TRUE + validator.push_diagnostic(incompatible_literal_cast( + cast_type.get_name(), + literal.get_literal_value().as_str(), + location.clone(), + )); } else if cast_type.is_date_or_time_type() || literal_type.is_date_or_time_type() { validator.push_diagnostic(incompatible_literal_cast( cast_type.get_name(), diff --git a/src/validation/tests/literals_validation_tests.rs b/src/validation/tests/literals_validation_tests.rs index 266fcefff1f..7c206d0573a 100644 --- a/src/validation/tests/literals_validation_tests.rs +++ b/src/validation/tests/literals_validation_tests.rs @@ -44,6 +44,44 @@ fn bool_literal_casts_are_validated() { assert_snapshot!(&diagnostics); } +#[test] +fn numeric_literal_casts_reject_bool_literals() { + let diagnostics = parse_and_validate_buffered( + " + PROGRAM prg + VAR + x : DINT; + r : REAL; + END_VAR + x := DINT#TRUE; + x := INT#FALSE; + r := REAL#TRUE; + x := DINT#1; + END_PROGRAM + ", + ); + + assert_snapshot!(&diagnostics, @" + error[E054]: Literal true is not compatible to DINT + ┌─ :7:18 + │ + 7 │ x := DINT#TRUE; + │ ^^^^^^^^^ Literal true is not compatible to DINT + + error[E054]: Literal false is not compatible to INT + ┌─ :8:18 + │ + 8 │ x := INT#FALSE; + │ ^^^^^^^^^ Literal false is not compatible to INT + + error[E054]: Literal true is not compatible to REAL + ┌─ :9:18 + │ + 9 │ r := REAL#TRUE; + │ ^^^^^^^^^ Literal true is not compatible to REAL + "); +} + #[test] fn string_literal_casts_are_validated() { let diagnostics = parse_and_validate_buffered( diff --git a/src/validation/tests/snapshots/rusty__validation__tests__literals_validation_tests__real_literal_casts_are_validated.snap b/src/validation/tests/snapshots/rusty__validation__tests__literals_validation_tests__real_literal_casts_are_validated.snap index 154fc3da8f8..d8c8239dcc5 100644 --- a/src/validation/tests/snapshots/rusty__validation__tests__literals_validation_tests__real_literal_casts_are_validated.snap +++ b/src/validation/tests/snapshots/rusty__validation__tests__literals_validation_tests__real_literal_casts_are_validated.snap @@ -2,12 +2,24 @@ source: src/validation/tests/literals_validation_tests.rs expression: "&diagnostics" --- +error[E054]: Literal true is not compatible to REAL + ┌─ :9:13 + │ +9 │ REAL#TRUE; + │ ^^^^^^^^^ Literal true is not compatible to REAL + error[E054]: Literal '3.14' is not compatible to REAL ┌─ :11:13 │ 11 │ REAL#'3.14'; │ ^^^^^^^^^^^ Literal '3.14' is not compatible to REAL +error[E054]: Literal true is not compatible to LREAL + ┌─ :13:13 + │ +13 │ LREAL#TRUE; + │ ^^^^^^^^^^ Literal true is not compatible to LREAL + error[E054]: Literal "3.14" is not compatible to LREAL ┌─ :15:13 │ 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 index 2843b375f85..fd8b8961d77 100644 --- 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 @@ -3,37 +3,37 @@ source: src/validation/tests/variable_validation_tests.rs expression: diagnostics --- warning[E023]: This equal statement has no effect, did you mean `value := 1`? - ┌─ :24:13 + ┌─ :21:13 │ -24 │ value = 1; +21 │ value = 1; │ ^^^^^^^^^ This equal statement has no effect, did you mean `value := 1`? warning[E023]: This equal statement has no effect, did you mean `value := condition`? - ┌─ :25:13 + ┌─ :22:13 │ -25 │ value = condition AND condition; +22 │ value = condition AND condition; │ ^^^^^^^^^^^^^^^^^ This equal statement has no effect, did you mean `value := condition`? warning[E023]: This equal statement has no effect, did you mean `value := condition`? - ┌─ :26:13 + ┌─ :23:13 │ -26 │ value = condition AND (condition = TRUE); +23 │ value = condition AND (condition = TRUE); │ ^^^^^^^^^^^^^^^^^ This equal statement has no effect, did you mean `value := condition`? warning[E023]: This equal statement has no effect, did you mean `condition := TRUE`? - ┌─ :26:36 + ┌─ :23:36 │ -26 │ value = condition AND (condition = TRUE); +23 │ value = condition AND (condition = TRUE); │ ^^^^^^^^^^^^^^^^ This equal statement has no effect, did you mean `condition := TRUE`? warning[E023]: This equal statement has no effect, did you mean `value := 1`? - ┌─ :28:26 + ┌─ :25:26 │ -28 │ IF TRUE THEN value = 1; END_IF +25 │ IF TRUE THEN value = 1; END_IF │ ^^^^^^^^^ This equal statement has no effect, did you mean `value := 1`? warning[E023]: This equal statement has no effect, did you mean `value := 1`? - ┌─ :29:27 + ┌─ :26:27 │ -29 │ WHILE TRUE DO value = 1; END_WHILE +26 │ WHILE TRUE DO value = 1; END_WHILE │ ^^^^^^^^^ This equal statement has no effect, did you mean `value := 1`? diff --git a/src/validation/tests/variable_validation_tests.rs b/src/validation/tests/variable_validation_tests.rs index e49d27e3dac..7c76314b3e3 100644 --- a/src/validation/tests/variable_validation_tests.rs +++ b/src/validation/tests/variable_validation_tests.rs @@ -467,17 +467,14 @@ fn assignment_suggestion_for_equal_operation_with_no_effect() { " PROGRAM main VAR - value : BOOL; + value : DINT; condition : BOOL; // These Should work - arr_eq : ARRAY[0..4] OF BOOL := [1 = 1, 2 = 2, 3 = 3, 4 = 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 @@ -2024,6 +2021,76 @@ fn fb_var_temp_visible_inside_fb_body_and_actions() { assert!(diagnostics.is_empty(), "expected clean diagnostics, got:\n{diagnostics}"); } +#[test] +fn typed_literal_initializers_are_validated_against_the_cast_type() { + let diagnostics = parse_and_validate_buffered( + r#" + PROGRAM mainProg + VAR + dintFromBool : DINT := DINT#TRUE; + dintFromReal : DINT := DINT#1.5; + realFromBool : REAL := REAL#TRUE; + boolFromInt : BOOL := BOOL#5; + validDint : DINT := DINT#5; + validReal : REAL := REAL#1.5; + validBool : BOOL := BOOL#TRUE; + validBoolFromInt : BOOL := BOOL#1; + END_VAR + END_PROGRAM + "#, + ); + + assert_snapshot!(diagnostics, @r#" + warning[E039]: This will overflow for type BOOL + ┌─ :7:40 + │ + 7 │ boolFromInt : BOOL := BOOL#5; + │ ^ This will overflow for type BOOL + + error[E054]: Literal true is not compatible to DINT + ┌─ :4:36 + │ + 4 │ dintFromBool : DINT := DINT#TRUE; + │ ^^^^^^^^^ Literal true is not compatible to DINT + + error[E033]: Unresolved constant `dintFromBool` variable: Expected integer value, found LiteralBool { value: true } + ┌─ :4:36 + │ + 4 │ dintFromBool : DINT := DINT#TRUE; + │ ^^^^^^^^^ Unresolved constant `dintFromBool` variable: Expected integer value, found LiteralBool { value: true } + + error[E054]: Literal 1.5 is not compatible to DINT + ┌─ :5:36 + │ + 5 │ dintFromReal : DINT := DINT#1.5; + │ ^^^^^^^^ Literal 1.5 is not compatible to DINT + + error[E033]: Unresolved constant `dintFromReal` variable: Expected integer value, found LiteralReal { value: "1.5" } + ┌─ :5:36 + │ + 5 │ dintFromReal : DINT := DINT#1.5; + │ ^^^^^^^^ Unresolved constant `dintFromReal` variable: Expected integer value, found LiteralReal { value: "1.5" } + + error[E054]: Literal true is not compatible to REAL + ┌─ :6:36 + │ + 6 │ realFromBool : REAL := REAL#TRUE; + │ ^^^^^^^^^ Literal true is not compatible to REAL + + error[E033]: Unresolved constant `realFromBool` variable: Expected floating point type, got: Some(LiteralBool { value: true }) + ┌─ :6:36 + │ + 6 │ realFromBool : REAL := REAL#TRUE; + │ ^^^^^^^^^ Unresolved constant `realFromBool` variable: Expected floating point type, got: Some(LiteralBool { value: true }) + + error[E053]: Literal 5 out of range (BOOL) + ┌─ :7:35 + │ + 7 │ boolFromInt : BOOL := BOOL#5; + │ ^^^^^^ Literal 5 out of range (BOOL) + "#); +} + #[test] fn initializers_with_incompatible_types_are_reported() { let diagnostics = parse_and_validate_buffered(