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
106 changes: 106 additions & 0 deletions docfx/articles/compiler/ADDED_MEMBERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,3 +176,109 @@ When the application tries to write to the variable it first validates that the

AttributeToolTip allows you to describe the variable or an object. These can be then used to give short hints to the user in the application. This attribute can be localized.

## Typed Enum Accessor Properties

### Overview

For properties that store enumeration values (either PLC enums or named value types with integral backing types), the AXSharp compiler automatically generates typed enum accessor properties. These properties provide a convenient way to access the enum value cast to a strongly-typed C# enum, without requiring explicit casting.

### When Generated

Typed enum accessor properties are generated for:
- Properties based on PLC `ENUM` types
- Properties based on **named value types with integral backing types** (BYTE, USINT, SINT, INT, UINT, WORD, DINT, DWORD, UDINT, LINT, LWORD, ULINT)

### Naming Convention

For a property named `Status` of enum type `Color`, the compiler generates an accessor property named `StatusEnum`.

### Example

**PLC code:**
~~~iecst
{S7.extern=ReadWrite}
TYPE Color : DINT
Black := 0;
White := 1;
Red := 2;
END_TYPE

CLASS PUBLIC MyClass
VAR PUBLIC
Status : Color;
END_VAR
END_CLASS
~~~

**Generated C# code:**
~~~C#
public partial class MyClass : AXSharp.Connector.ITwinObject
{
[AXSharp.Connector.EnumeratorDiscriminatorAttribute(typeof(Color))]
public OnlinerInt Status { get; }

public Color StatusEnum { get => (Color)Status.LastValue; }
}
~~~

### Usage

Instead of manually casting, you can use the generated typed accessor:

~~~C#
// Without typed accessor - requires casting
var statusValue = (Color)myObject.Status.LastValue;

// With typed accessor - automatic casting
var statusValue = myObject.StatusEnum;
~~~

### Important: Reading the Backing Property First

**The typed enum accessor properties read from the `LastValue` of the backing property.** This means that **the backing property must be read from the PLC first** for the accessor to return a valid value.

The accessor does **not** perform any I/O operation itself—it simply casts the last read value.

#### Example Scenario

~~~C#
// INCORRECT - StatusEnum will return default/zero value
var status = myObject.StatusEnum; // Invalid! No read was performed yet

// CORRECT - Read the backing property first
await myObject.Status.GetAsync(); // Read from PLC
var status = myObject.StatusEnum; // Now valid with the read value

// CORRECT - Bulk read on the object
myObject.Read(); // Cyclic read of all properties
var status = myObject.StatusEnum; // Valid after read/polling
~~~

#### Polling/Cyclic Scenario

When using a read loop that executes periodically (polling):

~~~C#
// Polling loop
while (isRunning)
{
myObject.Read(); // Reads all properties including Status

// Now all typed accessors have valid data
var status = myObject.StatusEnum;
var color = (Color)status;

await Task.Delay(100); // Update interval
}
~~~

### EnumeratorDiscriminatorAttribute

All properties with typed enum accessors are marked with `[EnumeratorDiscriminatorAttribute]`. This attribute identifies the corresponding enum type and is used internally by the framework for serialization and UI support.

You can also see this marking on POCO (Plain Old CLR Object) versions of your types, where it helps maintain enum type information:

~~~C#
[AXSharp.Connector.EnumeratorDiscriminatorAttribute(typeof(Color))]
public Color Status { get; set; }
~~~
2 changes: 1 addition & 1 deletion docfx/articles/compiler/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ Entry.Plc.weather.GeoLocation.Write();
~~~

- [Attributes](ATTRIBUTES.md)
- [Added members](ADDED_MEMBERS.md)
- [Added members](ADDED_MEMBERS.md) - including typed enum accessor properties
- [Config file](CONFIG_FILE.md)
- [Packaging and dependency management](PACKAGING.md)

Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions src/AXSharp.compiler/src/AXSharp.Cs.Compiler/Helpers/CsHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,29 @@

using System.Text;
using AX.ST.Semantic.Model.Declarations;
using AX.ST.Semantic.Model.Declarations.Types;
using AX.ST.Syntax.Tree;
using AXSharp.Connector;

namespace AXSharp.Compiler.Cs.Helpers;

internal static class CsHelpers
{
private static readonly HashSet<string> IntegralIecTypes = new(StringComparer.OrdinalIgnoreCase)
{
"BYTE", "USINT", "SINT", "INT", "UINT", "WORD",
"DINT", "DWORD", "UDINT", "LINT", "LWORD", "ULINT"
};

/// <summary>
/// Determines whether the backing type of a named value type is integral
/// and thus valid as a C# enum base type.
/// </summary>
public static bool HasIntegralBackingType(this INamedValueTypeDeclaration namedValueType)
{
return IntegralIecTypes.Contains(namedValueType.ValueTypeAccess.Type.Name.Trim());
}

public static string Transform(this IAccessModifierSyntax syntax)
{
return $"{syntax.ModifierKeyword.Text.ToLower()} ";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ public void CreateFieldDeclaration(IFieldDeclaration fieldDeclaration, IxNodeVis
AddToSource("OnlinerInt");
AddToSource($" {fieldDeclaration.Name}");
AddToSource("{get;}");
AddToSource($"{fieldDeclaration.AccessModifier.Transform()} {@enum.GetQualifiedName()} {fieldDeclaration.Name}Enum {{ get => ({@enum.GetQualifiedName()}){fieldDeclaration.Name}.LastValue; }}");
break;
case INamedValueTypeDeclaration namedValue:
AddToSource(
Expand All @@ -74,6 +75,8 @@ public void CreateFieldDeclaration(IFieldDeclaration fieldDeclaration, IxNodeVis
//fieldDeclaration.Type.Accept(visitor, this);
AddToSource($" {fieldDeclaration.Name}");
AddToSource("{get;}");
if (namedValue.HasIntegralBackingType())
AddToSource($"{fieldDeclaration.AccessModifier.Transform()} {namedValue.GetQualifiedName()} {fieldDeclaration.Name}Enum {{ get => ({namedValue.GetQualifiedName()}){fieldDeclaration.Name}.LastValue; }}");
break;
case IArrayTypeDeclaration array:
var arrayEligibility = array.IsEligibleForTranspile(SourceBuilder, warnMissingOrInconsistent: true);
Expand Down Expand Up @@ -159,6 +162,7 @@ public void CreateVariableDeclaration(IVariableDeclaration semantics, IxNodeVisi
AddToSource("OnlinerInt");
AddToSource($" {semantics.Name}");
AddToSource("{get;}");
AddToSource($"public {@enum.GetQualifiedName()} {semantics.Name}Enum {{ get => ({@enum.GetQualifiedName()}){semantics.Name}.LastValue; }}");
break;
case INamedValueTypeDeclaration namedValue:
AddToSource(
Expand All @@ -168,6 +172,8 @@ public void CreateVariableDeclaration(IVariableDeclaration semantics, IxNodeVisi
//semantics.Type.Accept(visitor, this);
AddToSource($" {semantics.Name}");
AddToSource("{get;}");
if (namedValue.HasIntegralBackingType())
AddToSource($"public {namedValue.GetQualifiedName()} {semantics.Name}Enum {{ get => ({namedValue.GetQualifiedName()}){semantics.Name}.LastValue; }}");
break;
case IArrayTypeDeclaration array:
var arrayEligible = array.IsEligibleForTranspile(SourceBuilder, warnMissingOrInconsistent: true);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// AXSharp.Compiler.Cs
// AXSharp.Compiler.Cs
// Copyright (c) 2023 MTS spol. s r.o., and Contributors. All Rights Reserved.
// Contributors: https://github.com/inxton/axsharp/graphs/contributors
// See the LICENSE file in the repository root for more information.
Expand Down Expand Up @@ -145,9 +145,14 @@ public void CreateFieldDeclaration(IFieldDeclaration fieldDeclaration, IxNodeVis
AddPropertyDeclaration(fieldDeclaration, fieldDeclaration, visitor);
AddToSource(" = string.Empty;");
break;
case INamedValueTypeDeclaration namedValueType:
case IEnumTypeDeclaration @enum:
AddToSource($"[AXSharp.Connector.EnumeratorDiscriminatorAttribute(typeof(global::{@enum.GetQualifiedName()}))]");
AddPropertyDeclaration(fieldDeclaration, fieldDeclaration, visitor);
break;
case INamedValueTypeDeclaration namedValueType:
AddToSource($"[AXSharp.Connector.EnumeratorDiscriminatorAttribute(typeof(global::{namedValueType.GetQualifiedName()}))]");
AddPropertyDeclaration(fieldDeclaration, fieldDeclaration, visitor);
break;
case IScalarTypeDeclaration scalar:
AddPropertyDeclaration(fieldDeclaration, fieldDeclaration, visitor);
AddToSource(scalar.CreateScalarInitializer(this.Project?.CompilerOptions?.TargetPlatfromMoniker));
Expand Down Expand Up @@ -312,7 +317,12 @@ public void CreateVariableDeclaration(IVariableDeclaration fieldDeclaration, IxN
AddPropertyDeclaration(fieldDeclaration, fieldDeclaration, visitor);
AddToSource(" = string.Empty;");
break;
case IEnumTypeDeclaration @enum:
AddToSource($"[AXSharp.Connector.EnumeratorDiscriminatorAttribute(typeof({@enum.GetQualifiedName()}))]");
AddPropertyDeclaration(fieldDeclaration, fieldDeclaration, visitor);
break;
case INamedValueTypeDeclaration namedValueType:
AddToSource($"[AXSharp.Connector.EnumeratorDiscriminatorAttribute(typeof(global::{namedValueType.GetQualifiedName()}))]");
AddPropertyDeclaration(fieldDeclaration, fieldDeclaration.Type, visitor);
break;
case IScalarTypeDeclaration scalar:
Expand Down Expand Up @@ -439,4 +449,4 @@ private static string ShortedQualifiedIfPossible(IDeclaration semantics)
? semantics.Type.FullyQualifiedName.Remove(0, semantics.ContainingNamespace.FullyQualifiedName.Length + 1)
: semantics.Type.FullyQualifiedName;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// AXSharp.Compiler.Cs
// AXSharp.Compiler.Cs
// Copyright (c) 2023 MTS spol. s r.o., and Contributors. All Rights Reserved.
// Contributors: https://github.com/inxton/axsharp/graphs/contributors
// See the LICENSE file in the repository root for more information.
Expand Down Expand Up @@ -147,13 +147,27 @@ public static string SetProperties(this ITypeDeclaration typeDeclaration)
/// <returns></returns>
public static string GetPropertyValue(this IDeclaration declaration, string propertyName, string memberName = "")
{
var propertyValue = declaration.Pragmas.FirstOrDefault(p =>
p.Content.Replace(" ", string.Empty).StartsWith($"{PRAGMA_PROPERTY_SET_SIGNATURE}{propertyName}"))
?.Content.Split('=');
try
{
// This is only to check for malformed pragmas, if there is any, exception will be thrown and caught in catch block and empty string will be returned.
string.Join("\r\n",
declaration.Pragmas.Where(p => p.Content.StartsWith(PRAGMA_PROPERTY_SET_SIGNATURE))
.Select(p => Pragmas.PragmaParser.PragmaCompiler.Compile(p).Product));

if (propertyValue is { Length: > 0 }) return propertyValue[1].Replace("\"", string.Empty).Trim();
var propertyValue = declaration.Pragmas.FirstOrDefault(p =>
p.Content.Replace(" ", string.Empty).StartsWith($"{PRAGMA_PROPERTY_SET_SIGNATURE}{propertyName}"))
?.Content.Split('=');

return memberName;
if (propertyValue is { Length: > 0 }) return propertyValue[1].Replace("\"", string.Empty).Trim();

return memberName;
}
catch (MalformedPragmaException ex)
{
// swallow
}

return string.Empty;
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// AXSharp.Compiler.Cs
// AXSharp.Compiler.Cs
// Copyright (c) 2023 MTS spol. s r.o., and Contributors. All Rights Reserved.
// Contributors: https://github.com/inxton/axsharp/graphs/contributors
// See the LICENSE file in the repository root for more information.
Expand Down Expand Up @@ -60,7 +60,7 @@ private static VisitorProduct Compile(IPragma pragma, Parser parser)
if (pragma.Location != null)
{
diagMessage =
$"[Error]: {pragma.Location.GetLineSpan().Filename}:{pragma.Location.GetLineSpan().StartLinePosition.Line}, {pragma.Location.GetLineSpan().StartLinePosition.Character} {malformedPragmaException.Message}";
$"[Error]: {pragma.Location.GetLineSpan().Filename}:{pragma.Location.GetLineSpan().StartLinePosition.Line + 1}, {pragma.Location.GetLineSpan().StartLinePosition.Character} {malformedPragmaException.Message}";
}

Log.Logger.Error(diagMessage);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// AXSharp.Compiler.Cs
// AXSharp.Compiler.Cs
// Copyright (c) 2023 MTS spol. s r.o., and Contributors. All Rights Reserved.
// Contributors: https://github.com/inxton/axsharp/graphs/contributors
// See the LICENSE file in the repository root for more information.
Expand Down Expand Up @@ -130,7 +130,7 @@ public PragmaGrammar()
AddedPropertySetter.Rule =
ix_set + colon + AddedPropertyIdentifier + assing + AddedPropertyInitializer;

Pragmas.Rule = AddedPropertyDeclaration | DeclarationAttribute | AddedPropertySetter | GenericAttribute;
Pragmas.Rule = AddedPropertyDeclaration | AddedPropertySetter | DeclarationAttribute | GenericAttribute;

Root = Pragmas;

Expand All @@ -145,4 +145,4 @@ public override void BuildAst(LanguageData language, ParseTree parseTree)
var astBuilder = new AstBuilder(astContext);
astBuilder.BuildAst(parseTree);
}
}
}
7 changes: 6 additions & 1 deletion src/AXSharp.compiler/src/ixc/Properties/launchSettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@
"ixc_data": {
"commandName": "Project",
"workingDirectory": "D:\\github\\Inxton\\axopen\\src\\data\\tests\\AXOpen.Data.Tests_L1\\ax"
},
"honolulu": {
"commandName": "Project",
"commandLineArgs": "--skip-deps",
"workingDirectory": "c:\\W\\Develop\\gh\\inxton\\simatic-ax\\axopen.templates\\axopen.template.simple\\ax\\"
}
}
}
}
Loading