Skip to content

Commit 8d97223

Browse files
committed
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).
1 parent 964c0c7 commit 8d97223

23 files changed

+1648
-19
lines changed

compiler/plc_diagnostics/src/diagnostics/diagnostics_registry.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ lazy_static! {
223223
E119, Error, include_str!("./error_codes/E119.md"), // Invalid use of `SUPER` keyword
224224
E120, Error, include_str!("./error_codes/E120.md"), // Invalid use of `THIS` keyword
225225
E121, Error, include_str!("./error_codes/E121.md"), // Recursive type alias
226+
E122, Error, include_str!("./error_codes/E122.md"), // Invalid enum base type
226227
);
227228
}
228229

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# E122: Invalid enum base type
2+
3+
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.
4+
5+
## Example
6+
7+
```st
8+
TYPE Color : STRING (red := 1, green := 2, blue := 3);
9+
END_TYPE
10+
```
11+
12+
In this example, `STRING` is not a valid base type for an enum. Only integer types are allowed.
13+
14+
## Another example
15+
16+
```st
17+
TYPE Status : REAL (active := 1, inactive := 0);
18+
END_TYPE
19+
20+
TYPE Timestamp : TIME (start := 0, stop := 1);
21+
END_TYPE
22+
```
23+
24+
These examples show other invalid base types:
25+
- `REAL` is a floating-point type, not an integer type
26+
- `TIME` is a time/date type, which although internally represented as an integer, should not be used as an enum base type
27+
28+
## Valid integer types
29+
30+
The following integer types are valid for enum base types:
31+
- `INT`, `UINT` - 16-bit integers
32+
- `SINT`, `USINT` - 8-bit integers
33+
- `DINT`, `UDINT` - 32-bit integers
34+
- `LINT`, `ULINT` - 64-bit integers
35+
- `BYTE` - 8-bit unsigned
36+
- `WORD` - 16-bit unsigned
37+
- `DWORD` - 32-bit unsigned
38+
- `LWORD` - 64-bit unsigned
39+
40+
## How to fix
41+
42+
**Use a valid integer type**
43+
44+
Change the base type to one of the supported integer types:
45+
46+
```st
47+
TYPE Color : INT (red := 1, green := 2, blue := 3);
48+
END_TYPE
49+
50+
TYPE Status : BYTE (active := 1, inactive := 0);
51+
END_TYPE
52+
```
53+
54+
**Or omit the type specification**
55+
56+
If no specific size is required, you can omit the type specification (will default to `DINT`):
57+
58+
```st
59+
TYPE Color (red := 1, green := 2, blue := 3);
60+
END_TYPE
61+
```

src/codegen/generators/data_type_generator.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -366,8 +366,8 @@ impl<'ink> DataTypeGenerator<'ink, '_> {
366366
Some(v) => Ok((it.get_qualified_name(), v)),
367367
None => self
368368
.types_index
369-
.get_associated_type(it.get_type_name())
370-
.map(get_default_for)
369+
.get_associated_type(it.get_type_name())
370+
.map(get_default_for)
371371
.map(|v| (it.get_qualified_name(), v)),
372372
})
373373
})
@@ -406,6 +406,9 @@ impl<'ink> DataTypeGenerator<'ink, '_> {
406406
DataTypeInformation::Alias { referenced_type, .. } => {
407407
self.generate_initial_value_for_type(data_type, referenced_type)
408408
}
409+
DataTypeInformation::Enum { referenced_type, .. } => {
410+
self.generate_initial_value_for_type(data_type, referenced_type)
411+
}
409412
//all other types (scalars, pointer and void)
410413
_ => Ok(None),
411414
}

src/codegen/tests/snapshots/rusty__codegen__tests__code_gen_tests__typed_enums_with_initializers_are_generated.snap

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ source_filename = "<internal>"
88
target datalayout = "[filtered]"
99
target triple = "[filtered]"
1010

11-
@x = global i8 0
12-
@y = global i16 0
13-
@z = global i32 0
11+
@x = global i8 1
12+
@y = global i16 10
13+
@z = global i32 22
1414
@MyEnum.red = unnamed_addr constant i8 1
1515
@MyEnum.yellow = unnamed_addr constant i8 2
1616
@MyEnum.green = unnamed_addr constant i8 3

src/codegen/tests/snapshots/rusty__codegen__tests__code_gen_tests__typed_enums_with_partly_initializers_are_generated.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ target datalayout = "[filtered]"
99
target triple = "[filtered]"
1010

1111
@twenty = unnamed_addr constant i16 20
12-
@x = global i8 0
12+
@x = global i8 7
1313
@MyEnum.red = unnamed_addr constant i8 7
1414
@MyEnum.yellow = unnamed_addr constant i8 8
1515
@MyEnum.green = unnamed_addr constant i8 9

src/codegen/tests/snapshots/rusty__codegen__tests__multifile_codegen_tests__enum_referenced_in_fb_nested.snap

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
---
22
source: src/codegen/tests/multifile_codegen_tests.rs
33
expression: "codegen_multi(units, crate::DebugLevel::None).join(\"\\n\")"
4+
snapshot_kind: text
45
---
56
; ModuleID = 'myEnum.st'
67
source_filename = "myEnum.st"
@@ -18,7 +19,7 @@ target triple = "[filtered]"
1819

1920
%fb = type { i32 }
2021

21-
@__fb__init = unnamed_addr constant %fb zeroinitializer
22+
@__fb__init = unnamed_addr constant %fb { i32 1 }
2223

2324
define void @fb(%fb* %0) {
2425
entry:
@@ -36,7 +37,7 @@ target triple = "[filtered]"
3637
%myStruct = type { %fb.2 }
3738
%fb.2 = type { i32 }
3839

39-
@__myStruct__init = unnamed_addr constant %myStruct zeroinitializer
40+
@__myStruct__init = unnamed_addr constant %myStruct { %fb.2 { i32 1 } }
4041
@__fb__init = external unnamed_addr constant %fb.2
4142

4243
declare void @fb(%fb.2*)
@@ -50,7 +51,7 @@ target triple = "[filtered]"
5051
%myStruct.4 = type { %fb.5 }
5152
%fb.5 = type { i32 }
5253

53-
@__fb2__init = unnamed_addr constant %fb2 zeroinitializer
54+
@__fb2__init = unnamed_addr constant %fb2 { %myStruct.4 { %fb.5 { i32 1 } } }
5455
@__myStruct__init = external unnamed_addr constant %myStruct.4
5556
@__fb__init = external unnamed_addr constant %fb.5
5657

src/codegen/tests/typesystem_test.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,63 @@ fn small_int_varargs_get_promoted_while_32bit_and_higher_keep_their_type() {
312312
filtered_assert_snapshot!(result);
313313
}
314314

315+
#[test]
316+
fn enum_typed_varargs_get_promoted() {
317+
let src = r#"
318+
{external}
319+
FUNCTION printf : DINT
320+
VAR_IN_OUT
321+
format: STRING;
322+
END_VAR
323+
VAR_INPUT
324+
args: ...;
325+
END_VAR
326+
END_FUNCTION
327+
328+
TYPE MyEnum : INT (a := 10, b := 20);
329+
END_TYPE
330+
331+
FUNCTION main : DINT
332+
VAR
333+
e1 : MyEnum := a;
334+
i1 : INT := 10;
335+
END_VAR
336+
printf('result : %d %d$N', e1, i1);
337+
END_FUNCTION
338+
"#;
339+
340+
let result = codegen(src);
341+
filtered_assert_snapshot!(result, @r#"
342+
; ModuleID = '<internal>'
343+
source_filename = "<internal>"
344+
target datalayout = "[filtered]"
345+
target triple = "[filtered]"
346+
347+
@MyEnum.a = unnamed_addr constant i16 10
348+
@MyEnum.b = unnamed_addr constant i16 20
349+
@utf08_literal_0 = private unnamed_addr constant [16 x i8] c"result : %d %d\0A\00"
350+
351+
declare i32 @printf(i8*, ...)
352+
353+
define i32 @main() {
354+
entry:
355+
%main = alloca i32, align 4
356+
%e1 = alloca i16, align 2
357+
%i1 = alloca i16, align 2
358+
store i16 10, i16* %e1, align 2
359+
store i16 10, i16* %i1, align 2
360+
store i32 0, i32* %main, align 4
361+
%load_e1 = load i16, i16* %e1, align 2
362+
%0 = sext i16 %load_e1 to i32
363+
%load_i1 = load i16, i16* %i1, align 2
364+
%1 = sext i16 %load_i1 to i32
365+
%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)
366+
%main_ret = load i32, i32* %main, align 4
367+
ret i32 %main_ret
368+
}
369+
"#);
370+
}
371+
315372
#[test]
316373
fn self_referential_struct_via_reference_codegen() {
317374
let result = codegen(

src/index/indexer/user_type_indexer.rs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,8 +292,10 @@ impl UserTypeIndexer<'_, '_> {
292292

293293
fn index_enum_type(&mut self, name: &str, numeric_type: &str, elements: &AstNode) {
294294
let mut variants = Vec::new();
295+
let mut zero_value_const_id: Option<ConstId> = None;
296+
let mut first_element_const_id: Option<ConstId> = None;
295297

296-
for ele in flatten_expression_list(elements) {
298+
for (idx, ele) in flatten_expression_list(elements).iter().enumerate() {
297299
let variant = get_enum_element_name(ele);
298300
if let AstStatement::Assignment(Assignment { right, .. }) = ele.get_stmt() {
299301
let scope = self.current_scope();
@@ -304,6 +306,16 @@ impl UserTypeIndexer<'_, '_> {
304306
None,
305307
);
306308

309+
// Track if we have a zero element and remember its ConstId
310+
if let AstStatement::Literal(AstLiteral::Integer(0)) = right.get_stmt() {
311+
zero_value_const_id = Some(init);
312+
}
313+
314+
// Remember the first element's ConstId
315+
if idx == 0 {
316+
first_element_const_id = Some(init);
317+
}
318+
307319
variants.push(self.index.register_enum_variant(
308320
name,
309321
&variant,
@@ -315,6 +327,17 @@ impl UserTypeIndexer<'_, '_> {
315327
}
316328
}
317329

330+
// If no explicit initializer was provided, determine the default value
331+
if self.pending_initializer.is_none() {
332+
self.pending_initializer = if zero_value_const_id.is_some() {
333+
// If zero is defined, use its ConstId
334+
zero_value_const_id
335+
} else {
336+
// Otherwise use the first element's ConstId
337+
first_element_const_id
338+
};
339+
}
340+
318341
let information = DataTypeInformation::Enum {
319342
name: name.to_owned(),
320343
variants,

src/parser.rs

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -963,7 +963,6 @@ fn parse_data_type_definition(
963963
} else if lexer.try_consume(KeywordRef) {
964964
parse_pointer_definition(lexer, name, lexer.last_range.start, None, true, false)
965965
} else if lexer.try_consume(KeywordParensOpen) {
966-
//enum without datatype
967966
parse_enum_type_definition(lexer, name)
968967
} else if lexer.token == KeywordString || lexer.token == KeywordWideString {
969968
parse_string_type_definition(lexer, name)
@@ -1095,6 +1094,9 @@ fn parse_string_size_expression(lexer: &mut ParseSession) -> Option<AstNode> {
10951094
let size_expr = parse_expression(lexer);
10961095
let error_range = lexer.source_range_factory.create_range(opening_location..lexer.range().end);
10971096

1097+
// Don't emit warnings if this looks like an enum (will be caught by validation)
1098+
let is_enum_like = matches!(size_expr.get_stmt(), AstStatement::ExpressionList(_));
1099+
10981100
if (opening_token == KeywordParensOpen && lexer.token == KeywordSquareParensClose)
10991101
|| (opening_token == KeywordSquareParensOpen && lexer.token == KeywordParensClose)
11001102
{
@@ -1103,7 +1105,7 @@ fn parse_string_size_expression(lexer: &mut ParseSession) -> Option<AstNode> {
11031105
.with_location(error_range)
11041106
.with_error_code("E009"),
11051107
);
1106-
} else if opening_token == KeywordParensOpen || lexer.token == KeywordParensClose {
1108+
} else if !is_enum_like && (opening_token == KeywordParensOpen || lexer.token == KeywordParensClose) {
11071109
lexer.accept_diagnostic(Diagnostic::new(
11081110
"Unusual type of parentheses around string size expression, consider using square parentheses '[]'").
11091111
with_location(error_range)
@@ -1131,13 +1133,33 @@ fn parse_string_type_definition(
11311133
let end = lexer.last_range.end;
11321134
let location = lexer.source_range_factory.create_range(start..end);
11331135

1134-
match (size, &name) {
1135-
(Some(size), _) => Some(DataTypeDeclaration::Definition {
1136+
// Check if this is actually an enum type (e.g., STRING (a := 1, b := 2))
1137+
// If size is an ExpressionList with assignments, it's likely an invalid enum definition
1138+
let is_enum_like = matches!(
1139+
&size,
1140+
Some(AstNode { stmt: AstStatement::ExpressionList(_), .. })
1141+
);
1142+
1143+
match (size, &name, is_enum_like) {
1144+
(Some(size), _, true) => {
1145+
// This looks like an enum definition with STRING/WSTRING as the type
1146+
// Create an EnumType so validation can catch it as invalid
1147+
Some(DataTypeDeclaration::Definition {
1148+
data_type: Box::new(DataType::EnumType {
1149+
name,
1150+
numeric_type: text,
1151+
elements: size,
1152+
}),
1153+
location,
1154+
scope: lexer.scope.clone(),
1155+
})
1156+
}
1157+
(Some(size), _, false) => Some(DataTypeDeclaration::Definition {
11361158
data_type: Box::new(DataType::StringType { name, is_wide, size: Some(size) }),
11371159
location,
11381160
scope: lexer.scope.clone(),
11391161
}),
1140-
(None, Some(name)) => Some(DataTypeDeclaration::Definition {
1162+
(None, Some(name), _) => Some(DataTypeDeclaration::Definition {
11411163
data_type: Box::new(DataType::SubRangeType {
11421164
name: Some(name.into()),
11431165
referenced_type: text,
@@ -1164,10 +1186,19 @@ fn parse_enum_type_definition(
11641186
let elements = parse_expression_list(lexer);
11651187
Some(elements)
11661188
})?;
1189+
1190+
// Check for Codesys-style type specification after the enum list
1191+
// TYPE COLOR : (...) DWORD;
1192+
let numeric_type = if lexer.token == Identifier {
1193+
lexer.slice_and_advance()
1194+
} else {
1195+
DINT_TYPE.to_string()
1196+
};
1197+
11671198
let initializer = lexer.try_consume(KeywordAssignment).then(|| parse_expression(lexer));
11681199
Some((
11691200
DataTypeDeclaration::Definition {
1170-
data_type: Box::new(DataType::EnumType { name, elements, numeric_type: DINT_TYPE.to_string() }),
1201+
data_type: Box::new(DataType::EnumType { name, elements, numeric_type }),
11711202
location: start.span(&lexer.last_location()),
11721203
scope: lexer.scope.clone(),
11731204
},

0 commit comments

Comments
 (0)