Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
}

Expand Down
61 changes: 61 additions & 0 deletions compiler/plc_diagnostics/src/diagnostics/error_codes/E122.md
Original file line number Diff line number Diff line change
@@ -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
```
6 changes: 5 additions & 1 deletion compiler/plc_driver/src/pipelines.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Design Question: Should finalize_enum_defaults() be integrated into evaluate_constants()?

Currently, finalize_enum_defaults() is called separately after evaluate_constants() in three places:

  • Main pipeline (pipelines.rs)
  • Test utilities (test_utils.rs, 2 locations)

Arguments for keeping them separate (current approach):

  • Clear separation of concerns: Constant evaluation and enum initialization are conceptually distinct operations
  • Modularity: Each function has a single, well-defined responsibility
  • Explicit pipeline: Makes it obvious what post-resolution steps are needed

Arguments for integrating into evaluate_constants():

  • Strong dependency: finalize_enum_defaults() fundamentally requires resolved constants and cannot work without them
  • Simpler API: Single function call instead of remembering to call both in sequence
  • Maintenance: Reduces chance of forgetting this pass in new code paths
  • Logical cohesion: Both operations finalize constant-related initialization

Should we consider renaming to something like resolve_constants_and_initializers() or updating the evaluate_constants() doc comment to clarify it also handles dependent initialization steps and add this pass to evaluate_constants()?


IndexedProject { project: ParsedProject { units }, index, unresolvables }
}
}
Expand Down
3 changes: 3 additions & 0 deletions src/codegen/generators/data_type_generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ source_filename = "<internal>"
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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:
Expand All @@ -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*)
Expand All @@ -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

Expand Down
57 changes: 57 additions & 0 deletions src/codegen/tests/typesystem_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '<internal>'
source_filename = "<internal>"
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(
Expand Down
53 changes: 53 additions & 0 deletions src/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ConstId> = None;
let mut first_variant_id: Option<ConstId> = 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>,
Expand Down
34 changes: 28 additions & 6 deletions src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -1095,6 +1094,9 @@ fn parse_string_size_expression(lexer: &mut ParseSession) -> Option<AstNode> {
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)
{
Expand All @@ -1103,7 +1105,9 @@ fn parse_string_size_expression(lexer: &mut ParseSession) -> Option<AstNode> {
.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)
Expand Down Expand Up @@ -1131,13 +1135,25 @@ 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))
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,
Expand All @@ -1164,10 +1180,16 @@ 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(),
},
Expand Down
Loading