From 8d97223fe9f62854062dba1b347de5ce22ca5435 Mon Sep 17 00:00:00 2001 From: Michael Haselberger Date: Mon, 10 Nov 2025 13:12:25 +0100 Subject: [PATCH 1/5] feat: add typed enums Add support for typed enums with explicit integer type specifications, supporting both IEC 61131-3 standard syntax (TYPE NAME : TYPE (...)) and Codesys syntax (TYPE NAME : (...) TYPE). --- .../src/diagnostics/diagnostics_registry.rs | 1 + .../src/diagnostics/error_codes/E122.md | 61 ++ src/codegen/generators/data_type_generator.rs | 7 +- ...enums_with_initializers_are_generated.snap | 6 +- ...ith_partly_initializers_are_generated.snap | 2 +- ...n_tests__enum_referenced_in_fb_nested.snap | 7 +- src/codegen/tests/typesystem_test.rs | 57 ++ src/index/indexer/user_type_indexer.rs | 25 +- src/parser.rs | 43 +- src/parser/tests/type_parser_tests.rs | 655 ++++++++++++++++++ src/resolver.rs | 18 + src/tests/adr/enum_adr.rs | 190 ++++- src/typesystem.rs | 2 +- src/validation/tests/enum_validation_test.rs | 303 ++++++++ src/validation/types.rs | 20 + .../typed_enum_61131_syntax_with_default.st | 36 + .../typed_enum_byte_values_are_correct.st | 32 + .../typed_enum_codesys_syntax_with_default.st | 32 + .../enums/typed_enum_various_integer_types.st | 38 + ...ped_enum_with_zero_and_explicit_default.st | 32 + .../enums/typed_enum_with_zero_no_default.st | 32 + .../typed_enum_without_zero_no_default.st | 32 + .../typed_enum_without_zero_with_default.st | 36 + 23 files changed, 1648 insertions(+), 19 deletions(-) create mode 100644 compiler/plc_diagnostics/src/diagnostics/error_codes/E122.md create mode 100644 tests/lit/single/enums/typed_enum_61131_syntax_with_default.st create mode 100644 tests/lit/single/enums/typed_enum_byte_values_are_correct.st create mode 100644 tests/lit/single/enums/typed_enum_codesys_syntax_with_default.st create mode 100644 tests/lit/single/enums/typed_enum_various_integer_types.st create mode 100644 tests/lit/single/enums/typed_enum_with_zero_and_explicit_default.st create mode 100644 tests/lit/single/enums/typed_enum_with_zero_no_default.st create mode 100644 tests/lit/single/enums/typed_enum_without_zero_no_default.st create mode 100644 tests/lit/single/enums/typed_enum_without_zero_with_default.st diff --git a/compiler/plc_diagnostics/src/diagnostics/diagnostics_registry.rs b/compiler/plc_diagnostics/src/diagnostics/diagnostics_registry.rs index b386e1afc87..b2753830c14 100644 --- a/compiler/plc_diagnostics/src/diagnostics/diagnostics_registry.rs +++ b/compiler/plc_diagnostics/src/diagnostics/diagnostics_registry.rs @@ -223,6 +223,7 @@ lazy_static! { E119, Error, include_str!("./error_codes/E119.md"), // Invalid use of `SUPER` keyword E120, Error, include_str!("./error_codes/E120.md"), // Invalid use of `THIS` keyword E121, Error, include_str!("./error_codes/E121.md"), // Recursive type alias + E122, Error, include_str!("./error_codes/E122.md"), // Invalid enum base type ); } diff --git a/compiler/plc_diagnostics/src/diagnostics/error_codes/E122.md b/compiler/plc_diagnostics/src/diagnostics/error_codes/E122.md new file mode 100644 index 00000000000..bba19740aac --- /dev/null +++ b/compiler/plc_diagnostics/src/diagnostics/error_codes/E122.md @@ -0,0 +1,61 @@ +# E122: Invalid enum base type + +This error occurs when an enum is declared with a base type that is not a valid integer type. Enums in IEC 61131-3 can only use integer types as their underlying representation. + +## Example + +```st +TYPE Color : STRING (red := 1, green := 2, blue := 3); +END_TYPE +``` + +In this example, `STRING` is not a valid base type for an enum. Only integer types are allowed. + +## Another example + +```st +TYPE Status : REAL (active := 1, inactive := 0); +END_TYPE + +TYPE Timestamp : TIME (start := 0, stop := 1); +END_TYPE +``` + +These examples show other invalid base types: +- `REAL` is a floating-point type, not an integer type +- `TIME` is a time/date type, which although internally represented as an integer, should not be used as an enum base type + +## Valid integer types + +The following integer types are valid for enum base types: +- `INT`, `UINT` - 16-bit integers +- `SINT`, `USINT` - 8-bit integers +- `DINT`, `UDINT` - 32-bit integers +- `LINT`, `ULINT` - 64-bit integers +- `BYTE` - 8-bit unsigned +- `WORD` - 16-bit unsigned +- `DWORD` - 32-bit unsigned +- `LWORD` - 64-bit unsigned + +## How to fix + +**Use a valid integer type** + +Change the base type to one of the supported integer types: + +```st +TYPE Color : INT (red := 1, green := 2, blue := 3); +END_TYPE + +TYPE Status : BYTE (active := 1, inactive := 0); +END_TYPE +``` + +**Or omit the type specification** + +If no specific size is required, you can omit the type specification (will default to `DINT`): + +```st +TYPE Color (red := 1, green := 2, blue := 3); +END_TYPE +``` diff --git a/src/codegen/generators/data_type_generator.rs b/src/codegen/generators/data_type_generator.rs index a5f1175c230..a51ef38fbe3 100644 --- a/src/codegen/generators/data_type_generator.rs +++ b/src/codegen/generators/data_type_generator.rs @@ -366,8 +366,8 @@ impl<'ink> DataTypeGenerator<'ink, '_> { Some(v) => Ok((it.get_qualified_name(), v)), None => self .types_index - .get_associated_type(it.get_type_name()) - .map(get_default_for) + .get_associated_type(it.get_type_name()) + .map(get_default_for) .map(|v| (it.get_qualified_name(), v)), }) }) @@ -406,6 +406,9 @@ impl<'ink> DataTypeGenerator<'ink, '_> { DataTypeInformation::Alias { referenced_type, .. } => { self.generate_initial_value_for_type(data_type, referenced_type) } + DataTypeInformation::Enum { referenced_type, .. } => { + self.generate_initial_value_for_type(data_type, referenced_type) + } //all other types (scalars, pointer and void) _ => Ok(None), } diff --git a/src/codegen/tests/snapshots/rusty__codegen__tests__code_gen_tests__typed_enums_with_initializers_are_generated.snap b/src/codegen/tests/snapshots/rusty__codegen__tests__code_gen_tests__typed_enums_with_initializers_are_generated.snap index 991b342c23d..ccbf7fce8ff 100644 --- a/src/codegen/tests/snapshots/rusty__codegen__tests__code_gen_tests__typed_enums_with_initializers_are_generated.snap +++ b/src/codegen/tests/snapshots/rusty__codegen__tests__code_gen_tests__typed_enums_with_initializers_are_generated.snap @@ -8,9 +8,9 @@ source_filename = "" target datalayout = "[filtered]" target triple = "[filtered]" -@x = global i8 0 -@y = global i16 0 -@z = global i32 0 +@x = global i8 1 +@y = global i16 10 +@z = global i32 22 @MyEnum.red = unnamed_addr constant i8 1 @MyEnum.yellow = unnamed_addr constant i8 2 @MyEnum.green = unnamed_addr constant i8 3 diff --git a/src/codegen/tests/snapshots/rusty__codegen__tests__code_gen_tests__typed_enums_with_partly_initializers_are_generated.snap b/src/codegen/tests/snapshots/rusty__codegen__tests__code_gen_tests__typed_enums_with_partly_initializers_are_generated.snap index 955f973b91e..802d41d2259 100644 --- a/src/codegen/tests/snapshots/rusty__codegen__tests__code_gen_tests__typed_enums_with_partly_initializers_are_generated.snap +++ b/src/codegen/tests/snapshots/rusty__codegen__tests__code_gen_tests__typed_enums_with_partly_initializers_are_generated.snap @@ -9,7 +9,7 @@ target datalayout = "[filtered]" target triple = "[filtered]" @twenty = unnamed_addr constant i16 20 -@x = global i8 0 +@x = global i8 7 @MyEnum.red = unnamed_addr constant i8 7 @MyEnum.yellow = unnamed_addr constant i8 8 @MyEnum.green = unnamed_addr constant i8 9 diff --git a/src/codegen/tests/snapshots/rusty__codegen__tests__multifile_codegen_tests__enum_referenced_in_fb_nested.snap b/src/codegen/tests/snapshots/rusty__codegen__tests__multifile_codegen_tests__enum_referenced_in_fb_nested.snap index 27d6d9e1ea0..9ebcf75f16a 100644 --- a/src/codegen/tests/snapshots/rusty__codegen__tests__multifile_codegen_tests__enum_referenced_in_fb_nested.snap +++ b/src/codegen/tests/snapshots/rusty__codegen__tests__multifile_codegen_tests__enum_referenced_in_fb_nested.snap @@ -1,6 +1,7 @@ --- source: src/codegen/tests/multifile_codegen_tests.rs expression: "codegen_multi(units, crate::DebugLevel::None).join(\"\\n\")" +snapshot_kind: text --- ; ModuleID = 'myEnum.st' source_filename = "myEnum.st" @@ -18,7 +19,7 @@ target triple = "[filtered]" %fb = type { i32 } -@__fb__init = unnamed_addr constant %fb zeroinitializer +@__fb__init = unnamed_addr constant %fb { i32 1 } define void @fb(%fb* %0) { entry: @@ -36,7 +37,7 @@ target triple = "[filtered]" %myStruct = type { %fb.2 } %fb.2 = type { i32 } -@__myStruct__init = unnamed_addr constant %myStruct zeroinitializer +@__myStruct__init = unnamed_addr constant %myStruct { %fb.2 { i32 1 } } @__fb__init = external unnamed_addr constant %fb.2 declare void @fb(%fb.2*) @@ -50,7 +51,7 @@ target triple = "[filtered]" %myStruct.4 = type { %fb.5 } %fb.5 = type { i32 } -@__fb2__init = unnamed_addr constant %fb2 zeroinitializer +@__fb2__init = unnamed_addr constant %fb2 { %myStruct.4 { %fb.5 { i32 1 } } } @__myStruct__init = external unnamed_addr constant %myStruct.4 @__fb__init = external unnamed_addr constant %fb.5 diff --git a/src/codegen/tests/typesystem_test.rs b/src/codegen/tests/typesystem_test.rs index 3928ec0a6f1..1b532edcd6f 100644 --- a/src/codegen/tests/typesystem_test.rs +++ b/src/codegen/tests/typesystem_test.rs @@ -312,6 +312,63 @@ fn small_int_varargs_get_promoted_while_32bit_and_higher_keep_their_type() { filtered_assert_snapshot!(result); } +#[test] +fn enum_typed_varargs_get_promoted() { + let src = r#" + {external} + FUNCTION printf : DINT + VAR_IN_OUT + format: STRING; + END_VAR + VAR_INPUT + args: ...; + END_VAR + END_FUNCTION + + TYPE MyEnum : INT (a := 10, b := 20); + END_TYPE + + FUNCTION main : DINT + VAR + e1 : MyEnum := a; + i1 : INT := 10; + END_VAR + printf('result : %d %d$N', e1, i1); + END_FUNCTION + "#; + + let result = codegen(src); + filtered_assert_snapshot!(result, @r#" + ; ModuleID = '' + source_filename = "" + target datalayout = "[filtered]" + target triple = "[filtered]" + + @MyEnum.a = unnamed_addr constant i16 10 + @MyEnum.b = unnamed_addr constant i16 20 + @utf08_literal_0 = private unnamed_addr constant [16 x i8] c"result : %d %d\0A\00" + + declare i32 @printf(i8*, ...) + + define i32 @main() { + entry: + %main = alloca i32, align 4 + %e1 = alloca i16, align 2 + %i1 = alloca i16, align 2 + store i16 10, i16* %e1, align 2 + store i16 10, i16* %i1, align 2 + store i32 0, i32* %main, align 4 + %load_e1 = load i16, i16* %e1, align 2 + %0 = sext i16 %load_e1 to i32 + %load_i1 = load i16, i16* %i1, align 2 + %1 = sext i16 %load_i1 to i32 + %call = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([16 x i8], [16 x i8]* @utf08_literal_0, i32 0, i32 0), i32 %0, i32 %1) + %main_ret = load i32, i32* %main, align 4 + ret i32 %main_ret + } + "#); +} + #[test] fn self_referential_struct_via_reference_codegen() { let result = codegen( diff --git a/src/index/indexer/user_type_indexer.rs b/src/index/indexer/user_type_indexer.rs index 7ebd0139fa0..3a73834f1e5 100644 --- a/src/index/indexer/user_type_indexer.rs +++ b/src/index/indexer/user_type_indexer.rs @@ -292,8 +292,10 @@ impl UserTypeIndexer<'_, '_> { fn index_enum_type(&mut self, name: &str, numeric_type: &str, elements: &AstNode) { let mut variants = Vec::new(); + let mut zero_value_const_id: Option = None; + let mut first_element_const_id: Option = None; - for ele in flatten_expression_list(elements) { + for (idx, ele) in flatten_expression_list(elements).iter().enumerate() { let variant = get_enum_element_name(ele); if let AstStatement::Assignment(Assignment { right, .. }) = ele.get_stmt() { let scope = self.current_scope(); @@ -304,6 +306,16 @@ impl UserTypeIndexer<'_, '_> { None, ); + // Track if we have a zero element and remember its ConstId + if let AstStatement::Literal(AstLiteral::Integer(0)) = right.get_stmt() { + zero_value_const_id = Some(init); + } + + // Remember the first element's ConstId + if idx == 0 { + first_element_const_id = Some(init); + } + variants.push(self.index.register_enum_variant( name, &variant, @@ -315,6 +327,17 @@ impl UserTypeIndexer<'_, '_> { } } + // If no explicit initializer was provided, determine the default value + if self.pending_initializer.is_none() { + self.pending_initializer = if zero_value_const_id.is_some() { + // If zero is defined, use its ConstId + zero_value_const_id + } else { + // Otherwise use the first element's ConstId + first_element_const_id + }; + } + let information = DataTypeInformation::Enum { name: name.to_owned(), variants, diff --git a/src/parser.rs b/src/parser.rs index 4df9893b3aa..e50d315e519 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -963,7 +963,6 @@ fn parse_data_type_definition( } else if lexer.try_consume(KeywordRef) { parse_pointer_definition(lexer, name, lexer.last_range.start, None, true, false) } else if lexer.try_consume(KeywordParensOpen) { - //enum without datatype parse_enum_type_definition(lexer, name) } else if lexer.token == KeywordString || lexer.token == KeywordWideString { parse_string_type_definition(lexer, name) @@ -1095,6 +1094,9 @@ fn parse_string_size_expression(lexer: &mut ParseSession) -> Option { let size_expr = parse_expression(lexer); let error_range = lexer.source_range_factory.create_range(opening_location..lexer.range().end); + // Don't emit warnings if this looks like an enum (will be caught by validation) + let is_enum_like = matches!(size_expr.get_stmt(), AstStatement::ExpressionList(_)); + if (opening_token == KeywordParensOpen && lexer.token == KeywordSquareParensClose) || (opening_token == KeywordSquareParensOpen && lexer.token == KeywordParensClose) { @@ -1103,7 +1105,7 @@ fn parse_string_size_expression(lexer: &mut ParseSession) -> Option { .with_location(error_range) .with_error_code("E009"), ); - } else if opening_token == KeywordParensOpen || lexer.token == KeywordParensClose { + } else if !is_enum_like && (opening_token == KeywordParensOpen || lexer.token == KeywordParensClose) { lexer.accept_diagnostic(Diagnostic::new( "Unusual type of parentheses around string size expression, consider using square parentheses '[]'"). with_location(error_range) @@ -1131,13 +1133,33 @@ fn parse_string_type_definition( let end = lexer.last_range.end; let location = lexer.source_range_factory.create_range(start..end); - match (size, &name) { - (Some(size), _) => Some(DataTypeDeclaration::Definition { + // Check if this is actually an enum type (e.g., STRING (a := 1, b := 2)) + // If size is an ExpressionList with assignments, it's likely an invalid enum definition + let is_enum_like = matches!( + &size, + Some(AstNode { stmt: AstStatement::ExpressionList(_), .. }) + ); + + match (size, &name, is_enum_like) { + (Some(size), _, true) => { + // This looks like an enum definition with STRING/WSTRING as the type + // Create an EnumType so validation can catch it as invalid + Some(DataTypeDeclaration::Definition { + data_type: Box::new(DataType::EnumType { + name, + numeric_type: text, + elements: size, + }), + location, + scope: lexer.scope.clone(), + }) + } + (Some(size), _, false) => Some(DataTypeDeclaration::Definition { data_type: Box::new(DataType::StringType { name, is_wide, size: Some(size) }), location, scope: lexer.scope.clone(), }), - (None, Some(name)) => Some(DataTypeDeclaration::Definition { + (None, Some(name), _) => Some(DataTypeDeclaration::Definition { data_type: Box::new(DataType::SubRangeType { name: Some(name.into()), referenced_type: text, @@ -1164,10 +1186,19 @@ fn parse_enum_type_definition( let elements = parse_expression_list(lexer); Some(elements) })?; + + // Check for Codesys-style type specification after the enum list + // TYPE COLOR : (...) DWORD; + let numeric_type = if lexer.token == Identifier { + lexer.slice_and_advance() + } else { + DINT_TYPE.to_string() + }; + let initializer = lexer.try_consume(KeywordAssignment).then(|| parse_expression(lexer)); Some(( DataTypeDeclaration::Definition { - data_type: Box::new(DataType::EnumType { name, elements, numeric_type: DINT_TYPE.to_string() }), + data_type: Box::new(DataType::EnumType { name, elements, numeric_type }), location: start.span(&lexer.last_location()), scope: lexer.scope.clone(), }, diff --git a/src/parser/tests/type_parser_tests.rs b/src/parser/tests/type_parser_tests.rs index 770aad11cea..26e230ec7ee 100644 --- a/src/parser/tests/type_parser_tests.rs +++ b/src/parser/tests/type_parser_tests.rs @@ -323,3 +323,658 @@ fn optional_semicolon_at_end_of_endstruct_keyword_is_consumed() { assert!(diagnostics.is_empty()) } + +#[test] +fn enum_61131_standard_style_type_before_list() { + // TYPE COLOR : DWORD (...) := default; + let (result, diagnostics) = parse( + r#" + TYPE COLOR : DWORD ( + white := 16#FFFFFF00, + yellow := 16#FFFF0000, + green := 16#FF00FF00, + blue := 16#FF0000FF, + black := 16#88000000 + ) := black; + END_TYPE + "#, + ); + assert_debug_snapshot!(result.user_types[0], @r#" + UserTypeDeclaration { + data_type: EnumType { + name: Some( + "COLOR", + ), + numeric_type: "DWORD", + elements: ExpressionList { + expressions: [ + Assignment { + left: ReferenceExpr { + kind: Member( + Identifier { + name: "white", + }, + ), + base: None, + }, + right: LiteralInteger { + value: 4294967040, + }, + }, + Assignment { + left: ReferenceExpr { + kind: Member( + Identifier { + name: "yellow", + }, + ), + base: None, + }, + right: LiteralInteger { + value: 4294901760, + }, + }, + Assignment { + left: ReferenceExpr { + kind: Member( + Identifier { + name: "green", + }, + ), + base: None, + }, + right: LiteralInteger { + value: 4278255360, + }, + }, + Assignment { + left: ReferenceExpr { + kind: Member( + Identifier { + name: "blue", + }, + ), + base: None, + }, + right: LiteralInteger { + value: 4278190335, + }, + }, + Assignment { + left: ReferenceExpr { + kind: Member( + Identifier { + name: "black", + }, + ), + base: None, + }, + right: LiteralInteger { + value: 2281701376, + }, + }, + ], + }, + }, + initializer: Some( + ReferenceExpr { + kind: Member( + Identifier { + name: "black", + }, + ), + base: None, + }, + ), + scope: None, + } + "#); + assert_eq!(diagnostics.len(), 0); +} + +#[test] +fn enum_mixed_style_type_before_and_after_list() { + // TYPE COLOR : INT (...) DWORD - invalid mixed syntax + let (_result, diagnostics) = parse_buffered( + r#" + TYPE MyEnum : INT (a := 1, b := 2) DWORD; + END_TYPE + "#, + ); + // This should produce a diagnostic since types are specified twice + assert!(!diagnostics.is_empty(), "Expected diagnostic for mixed enum type syntax"); + assert_snapshot!(diagnostics, @r" + error[E007]: Unexpected token: expected KeywordSemicolon but found 'DWORD' + ┌─ :2:44 + │ + 2 │ TYPE MyEnum : INT (a := 1, b := 2) DWORD; + │ ^^^^^ Unexpected token: expected KeywordSemicolon but found 'DWORD' + "); +} + +#[test] +fn enum_codesys_style_type_after_list() { + // TYPE COLOR : (...) DWORD := default; + let (result, diagnostics) = parse( + r#" + TYPE COLOR : ( + white := 16#FFFFFF00, + yellow := 16#FFFF0000, + green := 16#FF00FF00, + blue := 16#FF0000FF, + black := 16#88000000 + ) DWORD := black; + END_TYPE + "#, + ); + assert_debug_snapshot!(result.user_types[0], @r#" + UserTypeDeclaration { + data_type: EnumType { + name: Some( + "COLOR", + ), + numeric_type: "DWORD", + elements: ExpressionList { + expressions: [ + Assignment { + left: ReferenceExpr { + kind: Member( + Identifier { + name: "white", + }, + ), + base: None, + }, + right: LiteralInteger { + value: 4294967040, + }, + }, + Assignment { + left: ReferenceExpr { + kind: Member( + Identifier { + name: "yellow", + }, + ), + base: None, + }, + right: LiteralInteger { + value: 4294901760, + }, + }, + Assignment { + left: ReferenceExpr { + kind: Member( + Identifier { + name: "green", + }, + ), + base: None, + }, + right: LiteralInteger { + value: 4278255360, + }, + }, + Assignment { + left: ReferenceExpr { + kind: Member( + Identifier { + name: "blue", + }, + ), + base: None, + }, + right: LiteralInteger { + value: 4278190335, + }, + }, + Assignment { + left: ReferenceExpr { + kind: Member( + Identifier { + name: "black", + }, + ), + base: None, + }, + right: LiteralInteger { + value: 2281701376, + }, + }, + ], + }, + }, + initializer: Some( + ReferenceExpr { + kind: Member( + Identifier { + name: "black", + }, + ), + base: None, + }, + ), + scope: None, + } + "#); + assert_eq!(diagnostics.len(), 0); +} + +#[test] +fn enum_with_default_value_no_type_specified() { + let (result, diagnostics) = parse( + r#" + TYPE STATE : (idle := 0, running := 1, stopped := 2) := running; + END_TYPE + "#, + ); + assert_debug_snapshot!(result.user_types[0], @r#" + UserTypeDeclaration { + data_type: EnumType { + name: Some( + "STATE", + ), + numeric_type: "DINT", + elements: ExpressionList { + expressions: [ + Assignment { + left: ReferenceExpr { + kind: Member( + Identifier { + name: "idle", + }, + ), + base: None, + }, + right: LiteralInteger { + value: 0, + }, + }, + Assignment { + left: ReferenceExpr { + kind: Member( + Identifier { + name: "running", + }, + ), + base: None, + }, + right: LiteralInteger { + value: 1, + }, + }, + Assignment { + left: ReferenceExpr { + kind: Member( + Identifier { + name: "stopped", + }, + ), + base: None, + }, + right: LiteralInteger { + value: 2, + }, + }, + ], + }, + }, + initializer: Some( + ReferenceExpr { + kind: Member( + Identifier { + name: "running", + }, + ), + base: None, + }, + ), + scope: None, + } + "#); + assert_eq!(diagnostics.len(), 0); +} + +#[test] +fn enum_61131_style_with_byte_type_and_default() { + let (result, diagnostics) = parse( + r#" + TYPE STATE : BYTE (idle := 0, running := 1, stopped := 2) := running; + END_TYPE + "#, + ); + assert_debug_snapshot!(result.user_types[0], @r#" + UserTypeDeclaration { + data_type: EnumType { + name: Some( + "STATE", + ), + numeric_type: "BYTE", + elements: ExpressionList { + expressions: [ + Assignment { + left: ReferenceExpr { + kind: Member( + Identifier { + name: "idle", + }, + ), + base: None, + }, + right: LiteralInteger { + value: 0, + }, + }, + Assignment { + left: ReferenceExpr { + kind: Member( + Identifier { + name: "running", + }, + ), + base: None, + }, + right: LiteralInteger { + value: 1, + }, + }, + Assignment { + left: ReferenceExpr { + kind: Member( + Identifier { + name: "stopped", + }, + ), + base: None, + }, + right: LiteralInteger { + value: 2, + }, + }, + ], + }, + }, + initializer: Some( + ReferenceExpr { + kind: Member( + Identifier { + name: "running", + }, + ), + base: None, + }, + ), + scope: None, + } + "#); + assert_eq!(diagnostics.len(), 0); +} + +#[test] +fn enum_codesys_style_with_int_type_and_default() { + let (result, diagnostics) = parse( + r#" + TYPE PRIORITY : (low := 10, medium := 20, high := 30) INT := medium; + END_TYPE + "#, + ); + assert_debug_snapshot!(result.user_types[0], @r#" + UserTypeDeclaration { + data_type: EnumType { + name: Some( + "PRIORITY", + ), + numeric_type: "INT", + elements: ExpressionList { + expressions: [ + Assignment { + left: ReferenceExpr { + kind: Member( + Identifier { + name: "low", + }, + ), + base: None, + }, + right: LiteralInteger { + value: 10, + }, + }, + Assignment { + left: ReferenceExpr { + kind: Member( + Identifier { + name: "medium", + }, + ), + base: None, + }, + right: LiteralInteger { + value: 20, + }, + }, + Assignment { + left: ReferenceExpr { + kind: Member( + Identifier { + name: "high", + }, + ), + base: None, + }, + right: LiteralInteger { + value: 30, + }, + }, + ], + }, + }, + initializer: Some( + ReferenceExpr { + kind: Member( + Identifier { + name: "medium", + }, + ), + base: None, + }, + ), + scope: None, + } + "#); + assert_eq!(diagnostics.len(), 0); +} + +#[test] +fn enum_with_zero_element_no_default() { + let (result, diagnostics) = parse( + r#" + TYPE STATE_WITH_ZERO : BYTE (idle := 0, running := 1, stopped := 2); + END_TYPE + "#, + ); + assert_debug_snapshot!(result.user_types[0], @r#" + UserTypeDeclaration { + data_type: EnumType { + name: Some( + "STATE_WITH_ZERO", + ), + numeric_type: "BYTE", + elements: ExpressionList { + expressions: [ + Assignment { + left: ReferenceExpr { + kind: Member( + Identifier { + name: "idle", + }, + ), + base: None, + }, + right: LiteralInteger { + value: 0, + }, + }, + Assignment { + left: ReferenceExpr { + kind: Member( + Identifier { + name: "running", + }, + ), + base: None, + }, + right: LiteralInteger { + value: 1, + }, + }, + Assignment { + left: ReferenceExpr { + kind: Member( + Identifier { + name: "stopped", + }, + ), + base: None, + }, + right: LiteralInteger { + value: 2, + }, + }, + ], + }, + }, + initializer: None, + scope: None, + } + "#); + assert_eq!(diagnostics.len(), 0); +} + +#[test] +fn enum_without_zero_no_default() { + let (result, diagnostics) = parse( + r#" + TYPE PRIORITY : INT (low := 10, medium := 20, high := 30); + END_TYPE + "#, + ); + assert_debug_snapshot!(result.user_types[0], @r#" + UserTypeDeclaration { + data_type: EnumType { + name: Some( + "PRIORITY", + ), + numeric_type: "INT", + elements: ExpressionList { + expressions: [ + Assignment { + left: ReferenceExpr { + kind: Member( + Identifier { + name: "low", + }, + ), + base: None, + }, + right: LiteralInteger { + value: 10, + }, + }, + Assignment { + left: ReferenceExpr { + kind: Member( + Identifier { + name: "medium", + }, + ), + base: None, + }, + right: LiteralInteger { + value: 20, + }, + }, + Assignment { + left: ReferenceExpr { + kind: Member( + Identifier { + name: "high", + }, + ), + base: None, + }, + right: LiteralInteger { + value: 30, + }, + }, + ], + }, + }, + initializer: None, + scope: None, + } + "#); + assert_eq!(diagnostics.len(), 0); +} + +#[test] +fn enum_with_no_elements_produces_syntax_error() { + // Empty enums are syntactically invalid - parser requires at least one element + let (result, diagnostics) = parse_buffered( + r#" + TYPE EMPTY_ENUM : INT (); + END_TYPE + + TYPE ANOTHER_EMPTY_ENUM : () INT; + "#, + ); + assert!(diagnostics.len() > 0); + assert_snapshot!(diagnostics, @r" + error[E007]: Unexpected token: expected Literal but found ) + ┌─ :2:32 + │ + 2 │ TYPE EMPTY_ENUM : INT (); + │ ^ Unexpected token: expected Literal but found ) + + error[E007]: Unexpected token: expected Literal but found ) + ┌─ :5:36 + │ + 5 │ TYPE ANOTHER_EMPTY_ENUM : () INT; + │ ^ Unexpected token: expected Literal but found ) + + error[E007]: Unexpected token: expected KeywordEndType but found '' + ┌─ :6:9 + │ + 6 │ + │ ^ Unexpected token: expected KeywordEndType but found '' + "); + // User type should still be created despite the error (error recovery) + assert_debug_snapshot!(result.user_types[0], @r#" + UserTypeDeclaration { + data_type: SubRangeType { + name: Some( + "EMPTY_ENUM", + ), + referenced_type: "INT", + bounds: Some( + EmptyStatement, + ), + }, + initializer: None, + scope: None, + } + "#); + assert_debug_snapshot!(result.user_types[1], @r#" + UserTypeDeclaration { + data_type: EnumType { + name: Some( + "ANOTHER_EMPTY_ENUM", + ), + numeric_type: "INT", + elements: EmptyStatement, + }, + initializer: None, + scope: None, + } + "#); +} diff --git a/src/resolver.rs b/src/resolver.rs index 330d3d7c18c..73549a0c9a2 100644 --- a/src/resolver.rs +++ b/src/resolver.rs @@ -387,6 +387,24 @@ impl TypeAnnotator<'_> { ) .get_name() } + // Enum types need to be promoted based on their underlying integer type + DataTypeInformation::Enum { referenced_type, .. } => { + let enum_base_type = self.index.get_effective_type_or_void_by_name(referenced_type); + if let DataTypeInformation::Integer { .. } = enum_base_type.get_type_information() { + if !enum_base_type.information.is_bool() && !enum_base_type.information.is_character() { + get_bigger_type( + enum_base_type, + self.index.get_type_or_panic(DINT_TYPE), + self.index, + ) + .get_name() + } else { + type_name + } + } else { + type_name + } + } _ => type_name, } } else { diff --git a/src/tests/adr/enum_adr.rs b/src/tests/adr/enum_adr.rs index 57d490b2df9..282f1ebca07 100644 --- a/src/tests/adr/enum_adr.rs +++ b/src/tests/adr/enum_adr.rs @@ -56,8 +56,8 @@ fn enums_constants_are_automatically_numbered_or_user_defined() { target datalayout = "[filtered]" target triple = "[filtered]" - @myColor = global i32 0 - @myState = global i8 0 + @myColor = global i32 1 + @myState = global i8 1 @Color.red = unnamed_addr constant i32 1 @Color.yellow = unnamed_addr constant i32 2 @Color.green = unnamed_addr constant i32 4 @@ -149,3 +149,189 @@ fn using_enums() { } "#); } + +/// If zero is defined in an enum and no default value is specified, +/// the enum should be initialized with 0 +#[test] +fn enum_with_zero_element_no_default_initializes_to_zero() { + let src = r#" + TYPE STATE_WITH_ZERO : BYTE ( + idle := 0, + running := 1, + stopped := 2 + ); + END_TYPE + + VAR_GLOBAL + myState : STATE_WITH_ZERO; + END_VAR"#; + + // Should initialize to 0 (idle) + filtered_assert_snapshot!(codegen(src), @r#" + ; ModuleID = '' + source_filename = "" + target datalayout = "[filtered]" + target triple = "[filtered]" + + @myState = global i8 0 + @STATE_WITH_ZERO.idle = unnamed_addr constant i8 0 + @STATE_WITH_ZERO.running = unnamed_addr constant i8 1 + @STATE_WITH_ZERO.stopped = unnamed_addr constant i8 2 + "#); +} + +/// If zero is defined in an enum with a default value, +/// the enum should be initialized with the default value +#[test] +fn enum_with_zero_element_and_default_initializes_to_default() { + let src = r#" + TYPE STATE_WITH_DEFAULT : BYTE ( + idle := 0, + running := 1, + stopped := 2 + ) := running; + END_TYPE + + VAR_GLOBAL + myState : STATE_WITH_DEFAULT; + END_VAR"#; + + // Should initialize to 1 (running) + filtered_assert_snapshot!(codegen(src), @r#" + ; ModuleID = '' + source_filename = "" + target datalayout = "[filtered]" + target triple = "[filtered]" + + @myState = global i8 1 + @STATE_WITH_DEFAULT.idle = unnamed_addr constant i8 0 + @STATE_WITH_DEFAULT.running = unnamed_addr constant i8 1 + @STATE_WITH_DEFAULT.stopped = unnamed_addr constant i8 2 + "#); +} + +/// If no zero is defined and no default value is specified, +/// the enum should be initialized with the first element +#[test] +fn enum_without_zero_no_default_initializes_to_first_element() { + let src = r#" + TYPE PRIORITY : INT ( + low := 10, + medium := 20, + high := 30 + ); + END_TYPE + + VAR_GLOBAL + myPriority : PRIORITY; + END_VAR"#; + + // Should initialize to 10 (low - first element) + filtered_assert_snapshot!(codegen(src), @r#" + ; ModuleID = '' + source_filename = "" + target datalayout = "[filtered]" + target triple = "[filtered]" + + @myPriority = global i16 10 + @PRIORITY.low = unnamed_addr constant i16 10 + @PRIORITY.medium = unnamed_addr constant i16 20 + @PRIORITY.high = unnamed_addr constant i16 30 + "#); +} + +/// If no zero is defined but a default value is specified, +/// the enum should be initialized with the default value +#[test] +fn enum_without_zero_with_default_initializes_to_default() { + let src = r#" + TYPE PRIORITY_WITH_DEFAULT : INT ( + low := 10, + medium := 20, + high := 30 + ) := medium; + END_TYPE + + VAR_GLOBAL + myPriority : PRIORITY_WITH_DEFAULT; + END_VAR"#; + + // Should initialize to 20 (medium) + filtered_assert_snapshot!(codegen(src), @r#" + ; ModuleID = '' + source_filename = "" + target datalayout = "[filtered]" + target triple = "[filtered]" + + @myPriority = global i16 20 + @PRIORITY_WITH_DEFAULT.low = unnamed_addr constant i16 10 + @PRIORITY_WITH_DEFAULT.medium = unnamed_addr constant i16 20 + @PRIORITY_WITH_DEFAULT.high = unnamed_addr constant i16 30 + "#); +} + +/// Test 61131-Standard style syntax: TYPE COLOR : DWORD (...) := default; +#[test] +fn enum_61131_standard_style_with_type_before_list() { + let src = r#" + TYPE COLOR : DWORD ( + white := 16#FFFFFF00, + yellow := 16#FFFF0000, + green := 16#FF00FF00, + blue := 16#FF0000FF, + black := 16#88000000 + ) := black; + END_TYPE + + VAR_GLOBAL + myColor : COLOR; + END_VAR"#; + + // Should initialize to 16#88000000 (black) + filtered_assert_snapshot!(codegen(src), @r#" + ; ModuleID = '' + source_filename = "" + target datalayout = "[filtered]" + target triple = "[filtered]" + + @myColor = global i32 -2013265920 + @COLOR.white = unnamed_addr constant i32 -256 + @COLOR.yellow = unnamed_addr constant i32 -65536 + @COLOR.green = unnamed_addr constant i32 -16711936 + @COLOR.blue = unnamed_addr constant i32 -16776961 + @COLOR.black = unnamed_addr constant i32 -2013265920 + "#); +} + +/// Test Codesys style syntax: TYPE COLOR : (...) DWORD := default; +#[test] +fn enum_codesys_style_with_type_after_list() { + let src = r#" + TYPE COLOR_CODESYS : ( + white := 16#FFFFFF00, + yellow := 16#FFFF0000, + green := 16#FF00FF00, + blue := 16#FF0000FF, + black := 16#88000000 + ) DWORD := black; + END_TYPE + + VAR_GLOBAL + myColor : COLOR_CODESYS; + END_VAR"#; + + // Should initialize to 16#88000000 (black) + filtered_assert_snapshot!(codegen(src), @r#" + ; ModuleID = '' + source_filename = "" + target datalayout = "[filtered]" + target triple = "[filtered]" + + @myColor = global i32 -2013265920 + @COLOR_CODESYS.white = unnamed_addr constant i32 -256 + @COLOR_CODESYS.yellow = unnamed_addr constant i32 -65536 + @COLOR_CODESYS.green = unnamed_addr constant i32 -16711936 + @COLOR_CODESYS.blue = unnamed_addr constant i32 -16776961 + @COLOR_CODESYS.black = unnamed_addr constant i32 -2013265920 + "#); +} diff --git a/src/typesystem.rs b/src/typesystem.rs index dd9d8f3de31..da7e6487f68 100644 --- a/src/typesystem.rs +++ b/src/typesystem.rs @@ -500,7 +500,7 @@ impl DataTypeInformation { } pub fn is_int(&self) -> bool { - // internally an enum is represented as a DINT + // includes enums as they are represented as integers internally matches!(self, DataTypeInformation::Integer { .. } | DataTypeInformation::Enum { .. }) } diff --git a/src/validation/tests/enum_validation_test.rs b/src/validation/tests/enum_validation_test.rs index cb3756f6b10..133cf242f4a 100644 --- a/src/validation/tests/enum_validation_test.rs +++ b/src/validation/tests/enum_validation_test.rs @@ -117,3 +117,306 @@ fn enum_variants_mismatch_but_values_are_identical() { assert_snapshot!(diagnostics); } + +#[test] +fn enum_with_invalid_type() { + let diagnostics = parse_and_validate_buffered( + " + TYPE MyIntAlias : INT; END_TYPE + TYPE MyRealAlias : REAL; END_TYPE + TYPE MyStruct : STRUCT x: INT; END_STRUCT; END_TYPE + TYPE MyArray : ARRAY[1..10] OF INT; END_TYPE + TYPE MyStringAlias : STRING; END_TYPE + + // Invalid: REAL + TYPE InvalidEnum1 : REAL (red := 1, green := 2, blue := 3); END_TYPE + + // Invalid: STRING + TYPE InvalidEnum2 : STRING (a := 1, b := 2); END_TYPE + + // Invalid: REAL alias + TYPE InvalidEnum3 : MyRealAlias (x := 1, y := 2); END_TYPE + + // Invalid: Non-existent type + TYPE InvalidEnum4 : NonExistentType (p := 1, q := 2); END_TYPE + + // Invalid: WSTRING + TYPE InvalidEnum5 : WSTRING (a := 1, b := 2); END_TYPE + + // Invalid: Struct type + TYPE InvalidEnum6 : MyStruct (red := 1, blue := 2); END_TYPE + + // Invalid: Array type + TYPE InvalidEnum7 : MyArray (a := 1, b := 2); END_TYPE + + // Invalid: String alias + TYPE InvalidEnum8 : MyStringAlias (p := 1, q := 2); END_TYPE + + // Invalid: LREAL (floating point) + TYPE InvalidEnum9 : LREAL (low := 1, high := 2); END_TYPE + + // Valid: INT (61131 standard syntax) + TYPE ValidEnum1 : INT (red := 1, green := 2); END_TYPE + + // Valid: INT (Codesys syntax) + TYPE ValidEnum2 : (red := 1, green := 2) INT; END_TYPE + + // Valid: DWORD (61131 standard syntax) + TYPE ValidEnum3 : DWORD (a := 1, b := 2); END_TYPE + + // Valid: DWORD (Codesys syntax) + TYPE ValidEnum4 : (a := 1, b := 2) DWORD; END_TYPE + + // Valid: BYTE + TYPE ValidEnum5 : BYTE (x := 1, y := 2); END_TYPE + + // Valid: BYTE (Codesys syntax) + TYPE ValidEnum6 : (x := 1, y := 2) BYTE; END_TYPE + + // Valid: INT alias (61131 syntax) + TYPE ValidEnum7 : MyIntAlias (z := 1, w := 2); END_TYPE + + // Valid: INT alias (Codesys syntax) + TYPE ValidEnum8 : (z := 1, w := 2) MyIntAlias; END_TYPE + + // Valid: BOOL + TYPE ValidEnum9 : BOOL (false_val := 0, true_val := 1); END_TYPE + + // Valid: BOOL (Codesys syntax) + TYPE ValidEnum10 : (false_val := 0, true_val := 1) BOOL; END_TYPE + + // Valid: All integer types (61131 syntax) + TYPE ValidEnum11 : LINT (a := 1, b := 2); END_TYPE + TYPE ValidEnum12 : ULINT (a := 1, b := 2); END_TYPE + TYPE ValidEnum13 : USINT (a := 1, b := 2); END_TYPE + TYPE ValidEnum14 : UINT (a := 1, b := 2); END_TYPE + TYPE ValidEnum15 : UDINT (a := 1, b := 2); END_TYPE + TYPE ValidEnum16 : WORD (a := 1, b := 2); END_TYPE + TYPE ValidEnum17 : LWORD (a := 1, b := 2); END_TYPE + TYPE ValidEnum18 : SINT (a := 1, b := 2); END_TYPE + TYPE ValidEnum19 : DINT (a := 1, b := 2); END_TYPE + + // Valid: Some integer types (Codesys syntax) + TYPE ValidEnum20 : (a := 1, b := 2) LINT; END_TYPE + TYPE ValidEnum21 : (a := 1, b := 2) ULINT; END_TYPE + TYPE ValidEnum22 : (a := 1, b := 2) WORD; END_TYPE + TYPE ValidEnum23 : (a := 1, b := 2) LWORD; END_TYPE + ", + ); + + assert_snapshot!(diagnostics, @r" + error[E122]: Invalid type 'REAL' for enum. Only integer types are allowed + ┌─ :9:14 + │ + 9 │ TYPE InvalidEnum1 : REAL (red := 1, green := 2, blue := 3); END_TYPE + │ ^^^^^^^^^^^^ Invalid type 'REAL' for enum. Only integer types are allowed + + error[E122]: Invalid type 'STRING' for enum. Only integer types are allowed + ┌─ :12:14 + │ + 12 │ TYPE InvalidEnum2 : STRING (a := 1, b := 2); END_TYPE + │ ^^^^^^^^^^^^ Invalid type 'STRING' for enum. Only integer types are allowed + + error[E122]: Invalid type 'MyRealAlias' for enum. Only integer types are allowed + ┌─ :15:14 + │ + 15 │ TYPE InvalidEnum3 : MyRealAlias (x := 1, y := 2); END_TYPE + │ ^^^^^^^^^^^^ Invalid type 'MyRealAlias' for enum. Only integer types are allowed + + error[E052]: Unknown type: NonExistentType + ┌─ :18:14 + │ + 18 │ TYPE InvalidEnum4 : NonExistentType (p := 1, q := 2); END_TYPE + │ ^^^^^^^^^^^^ Unknown type: NonExistentType + + error[E122]: Invalid type 'WSTRING' for enum. Only integer types are allowed + ┌─ :21:14 + │ + 21 │ TYPE InvalidEnum5 : WSTRING (a := 1, b := 2); END_TYPE + │ ^^^^^^^^^^^^ Invalid type 'WSTRING' for enum. Only integer types are allowed + + error[E122]: Invalid type 'MyStruct' for enum. Only integer types are allowed + ┌─ :24:14 + │ + 24 │ TYPE InvalidEnum6 : MyStruct (red := 1, blue := 2); END_TYPE + │ ^^^^^^^^^^^^ Invalid type 'MyStruct' for enum. Only integer types are allowed + + error[E122]: Invalid type 'MyArray' for enum. Only integer types are allowed + ┌─ :27:14 + │ + 27 │ TYPE InvalidEnum7 : MyArray (a := 1, b := 2); END_TYPE + │ ^^^^^^^^^^^^ Invalid type 'MyArray' for enum. Only integer types are allowed + + error[E122]: Invalid type 'MyStringAlias' for enum. Only integer types are allowed + ┌─ :30:14 + │ + 30 │ TYPE InvalidEnum8 : MyStringAlias (p := 1, q := 2); END_TYPE + │ ^^^^^^^^^^^^ Invalid type 'MyStringAlias' for enum. Only integer types are allowed + + error[E122]: Invalid type 'LREAL' for enum. Only integer types are allowed + ┌─ :33:14 + │ + 33 │ TYPE InvalidEnum9 : LREAL (low := 1, high := 2); END_TYPE + │ ^^^^^^^^^^^^ Invalid type 'LREAL' for enum. Only integer types are allowed + "); +} + +#[test] +fn enum_with_time_types_should_be_invalid() { + let diagnostics = parse_and_validate_buffered( + " + // Time types should not be allowed as enum base types + TYPE InvalidEnum1 : TIME (a := 1, b := 2); END_TYPE + TYPE InvalidEnum2 : DATE (x := 1, y := 2); END_TYPE + TYPE InvalidEnum3 : TOD (morning := 1, evening := 2); END_TYPE + TYPE InvalidEnum4 : DT (start := 1, end := 2); END_TYPE + TYPE InvalidEnum5 : DATE_AND_TIME (t1 := 1, t2 := 2); END_TYPE + TYPE InvalidEnum6 : TIME_OF_DAY (t1 := 1, t2 := 2); END_TYPE + TYPE InvalidEnum7 : LTIME (a := 1, b := 2); END_TYPE + + // Valid: Regular integer types should still work + TYPE ValidEnum1 : INT (a := 1, b := 2); END_TYPE + TYPE ValidEnum2 : LINT (a := 1, b := 2); END_TYPE + ", + ); + + assert_snapshot!(diagnostics, @r" + error[E122]: Invalid type 'TIME' for enum. Only integer types are allowed + ┌─ :3:14 + │ + 3 │ TYPE InvalidEnum1 : TIME (a := 1, b := 2); END_TYPE + │ ^^^^^^^^^^^^ Invalid type 'TIME' for enum. Only integer types are allowed + + error[E122]: Invalid type 'DATE' for enum. Only integer types are allowed + ┌─ :4:14 + │ + 4 │ TYPE InvalidEnum2 : DATE (x := 1, y := 2); END_TYPE + │ ^^^^^^^^^^^^ Invalid type 'DATE' for enum. Only integer types are allowed + + error[E122]: Invalid type 'TOD' for enum. Only integer types are allowed + ┌─ :5:14 + │ + 5 │ TYPE InvalidEnum3 : TOD (morning := 1, evening := 2); END_TYPE + │ ^^^^^^^^^^^^ Invalid type 'TOD' for enum. Only integer types are allowed + + error[E122]: Invalid type 'DT' for enum. Only integer types are allowed + ┌─ :6:14 + │ + 6 │ TYPE InvalidEnum4 : DT (start := 1, end := 2); END_TYPE + │ ^^^^^^^^^^^^ Invalid type 'DT' for enum. Only integer types are allowed + + error[E122]: Invalid type 'DATE_AND_TIME' for enum. Only integer types are allowed + ┌─ :7:14 + │ + 7 │ TYPE InvalidEnum5 : DATE_AND_TIME (t1 := 1, t2 := 2); END_TYPE + │ ^^^^^^^^^^^^ Invalid type 'DATE_AND_TIME' for enum. Only integer types are allowed + + error[E122]: Invalid type 'TIME_OF_DAY' for enum. Only integer types are allowed + ┌─ :8:14 + │ + 8 │ TYPE InvalidEnum6 : TIME_OF_DAY (t1 := 1, t2 := 2); END_TYPE + │ ^^^^^^^^^^^^ Invalid type 'TIME_OF_DAY' for enum. Only integer types are allowed + + error[E122]: Invalid type 'LTIME' for enum. Only integer types are allowed + ┌─ :9:14 + │ + 9 │ TYPE InvalidEnum7 : LTIME (a := 1, b := 2); END_TYPE + │ ^^^^^^^^^^^^ Invalid type 'LTIME' for enum. Only integer types are allowed + "); +} + +#[test] +fn enum_variants_initialized_with_other_enum_values() { + let diagnostics = parse_and_validate_buffered( + " + TYPE SubEnum : INT (a := 10, b := 20, c := 30); END_TYPE + + TYPE MainEnum : INT ( + x := SubEnum.a, + y := SubEnum.b, + z := SubEnum.c + ); END_TYPE + + VAR_GLOBAL + myMain : MainEnum; + mySub : SubEnum; + END_VAR + + PROGRAM main + myMain := MainEnum.x; // Should be ok + myMain := SubEnum.a; + mySub := MainEnum.x; + END_PROGRAM + ", + ); + + assert_snapshot!(diagnostics, @r" + note[E092]: Replace `SubEnum.a` with `x` + ┌─ :17:23 + │ + 4 │ TYPE MainEnum : INT ( + │ -------- see also + · + 17 │ myMain := SubEnum.a; + │ ^^^^^^^^^ Replace `SubEnum.a` with `x` + + note[E092]: Replace `MainEnum.x` with `a` + ┌─ :18:22 + │ + 2 │ TYPE SubEnum : INT (a := 10, b := 20, c := 30); END_TYPE + │ ------- see also + · + 18 │ mySub := MainEnum.x; + │ ^^^^^^^^^^ Replace `MainEnum.x` with `a` + "); +} + +#[test] +#[ignore = "currently fails during codegen"] +fn enum_type_assigned_without_qualifier() { + let diagnostics = parse_and_validate_buffered( + " + TYPE Color : INT (red := 1, green := 2, blue := 3); END_TYPE + + VAR_GLOBAL + myColor : Color; + END_VAR + + PROGRAM main + myColor := red; // Unqualified variant - ok + myColor := Color; // Type itself - should be unresolvable + END_PROGRAM + ", + ); + + assert!(diagnostics.len() > 0); + assert_snapshot!(diagnostics, @r#""#); +} + +#[test] +#[ignore = "currently fails during codegen"] +fn type_name_used_as_value() { + let diagnostics = parse_and_validate_buffered( + " + TYPE MyInt : INT; END_TYPE + TYPE Color : INT (red := 1, green := 2, blue := 3); END_TYPE + TYPE MyStruct : STRUCT x: INT; y: INT; END_STRUCT; END_TYPE + + PROGRAM main + VAR + a : INT; + b : MyInt; + c : Color; + d : MyStruct; + END_VAR + a := INT; // Type name as value - generic type + b := MyInt; // Type name as value - alias type + c := Color; // Type name as value - enum type + d := MyStruct; // Type name as value - struct type + END_PROGRAM + ", + ); + + assert!(diagnostics.len() > 0); + assert_snapshot!(diagnostics, @r#""#); +} diff --git a/src/validation/types.rs b/src/validation/types.rs index f5f0b3915ce..89f4693d4de 100644 --- a/src/validation/types.rs +++ b/src/validation/types.rs @@ -40,6 +40,26 @@ pub fn visit_data_type( ) { validate_data_type(validator, data_type, location); + // Validate enum numeric type + if let DataType::EnumType { numeric_type, .. } = data_type { + if let Some(resolved_type) = context.index.find_effective_type_by_name(numeric_type) { + let type_info = resolved_type.get_type_information(); + if !type_info.is_int() || type_info.is_date_or_time_type() { + validator.push_diagnostic( + Diagnostic::new(format!( + "Invalid type '{}' for enum. Only integer types are allowed", + numeric_type + )) + .with_error_code("E122") + .with_location(location), + ); + } + } + else { + validator.push_diagnostic(Diagnostic::unknown_type(numeric_type, location)); + } + } + let context = &context.with_optional_qualifier(data_type.get_name()); match data_type { DataType::StructType { variables, .. } => { diff --git a/tests/lit/single/enums/typed_enum_61131_syntax_with_default.st b/tests/lit/single/enums/typed_enum_61131_syntax_with_default.st new file mode 100644 index 00000000000..27e1ada8e35 --- /dev/null +++ b/tests/lit/single/enums/typed_enum_61131_syntax_with_default.st @@ -0,0 +1,36 @@ +// RUN: (%COMPILE %s && %RUN) | %CHECK %s + +// Test IEC 61131-3 syntax: TYPE NAME : TYPE (variants) := default; +TYPE Status : DWORD ( + inactive := 16#00000000, + active := 16#00000001, + error := 16#FFFFFFFF +) := inactive; +END_TYPE + +VAR_GLOBAL + s_global: Status; // Should initialize to inactive (explicit default) +END_VAR + +PROGRAM prog +VAR + s1 : Status; // Should initialize to inactive (explicit default) + s2 : Status := active; + s3 : Status := error; +END_VAR + // CHECK: 0 + printf('%d$N', s1); // inactive (default) + + // CHECK: 1 + printf('%d$N', s2); // active + + // CHECK: -1 + printf('%d$N', s3); // error (as signed) + + // CHECK: 0 + printf('%d$N', s_global); // inactive (default) +END_PROGRAM + +FUNCTION main + prog(); +END_FUNCTION diff --git a/tests/lit/single/enums/typed_enum_byte_values_are_correct.st b/tests/lit/single/enums/typed_enum_byte_values_are_correct.st new file mode 100644 index 00000000000..a9363978205 --- /dev/null +++ b/tests/lit/single/enums/typed_enum_byte_values_are_correct.st @@ -0,0 +1,32 @@ +// RUN: (%COMPILE %s && %RUN) | %CHECK %s + +// Test BYTE enum values work correctly +TYPE Status : BYTE (inactive := 0, active := 1, error := 255); +END_TYPE + +VAR_GLOBAL + status_global: Status := active; +END_VAR + +PROGRAM prog +VAR + s1 : Status := inactive; + s2 : Status := active; + s3 : Status := error; +END_VAR + // CHECK: 0 + printf('%d$N', s1); // inactive + + // CHECK: 1 + printf('%d$N', s2); // active + + // CHECK: 255 + printf('%d$N', s3); // error + + // CHECK: 1 + printf('%d$N', status_global); // active +END_PROGRAM + +FUNCTION main + prog(); +END_FUNCTION diff --git a/tests/lit/single/enums/typed_enum_codesys_syntax_with_default.st b/tests/lit/single/enums/typed_enum_codesys_syntax_with_default.st new file mode 100644 index 00000000000..bb2252efb58 --- /dev/null +++ b/tests/lit/single/enums/typed_enum_codesys_syntax_with_default.st @@ -0,0 +1,32 @@ +// RUN: (%COMPILE %s && %RUN) | %CHECK %s + +// Test Codesys syntax: TYPE NAME : (variants) TYPE := default; +TYPE Priority : ( + low := 10, + medium := 20, + high := 30 +) INT := medium; +END_TYPE + +VAR_GLOBAL + p_global: Priority; // Should initialize to medium (explicit default) +END_VAR + +PROGRAM prog +VAR + p1 : Priority; // Should initialize to medium (explicit default) + p2 : Priority := high; +END_VAR + // CHECK: 20 + printf('%d$N', p1); // medium (default) + + // CHECK: 30 + printf('%d$N', p2); // high + + // CHECK: 20 + printf('%d$N', p_global); // medium (default) +END_PROGRAM + +FUNCTION main + prog(); +END_FUNCTION diff --git a/tests/lit/single/enums/typed_enum_various_integer_types.st b/tests/lit/single/enums/typed_enum_various_integer_types.st new file mode 100644 index 00000000000..46743fe8ebf --- /dev/null +++ b/tests/lit/single/enums/typed_enum_various_integer_types.st @@ -0,0 +1,38 @@ +// RUN: (%COMPILE %s && %RUN) | %CHECK %s + +// Test various integer types work correctly +TYPE ByteEnum : BYTE (a := 1, b := 2); +END_TYPE + +TYPE IntEnum : INT (x := 100, y := 200); +END_TYPE + +TYPE DwordEnum : DWORD (big := 16#FFFF0000, small := 16#00000000); +END_TYPE + +TYPE LintEnum : LINT (huge := 9223372036854775807, semi_huge := 123421341234); // Max LINT +END_TYPE + +PROGRAM prog +VAR + be : ByteEnum := b; + ie : IntEnum := y; + de : DwordEnum := big; + le : LintEnum := huge; +END_VAR + // CHECK: 2 + printf('%d$N', be); // BYTE + + // CHECK: 200 + printf('%d$N', ie); // INT + + // CHECK: -65536 + printf('%d$N', de); // DWORD (as signed) + + // CHECK: 9223372036854775807 + printf('%lld$N', le); // LINT +END_PROGRAM + +FUNCTION main + prog(); +END_FUNCTION diff --git a/tests/lit/single/enums/typed_enum_with_zero_and_explicit_default.st b/tests/lit/single/enums/typed_enum_with_zero_and_explicit_default.st new file mode 100644 index 00000000000..8aa12d9f4f8 --- /dev/null +++ b/tests/lit/single/enums/typed_enum_with_zero_and_explicit_default.st @@ -0,0 +1,32 @@ +// RUN: (%COMPILE %s && %RUN) | %CHECK %s + +// Test enum with zero variant BUT explicit default - should use explicit default +TYPE Status : INT ( + inactive := 0, + active := 1, + error := 2 +) := active; +END_TYPE + +VAR_GLOBAL + s_global: Status; // Should initialize to active (explicit default) +END_VAR + +PROGRAM prog +VAR + s1 : Status; // Should initialize to active (explicit default), not inactive + s2 : Status := error; +END_VAR + // CHECK: 1 + printf('%d$N', s1); // active (explicit default overrides zero) + + // CHECK: 2 + printf('%d$N', s2); // error + + // CHECK: 1 + printf('%d$N', s_global); // active (explicit default) +END_PROGRAM + +FUNCTION main + prog(); +END_FUNCTION diff --git a/tests/lit/single/enums/typed_enum_with_zero_no_default.st b/tests/lit/single/enums/typed_enum_with_zero_no_default.st new file mode 100644 index 00000000000..076ca196228 --- /dev/null +++ b/tests/lit/single/enums/typed_enum_with_zero_no_default.st @@ -0,0 +1,32 @@ +// RUN: (%COMPILE %s && %RUN) | %CHECK %s + +// Test enum with zero variant and NO explicit default - should init to zero +TYPE Status : INT ( + inactive := 0, + active := 1, + error := 2 +); +END_TYPE + +VAR_GLOBAL + s_global: Status; // Should initialize to inactive (zero value) +END_VAR + +PROGRAM prog +VAR + s1 : Status; // Should initialize to inactive (zero value) + s2 : Status := active; +END_VAR + // CHECK: 0 + printf('%d$N', s1); // inactive (zero default) + + // CHECK: 1 + printf('%d$N', s2); // active + + // CHECK: 0 + printf('%d$N', s_global); // inactive (zero default) +END_PROGRAM + +FUNCTION main + prog(); +END_FUNCTION diff --git a/tests/lit/single/enums/typed_enum_without_zero_no_default.st b/tests/lit/single/enums/typed_enum_without_zero_no_default.st new file mode 100644 index 00000000000..a83bc8046c2 --- /dev/null +++ b/tests/lit/single/enums/typed_enum_without_zero_no_default.st @@ -0,0 +1,32 @@ +// RUN: (%COMPILE %s && %RUN) | %CHECK %s + +// Test enum without zero variant and NO explicit default - should init to first element +TYPE Priority : INT ( + low := 10, + medium := 20, + high := 30 +); +END_TYPE + +VAR_GLOBAL + p_global: Priority; // Should initialize to low (first element) +END_VAR + +PROGRAM prog +VAR + p1 : Priority; // Should initialize to low (first element) + p2 : Priority := high; +END_VAR + // CHECK: 10 + printf('%d$N', p1); // low (first element default) + + // CHECK: 30 + printf('%d$N', p2); // high + + // CHECK: 10 + printf('%d$N', p_global); // low (first element default) +END_PROGRAM + +FUNCTION main + prog(); +END_FUNCTION diff --git a/tests/lit/single/enums/typed_enum_without_zero_with_default.st b/tests/lit/single/enums/typed_enum_without_zero_with_default.st new file mode 100644 index 00000000000..f0fb2c90d44 --- /dev/null +++ b/tests/lit/single/enums/typed_enum_without_zero_with_default.st @@ -0,0 +1,36 @@ +// RUN: (%COMPILE %s && %RUN) | %CHECK %s + +// Test enum without zero variant but WITH explicit default - should init to explicit default +TYPE PRIORITY : INT ( + low := 10, + medium := 20, + high := 30 +) := medium; +END_TYPE + +VAR_GLOBAL + p_global: PRIORITY; +END_VAR + +PROGRAM prog +VAR + p1 : PRIORITY; // Should initialize to medium (explicit default), not low + p2 : PRIORITY := high; +END_VAR + // CHECK: 20 + printf('%d$N', p1); // Should be medium (explicit default) + + // CHECK: 30 + printf('%d$N', p2); // high + + p1 := PRIORITY.low; + // CHECK: 10 + printf('%d$N', p1); // low + + // CHECK: 20 + printf('%d$N', p_global); // medium +END_PROGRAM + +FUNCTION main + prog(); +END_FUNCTION From 2460c83dab41410a3ed5d257c4cfb1677c815ba4 Mon Sep 17 00:00:00 2001 From: Michael Haselberger Date: Tue, 11 Nov 2025 09:22:40 +0100 Subject: [PATCH 2/5] fmt --- src/codegen/generators/data_type_generator.rs | 4 ++-- src/parser.rs | 22 ++++++------------- src/resolver.rs | 11 +++++++--- src/validation/tests/enum_validation_test.rs | 2 +- src/validation/types.rs | 3 +-- 5 files changed, 19 insertions(+), 23 deletions(-) diff --git a/src/codegen/generators/data_type_generator.rs b/src/codegen/generators/data_type_generator.rs index a51ef38fbe3..44fc56923ab 100644 --- a/src/codegen/generators/data_type_generator.rs +++ b/src/codegen/generators/data_type_generator.rs @@ -366,8 +366,8 @@ impl<'ink> DataTypeGenerator<'ink, '_> { Some(v) => Ok((it.get_qualified_name(), v)), None => self .types_index - .get_associated_type(it.get_type_name()) - .map(get_default_for) + .get_associated_type(it.get_type_name()) + .map(get_default_for) .map(|v| (it.get_qualified_name(), v)), }) }) diff --git a/src/parser.rs b/src/parser.rs index e50d315e519..f0129471449 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1105,7 +1105,9 @@ fn parse_string_size_expression(lexer: &mut ParseSession) -> Option { .with_location(error_range) .with_error_code("E009"), ); - } else if !is_enum_like && (opening_token == KeywordParensOpen || lexer.token == KeywordParensClose) { + } else if !is_enum_like + && (opening_token == KeywordParensOpen || lexer.token == KeywordParensClose) + { lexer.accept_diagnostic(Diagnostic::new( "Unusual type of parentheses around string size expression, consider using square parentheses '[]'"). with_location(error_range) @@ -1135,21 +1137,14 @@ fn parse_string_type_definition( // Check if this is actually an enum type (e.g., STRING (a := 1, b := 2)) // If size is an ExpressionList with assignments, it's likely an invalid enum definition - let is_enum_like = matches!( - &size, - Some(AstNode { stmt: AstStatement::ExpressionList(_), .. }) - ); + let is_enum_like = matches!(&size, Some(AstNode { stmt: AstStatement::ExpressionList(_), .. })); match (size, &name, is_enum_like) { (Some(size), _, true) => { // This looks like an enum definition with STRING/WSTRING as the type // Create an EnumType so validation can catch it as invalid Some(DataTypeDeclaration::Definition { - data_type: Box::new(DataType::EnumType { - name, - numeric_type: text, - elements: size, - }), + data_type: Box::new(DataType::EnumType { name, numeric_type: text, elements: size }), location, scope: lexer.scope.clone(), }) @@ -1189,11 +1184,8 @@ fn parse_enum_type_definition( // Check for Codesys-style type specification after the enum list // TYPE COLOR : (...) DWORD; - let numeric_type = if lexer.token == Identifier { - lexer.slice_and_advance() - } else { - DINT_TYPE.to_string() - }; + let numeric_type = + if lexer.token == Identifier { lexer.slice_and_advance() } else { DINT_TYPE.to_string() }; let initializer = lexer.try_consume(KeywordAssignment).then(|| parse_expression(lexer)); Some(( diff --git a/src/resolver.rs b/src/resolver.rs index 73549a0c9a2..3d9bc62be65 100644 --- a/src/resolver.rs +++ b/src/resolver.rs @@ -389,9 +389,14 @@ impl TypeAnnotator<'_> { } // Enum types need to be promoted based on their underlying integer type DataTypeInformation::Enum { referenced_type, .. } => { - let enum_base_type = self.index.get_effective_type_or_void_by_name(referenced_type); - if let DataTypeInformation::Integer { .. } = enum_base_type.get_type_information() { - if !enum_base_type.information.is_bool() && !enum_base_type.information.is_character() { + let enum_base_type = + self.index.get_effective_type_or_void_by_name(referenced_type); + if let DataTypeInformation::Integer { .. } = + enum_base_type.get_type_information() + { + if !enum_base_type.information.is_bool() + && !enum_base_type.information.is_character() + { get_bigger_type( enum_base_type, self.index.get_type_or_panic(DINT_TYPE), diff --git a/src/validation/tests/enum_validation_test.rs b/src/validation/tests/enum_validation_test.rs index 133cf242f4a..db379575c52 100644 --- a/src/validation/tests/enum_validation_test.rs +++ b/src/validation/tests/enum_validation_test.rs @@ -416,7 +416,7 @@ fn type_name_used_as_value() { END_PROGRAM ", ); - + assert!(diagnostics.len() > 0); assert_snapshot!(diagnostics, @r#""#); } diff --git a/src/validation/types.rs b/src/validation/types.rs index 89f4693d4de..af38b9f4506 100644 --- a/src/validation/types.rs +++ b/src/validation/types.rs @@ -54,8 +54,7 @@ pub fn visit_data_type( .with_location(location), ); } - } - else { + } else { validator.push_diagnostic(Diagnostic::unknown_type(numeric_type, location)); } } From b676e54e77a6edecec524d088a2769e9806c0154 Mon Sep 17 00:00:00 2001 From: Michael Haselberger Date: Tue, 11 Nov 2025 10:11:12 +0100 Subject: [PATCH 3/5] move evaluation to its own pass after constant resolution --- compiler/plc_driver/src/pipelines.rs | 6 +- src/index.rs | 53 +++++++ src/index/indexer/user_type_indexer.rs | 25 +--- src/parser.rs | 1 - src/parser/tests/type_parser_tests.rs | 132 ------------------ src/test_utils.rs | 3 + ...itializer_works_with_constant_variables.st | 32 +++++ 7 files changed, 94 insertions(+), 158 deletions(-) create mode 100644 tests/lit/single/enums/enum_ensure_zero_variant_initializer_works_with_constant_variables.st diff --git a/compiler/plc_driver/src/pipelines.rs b/compiler/plc_driver/src/pipelines.rs index 7bc1b4b2096..1cb8017b8db 100644 --- a/compiler/plc_driver/src/pipelines.rs +++ b/compiler/plc_driver/src/pipelines.rs @@ -561,7 +561,11 @@ impl ParsedProject { global_index.import(indexer::index(&builtins)); //TODO: evaluate constants should probably be a participant - let (index, unresolvables) = plc::resolver::const_evaluator::evaluate_constants(global_index); + let (mut index, unresolvables) = plc::resolver::const_evaluator::evaluate_constants(global_index); + + // Fix up enum defaults after constants are resolved + index.finalize_enum_defaults(); + IndexedProject { project: ParsedProject { units }, index, unresolvables } } } diff --git a/src/index.rs b/src/index.rs index e9a5b41fe97..b3b76c8b5ca 100644 --- a/src/index.rs +++ b/src/index.rs @@ -2025,6 +2025,59 @@ impl Index { self.type_index.pou_types.insert(datatype.get_name().to_lowercase(), datatype); } + /// Fixes up enum types to set their default initial values. + /// This must be called after constant resolution, as it needs to evaluate + /// constant expressions to determine which variant is zero. + /// + /// For each enum without an explicit initializer, this sets the initial_value to: + /// 1. The zero-variant (if one exists), or + /// 2. The first variant (as fallback) + pub fn finalize_enum_defaults(&mut self) { + // Process all types and update enum defaults + let mut fixed_types = Vec::new(); + + for (name, mut datatypes) in self.type_index.types.drain(..) { + for mut datatype in datatypes.drain(..) { + if let DataTypeInformation::Enum { variants, .. } = &datatype.information { + // Only process if there's no explicit initializer + if datatype.initial_value.is_none() && !variants.is_empty() { + let mut zero_variant_id: Option = None; + let mut first_variant_id: Option = None; + + // Look for a variant that evaluates to zero, or use the first one + for (idx, variant) in variants.iter().enumerate() { + if let Some(variant_init) = variant.initial_value { + if idx == 0 { + first_variant_id = Some(variant_init); + } + + if let Ok(0) = + self.constant_expressions.get_constant_int_statement_value(&variant_init) + { + zero_variant_id = Some(variant_init); + break; + } + } + } + + // Prefer zero variant, fall back to first variant + let default_value = zero_variant_id.or(first_variant_id); + if let Some(const_id) = default_value { + datatype.initial_value = Some(const_id); + } + } + } + + fixed_types.push((name.clone(), datatype)); + } + } + + // Re-insert all types + for (name, datatype) in fixed_types { + self.type_index.types.insert(name, datatype); + } + } + pub fn find_callable_instance_variable( &self, context: Option<&str>, diff --git a/src/index/indexer/user_type_indexer.rs b/src/index/indexer/user_type_indexer.rs index 3a73834f1e5..7ebd0139fa0 100644 --- a/src/index/indexer/user_type_indexer.rs +++ b/src/index/indexer/user_type_indexer.rs @@ -292,10 +292,8 @@ impl UserTypeIndexer<'_, '_> { fn index_enum_type(&mut self, name: &str, numeric_type: &str, elements: &AstNode) { let mut variants = Vec::new(); - let mut zero_value_const_id: Option = None; - let mut first_element_const_id: Option = None; - for (idx, ele) in flatten_expression_list(elements).iter().enumerate() { + for ele in flatten_expression_list(elements) { let variant = get_enum_element_name(ele); if let AstStatement::Assignment(Assignment { right, .. }) = ele.get_stmt() { let scope = self.current_scope(); @@ -306,16 +304,6 @@ impl UserTypeIndexer<'_, '_> { None, ); - // Track if we have a zero element and remember its ConstId - if let AstStatement::Literal(AstLiteral::Integer(0)) = right.get_stmt() { - zero_value_const_id = Some(init); - } - - // Remember the first element's ConstId - if idx == 0 { - first_element_const_id = Some(init); - } - variants.push(self.index.register_enum_variant( name, &variant, @@ -327,17 +315,6 @@ impl UserTypeIndexer<'_, '_> { } } - // If no explicit initializer was provided, determine the default value - if self.pending_initializer.is_none() { - self.pending_initializer = if zero_value_const_id.is_some() { - // If zero is defined, use its ConstId - zero_value_const_id - } else { - // Otherwise use the first element's ConstId - first_element_const_id - }; - } - let information = DataTypeInformation::Enum { name: name.to_owned(), variants, diff --git a/src/parser.rs b/src/parser.rs index f0129471449..c2f1e5b2dfc 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1136,7 +1136,6 @@ fn parse_string_type_definition( let location = lexer.source_range_factory.create_range(start..end); // Check if this is actually an enum type (e.g., STRING (a := 1, b := 2)) - // If size is an ExpressionList with assignments, it's likely an invalid enum definition let is_enum_like = matches!(&size, Some(AstNode { stmt: AstStatement::ExpressionList(_), .. })); match (size, &name, is_enum_like) { diff --git a/src/parser/tests/type_parser_tests.rs b/src/parser/tests/type_parser_tests.rs index 26e230ec7ee..f0f881ab734 100644 --- a/src/parser/tests/type_parser_tests.rs +++ b/src/parser/tests/type_parser_tests.rs @@ -785,138 +785,6 @@ fn enum_codesys_style_with_int_type_and_default() { assert_eq!(diagnostics.len(), 0); } -#[test] -fn enum_with_zero_element_no_default() { - let (result, diagnostics) = parse( - r#" - TYPE STATE_WITH_ZERO : BYTE (idle := 0, running := 1, stopped := 2); - END_TYPE - "#, - ); - assert_debug_snapshot!(result.user_types[0], @r#" - UserTypeDeclaration { - data_type: EnumType { - name: Some( - "STATE_WITH_ZERO", - ), - numeric_type: "BYTE", - elements: ExpressionList { - expressions: [ - Assignment { - left: ReferenceExpr { - kind: Member( - Identifier { - name: "idle", - }, - ), - base: None, - }, - right: LiteralInteger { - value: 0, - }, - }, - Assignment { - left: ReferenceExpr { - kind: Member( - Identifier { - name: "running", - }, - ), - base: None, - }, - right: LiteralInteger { - value: 1, - }, - }, - Assignment { - left: ReferenceExpr { - kind: Member( - Identifier { - name: "stopped", - }, - ), - base: None, - }, - right: LiteralInteger { - value: 2, - }, - }, - ], - }, - }, - initializer: None, - scope: None, - } - "#); - assert_eq!(diagnostics.len(), 0); -} - -#[test] -fn enum_without_zero_no_default() { - let (result, diagnostics) = parse( - r#" - TYPE PRIORITY : INT (low := 10, medium := 20, high := 30); - END_TYPE - "#, - ); - assert_debug_snapshot!(result.user_types[0], @r#" - UserTypeDeclaration { - data_type: EnumType { - name: Some( - "PRIORITY", - ), - numeric_type: "INT", - elements: ExpressionList { - expressions: [ - Assignment { - left: ReferenceExpr { - kind: Member( - Identifier { - name: "low", - }, - ), - base: None, - }, - right: LiteralInteger { - value: 10, - }, - }, - Assignment { - left: ReferenceExpr { - kind: Member( - Identifier { - name: "medium", - }, - ), - base: None, - }, - right: LiteralInteger { - value: 20, - }, - }, - Assignment { - left: ReferenceExpr { - kind: Member( - Identifier { - name: "high", - }, - ), - base: None, - }, - right: LiteralInteger { - value: 30, - }, - }, - ], - }, - }, - initializer: None, - scope: None, - } - "#); - assert_eq!(diagnostics.len(), 0); -} - #[test] fn enum_with_no_elements_produces_syntax_error() { // Empty enums are syntactically invalid - parser requires at least one element diff --git a/src/test_utils.rs b/src/test_utils.rs index 58daa2a7b09..75ee3622156 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -141,6 +141,8 @@ pub mod tests { id_provider: IdProvider, ) -> Lowered { let (mut index, _) = evaluate_constants(index); + index.finalize_enum_defaults(); + let mut all_annotations = AnnotationMapImpl::default(); let (mut annotations, ..) = TypeAnnotator::visit_unit(&index, &unit, id_provider.clone()); @@ -302,6 +304,7 @@ pub mod tests { }, ); let (mut index, ..) = evaluate_constants(index); + index.finalize_enum_defaults(); let mut all_annotations = AnnotationMapImpl::default(); let units = units .into_iter() diff --git a/tests/lit/single/enums/enum_ensure_zero_variant_initializer_works_with_constant_variables.st b/tests/lit/single/enums/enum_ensure_zero_variant_initializer_works_with_constant_variables.st new file mode 100644 index 00000000000..c608eed3aed --- /dev/null +++ b/tests/lit/single/enums/enum_ensure_zero_variant_initializer_works_with_constant_variables.st @@ -0,0 +1,32 @@ +// RUN: (%COMPILE %s && %RUN) | %CHECK %s + +VAR_GLOBAL CONSTANT + MYCONST: INT := 0; +END_VAR + +// Test enum with no explicit default and a zero variant that is NOT also the first variant - BUT instead of a literal use a constant to initialize +TYPE Status : INT ( + active := 1, + inactive := MYCONST, + error := 2 +); +END_TYPE + +VAR_GLOBAL + s_global: Status; // Should initialize to inactive +END_VAR + +PROGRAM prog +VAR + s1 : Status; // Should initialize to inactive +END_VAR + // CHECK: 0 + printf('%d$N', s1); // inactive + + // CHECK: 0 + printf('%d$N', s_global); // inactive +END_PROGRAM + +FUNCTION main + prog(); +END_FUNCTION From c27527a08176dc7bbdce9422fa868c07549be8c1 Mon Sep 17 00:00:00 2001 From: Michael Haselberger Date: Tue, 11 Nov 2025 10:35:23 +0100 Subject: [PATCH 4/5] add issue reference to ignored tests --- src/validation/tests/enum_validation_test.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/validation/tests/enum_validation_test.rs b/src/validation/tests/enum_validation_test.rs index db379575c52..c710706dbc3 100644 --- a/src/validation/tests/enum_validation_test.rs +++ b/src/validation/tests/enum_validation_test.rs @@ -372,7 +372,7 @@ fn enum_variants_initialized_with_other_enum_values() { } #[test] -#[ignore = "currently fails during codegen"] +#[ignore = "currently fails during codegen, tracked in #1546"] fn enum_type_assigned_without_qualifier() { let diagnostics = parse_and_validate_buffered( " @@ -394,7 +394,7 @@ fn enum_type_assigned_without_qualifier() { } #[test] -#[ignore = "currently fails during codegen"] +#[ignore = "currently fails during codegen, tracked in #1546"] fn type_name_used_as_value() { let diagnostics = parse_and_validate_buffered( " From d34959b7b6637f0420ab549a9ac8bc052c393628 Mon Sep 17 00:00:00 2001 From: Michael Haselberger Date: Tue, 11 Nov 2025 14:08:23 +0100 Subject: [PATCH 5/5] remove some nesting --- src/resolver.rs | 39 +++++++++++++++++---------------------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/src/resolver.rs b/src/resolver.rs index 3d9bc62be65..6607da60867 100644 --- a/src/resolver.rs +++ b/src/resolver.rs @@ -388,28 +388,23 @@ impl TypeAnnotator<'_> { .get_name() } // Enum types need to be promoted based on their underlying integer type - DataTypeInformation::Enum { referenced_type, .. } => { - let enum_base_type = - self.index.get_effective_type_or_void_by_name(referenced_type); - if let DataTypeInformation::Integer { .. } = - enum_base_type.get_type_information() - { - if !enum_base_type.information.is_bool() - && !enum_base_type.information.is_character() - { - get_bigger_type( - enum_base_type, - self.index.get_type_or_panic(DINT_TYPE), - self.index, - ) - .get_name() - } else { - type_name - } - } else { - type_name - } - } + DataTypeInformation::Enum { referenced_type, .. } => self + .index + .get_effective_type_by_name(referenced_type) + .ok() + .filter(|dt| { + let info = dt.get_type_information(); + info.is_int() && !(info.is_bool() || info.is_character()) + }) + .map(|enum_base_type| { + get_bigger_type( + enum_base_type, + self.index.get_type_or_panic(DINT_TYPE), + self.index, + ) + .get_name() + }) + .unwrap_or(type_name), _ => type_name, } } else {