| // | |
| // Licensed to the Apache Software Foundation (ASF) under one | |
| // or more contributor license agreements. See the NOTICE file | |
| // distributed with this work for additional information | |
| // regarding copyright ownership. The ASF licenses this file | |
| // to you under the Apache License, Version 2.0 (the | |
| // "License"); you may not use this file except in compliance | |
| // with the License. You may obtain a copy of the License at | |
| // | |
| // https://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, | |
| // software distributed under the License is distributed on an | |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| // KIND, either express or implied. See the License for the | |
| // specific language governing permissions and limitations | |
| // under the License. | |
| // | |
| using System; | |
| using System.Collections.Generic; | |
| using System.Linq; | |
| using org.apache.plc4net.tools.codegen.model; | |
| using org.apache.plc4net.tools.codegen.model.fields; | |
| using org.apache.plc4net.tools.codegen.model.terms; | |
| namespace org.apache.plc4net.tools.codegen.output | |
| { | |
| /// <summary> | |
| /// Emits C# for an mspec <see cref="Protocol"/>: one file per complex type | |
| /// (class + <c>StaticParse</c> + <c>Serialize</c> + length) and per enum, | |
| /// plus a constants class. This is the pure-.NET replacement for the Java | |
| /// freemarker <c>model-template</c> / <c>io-template</c>. | |
| /// </summary> | |
| public sealed class CSharpGenerator | |
| { | |
| private readonly Protocol _protocol; | |
| private readonly string _namespace; | |
| private readonly string _protocolName; | |
| private readonly bool _manualStaticCalls; | |
| /// <param name="manualStaticCalls"> | |
| /// Leave each <c>STATIC_CALL</c> this generator has no implementation for to a hand-written | |
| /// part of the protocol's partial <c>StaticHelper</c> class, as plc4j and plc4go do: the calls | |
| /// are generated as the mspec writes them and the compiler fails the build for a function that | |
| /// is missing. Without it, such a call fails the generation. | |
| /// </param> | |
| public CSharpGenerator( | |
| Protocol protocol, string protocolName, string namespaceName, bool manualStaticCalls = false) | |
| { | |
| _protocol = protocol; | |
| _protocolName = protocolName; | |
| _namespace = namespaceName; | |
| _manualStaticCalls = manualStaticCalls; | |
| } | |
| /// <summary>Generates every file; key is a relative path.</summary> | |
| /// <summary>Fails generation on model states that would silently | |
| /// produce wrong or non-compiling drivers: a <c>byteOrder</c> other | |
| /// than <c>BIG_ENDIAN</c> / <c>LITTLE_ENDIAN</c> on a type, any | |
| /// declared <c>byteOrder</c> at all on an enum or a dataIo (their | |
| /// generated code has no parse/serialize body to scope it to - see | |
| /// <see cref="IsLittleEndian"/>), and enum fields keyed on an | |
| /// accessor the enum does not expose as a non-external argument (the | |
| /// reverse lookup would never be emitted, and generated code would | |
| /// not compile).</summary> | |
| private void ValidateModel() | |
| { | |
| foreach (var t in _protocol.Types) | |
| { | |
| if (t.Attributes.TryGetValue("byteOrder", out var order) | |
| && order is StringLiteral o && o.Value != "BIG_ENDIAN" && o.Value != "LITTLE_ENDIAN") | |
| { | |
| throw new NotSupportedException( | |
| $"type '{t.Name}' declares byteOrder '{o.Value}'; " | |
| + "only BIG_ENDIAN and LITTLE_ENDIAN are implemented"); | |
| } | |
| } | |
| foreach (var t in _protocol.Enums.Cast<TypeDefinition>().Concat(_protocol.DataIos)) | |
| { | |
| if (t.Attributes.TryGetValue("byteOrder", out var order) | |
| && order is StringLiteral o && o.Value != "BIG_ENDIAN") | |
| { | |
| throw new NotSupportedException( | |
| $"type '{t.Name}' declares byteOrder '{o.Value}'; " | |
| + "little-endian generation is only implemented for complex types, not enums or dataIo"); | |
| } | |
| } | |
| foreach (var t in _protocol.Types) | |
| { | |
| foreach (var f in t.Fields) | |
| { | |
| if (f is UnsupportedField unsupported) | |
| { | |
| throw new NotSupportedException( | |
| $"type '{t.Name}' contains unsupported mspec field " | |
| + $"'{unsupported.MspecKeyword}': {unsupported.RawText}"); | |
| } | |
| if (f is ArrayField { LoopType: ArrayField.Loop.Terminated }) | |
| { | |
| throw new NotSupportedException( | |
| $"type '{t.Name}' contains a terminated array '{f.Name}'; " | |
| + "terminated arrays are not implemented and cannot be emitted as count arrays"); | |
| } | |
| if (f.Type is SimpleTypeReference { BaseType: SimpleTypeReference.Base.Time | |
| or SimpleTypeReference.Base.Date or SimpleTypeReference.Base.DateTime }) | |
| { | |
| throw new NotSupportedException( | |
| $"type '{t.Name}.{f.Name}' uses '{f.Type}', which has no C# buffer mapping yet"); | |
| } | |
| if (f is not EnumField ef || ef.KeyAccessor == null | |
| || ef.Type is not EnumTypeReference et) | |
| { | |
| continue; | |
| } | |
| var e = _protocol.FindEnum(et.Name); | |
| var ki = e?.Arguments.FindIndex(a => a.Name == ef.KeyAccessor) ?? -1; | |
| if (ki < 0 || IsExternalEnum(e!.Arguments[ki].Type)) | |
| { | |
| throw new InvalidOperationException( | |
| $"enum field '{t.Name}.{ef.Name}' keys on '{ef.KeyAccessor}', which '{et.Name}' " | |
| + "does not declare as a non-external argument; " | |
| + "the generated reverse lookup would not exist"); | |
| } | |
| } | |
| } | |
| foreach (var dio in _protocol.DataIos) | |
| { | |
| foreach (var cs in dio.Cases) | |
| { | |
| foreach (var f in cs.Fields) | |
| { | |
| if (f is UnsupportedField unsupported) | |
| { | |
| throw new NotSupportedException( | |
| $"dataIo '{dio.Name}.{cs.Name}' contains unsupported mspec field " | |
| + $"'{unsupported.MspecKeyword}': {unsupported.RawText}"); | |
| } | |
| if (f is ArrayField { LoopType: ArrayField.Loop.Terminated }) | |
| { | |
| throw new NotSupportedException( | |
| $"dataIo '{dio.Name}.{cs.Name}' contains a terminated array '{f.Name}'; " | |
| + "terminated arrays are not implemented"); | |
| } | |
| } | |
| if (!IsSupportedDataIoCase(dio, cs)) | |
| { | |
| throw new NotSupportedException( | |
| $"dataIo '{dio.Name}' case '{cs.Name}' is not a supported C# value shape"); | |
| } | |
| } | |
| } | |
| var staticCalls = new SortedDictionary<string, string>(StringComparer.Ordinal); | |
| CollectStaticCalls(staticCalls); | |
| foreach (var name in staticCalls.Keys) | |
| { | |
| if (!_manualStaticCalls && !IsGeneratedStaticCall(name)) | |
| { | |
| throw new NotSupportedException( | |
| $"{_protocolName}.mspec uses STATIC_CALL '{name}', but no C# implementation is available; " | |
| + "pass --manual-static-calls if the hand-written part of the static helper supplies it"); | |
| } | |
| } | |
| } | |
| public Dictionary<string, string> Generate() | |
| { | |
| ValidateModel(); | |
| var files = new Dictionary<string, string>(); | |
| foreach (var e in _protocol.Enums) | |
| { | |
| // An external enum (PlcValueType) is hand-written in the SPI; | |
| // emitting a stub here would shadow it with an empty type. | |
| if (e.IsExternal) | |
| { | |
| continue; | |
| } | |
| files[$"model/{e.Name}.cs"] = EmitEnum(e); | |
| } | |
| foreach (var t in _protocol.Types) | |
| { | |
| files[$"model/{t.Name}.cs"] = EmitComplexType(t); | |
| } | |
| foreach (var dio in _protocol.DataIos) | |
| { | |
| files[$"model/{dio.Name}.cs"] = EmitDataIo(dio); | |
| } | |
| if (_protocol.Constants.Count > 0) | |
| { | |
| files[$"model/{Pascal(_protocolName)}Constants.cs"] = EmitConstants(); | |
| } | |
| files[$"model/{Pascal(_protocolName)}StaticHelper.cs"] = EmitStaticHelper(); | |
| return files; | |
| } | |
| // ── complex types ─────────────────────────────────────── | |
| private string EmitComplexType(ComplexTypeDefinition type) | |
| { | |
| var scope = new FieldScope(_protocol, type); | |
| var render = new CSharpExpressionRenderer(scope) | |
| { | |
| StaticHelperClass = $"{Pascal(_protocolName)}StaticHelper", | |
| }; | |
| var c = new CodeWriter(); | |
| Header(c); | |
| c.Line("using System;"); | |
| c.Line("using System.Linq;"); | |
| c.Line("using org.apache.plc4net.spi.drivers;"); | |
| c.Line("using org.apache.plc4net.spi.generation;"); | |
| c.Line(); | |
| c.Line($"namespace {_namespace}.model"); | |
| c.Line("{"); | |
| c.Indent(); | |
| var parent = type.IsDiscriminatedChild ? $" : {type.ParentName}" : " : IMessage"; | |
| var mods = type.IsDiscriminatedParent ? "abstract partial" : "partial"; | |
| c.Line($"public {mods} class {type.Name}{parent}"); | |
| c.Line("{"); | |
| c.Indent(); | |
| EmitConstFields(c, type, render); | |
| EmitDiscriminatorAccessors(c, type, render); | |
| EmitProperties(c, type, render); | |
| EmitConstructor(c, type); | |
| EmitStaticParse(c, type, render, scope); | |
| EmitSerialize(c, type, render, scope); | |
| EmitLength(c, type, render, scope); | |
| c.Outdent(); | |
| c.Line("}"); | |
| c.Outdent(); | |
| c.Line("}"); | |
| return c.ToString(); | |
| } | |
| private void EmitConstFields(CodeWriter c, ComplexTypeDefinition type, CSharpExpressionRenderer r) | |
| { | |
| foreach (var f in type.ConstFields) | |
| { | |
| c.Line($"public const {CSharpTypeMapper.CSharpType(f.Type)} {Pascal(f.Name)} = {r.Render(f.ReferenceValue)};"); | |
| } | |
| if (type.ConstFields.Any()) | |
| { | |
| c.Line(); | |
| } | |
| } | |
| private void EmitDiscriminatorAccessors(CodeWriter c, ComplexTypeDefinition type, CSharpExpressionRenderer r) | |
| { | |
| if (type.IsDiscriminatedParent) | |
| { | |
| foreach (var d in type.Fields.OfType<DiscriminatorField>()) | |
| { | |
| c.Line($"public abstract {CSharpTypeMapper.CSharpType(d.Type)} {Pascal(d.Name)} {{ get; }}"); | |
| } | |
| // A parser-arg discriminator with no matching field becomes an | |
| // abstract accessor too, so children can pin their value. | |
| foreach (var name in ParserArgDiscriminatorNames(type)) | |
| { | |
| var arg = type.Arguments.First(a => a.Name == name); | |
| c.Line($"public abstract {CSharpTypeMapper.CSharpType(arg.Type)} {Pascal(name)} {{ get; }}"); | |
| } | |
| c.Line(); | |
| } | |
| if (type.IsDiscriminatedChild && _protocol.FindType(type.ParentName) is { } p) | |
| { | |
| var values = type.DiscriminatorValues ?? Array.Empty<Term>(); | |
| var discriminators = DiscriminatorList(p, r); | |
| var retained = RetainedDiscriminators(type).Select(d => d.Name).ToHashSet(); | |
| var emitted = false; | |
| for (var i = 0; i < discriminators.Count; i++) | |
| { | |
| var d = discriminators[i]; | |
| if (d.AccessorName == null) | |
| { | |
| continue; // property field or dotted path - no accessor | |
| } | |
| if (retained.Contains(d.AccessorName)) | |
| { | |
| // The case does not pin this wire discriminator, so the | |
| // value the parent read is kept and written back. | |
| c.Line($"public override {CSharpTypeMapper.CSharpType(d.Type)} {Pascal(d.AccessorName)} {{ get; }}"); | |
| emitted = true; | |
| continue; | |
| } | |
| var pinned = values.Count > i && values[i] != null; | |
| // An unpinned discriminator that is not read from the wire (a | |
| // parser argument such as Modbus' `response`) takes the type's | |
| // default, as in plc4j. | |
| var rhs = pinned | |
| ? RenderDiscriminatorValue(values[i], d.Type, r) | |
| : $"default({CSharpTypeMapper.CSharpType(d.Type)})"; | |
| c.Line($"public override {CSharpTypeMapper.CSharpType(d.Type)} {Pascal(d.AccessorName)} => {rhs};"); | |
| emitted = true; | |
| } | |
| if (emitted) | |
| { | |
| c.Line(); | |
| } | |
| } | |
| } | |
| private void EmitProperties(CodeWriter c, ComplexTypeDefinition type, CSharpExpressionRenderer parseRender) | |
| { | |
| foreach (var f in type.PropertyFields) | |
| { | |
| c.Line($"public {PropertyType(f)} {Pascal(f.Name)} {{ get; }}"); | |
| } | |
| var serializeRender = new CSharpExpressionRenderer(new FieldScope(_protocol, type, serialize: true)) | |
| { | |
| StaticHelperClass = parseRender.StaticHelperClass, | |
| }; | |
| foreach (var f in type.Fields.OfType<VirtualField>()) | |
| { | |
| c.Line($"public {CSharpTypeMapper.CSharpType(f.Type)} {Pascal(f.Name)} => {WithType(serializeRender.Render(f.ValueExpression), f.Type)};"); | |
| } | |
| if (type.PropertyFields.Any() || type.Fields.OfType<VirtualField>().Any()) | |
| { | |
| c.Line(); | |
| } | |
| } | |
| private void EmitConstructor(CodeWriter c, ComplexTypeDefinition type) | |
| { | |
| var all = AllValueFields(type); | |
| var retained = RetainedDiscriminators(type); | |
| if (all.Count == 0 && retained.Count == 0) | |
| { | |
| return; | |
| } | |
| var own = type.PropertyFields.ToList(); | |
| var inherited = ParentPrefixFields(type).Concat(ParentSuffixFields(type)).ToList(); | |
| var pars = string.Join(", ", all | |
| .Select(f => $"{PropertyType(f)} {Camel(f.Name)}") | |
| .Concat(retained.Select(d => $"{CSharpTypeMapper.CSharpType(d.Type)} {Camel(d.Name)}"))); | |
| var mod = type.IsDiscriminatedParent ? "protected" : "public"; | |
| var baseCall = inherited.Count > 0 | |
| ? " : base(" + string.Join(", ", inherited.Select(f => Camel(f.Name))) + ")" | |
| : ""; | |
| c.Line($"{mod} {type.Name}({pars}){baseCall}"); | |
| c.Line("{"); | |
| c.Indent(); | |
| foreach (var f in own) | |
| { | |
| c.Line($"{Pascal(f.Name)} = {Camel(f.Name)};"); | |
| } | |
| foreach (var d in retained) | |
| { | |
| c.Line($"{Pascal(d.Name)} = {Camel(d.Name)};"); | |
| } | |
| c.Outdent(); | |
| c.Line("}"); | |
| c.Line(); | |
| } | |
| // ── StaticParse ───────────────────────────────────────── | |
| /// <summary> | |
| /// True for a type whose mspec declaration itself carries | |
| /// <c>byteOrder='"LITTLE_ENDIAN"'</c> - never true for a discriminated | |
| /// child, a type-switch case, or a type a field merely references: none | |
| /// of those repeat the attribute, because they inherit it from the | |
| /// ambient buffer their parent already pushed (see | |
| /// <see cref="ReadBuffer.PushByteOrder"/>). Only the declaring type's | |
| /// own <c>StaticParse</c> / <c>Serialize</c> pushes and pops it. | |
| /// </summary> | |
| private static bool IsLittleEndian(ComplexTypeDefinition type) => | |
| type.Attributes.TryGetValue("byteOrder", out var order) | |
| && order is StringLiteral { Value: "LITTLE_ENDIAN" }; | |
| private void EmitStaticParse( | |
| CodeWriter c, ComplexTypeDefinition type, CSharpExpressionRenderer r, FieldScope scope) | |
| { | |
| var args = ParseSignatureArgs(type); | |
| // `new` only when the child's StaticParse has the same signature as | |
| // the base's (no extra context parameters) - otherwise it neither | |
| // hides anything (CS0109) nor needs to. | |
| var hide = type.IsDiscriminatedChild | |
| && ParentContextFields(type).Count == 0 | |
| && RetainedDiscriminators(type).Count == 0 | |
| ? "new " | |
| : ""; | |
| c.Line($"public static {hide}{type.Name} StaticParse(ReadBuffer readBuffer{Comma(args)})"); | |
| c.Line("{"); | |
| c.Indent(); | |
| var littleEndian = IsLittleEndian(type); | |
| if (littleEndian) | |
| { | |
| c.Line("readBuffer.PushByteOrder(org.apache.plc4net.spi.generation.ByteOrder.LITTLE_ENDIAN);"); | |
| c.Line("try"); | |
| c.Line("{"); | |
| c.Indent(); | |
| } | |
| // `curPos` is "bytes since this type's parse began". A child that | |
| // needs it receives _startPos from the parent (the byte count is | |
| // measured from where the parent started, before the discriminators); | |
| // everyone else captures it here. | |
| if (NeedsStartPos(type) && !(type.IsDiscriminatedChild && UsesCurPos(type))) | |
| { | |
| c.Line("var _startPos = readBuffer.GetPos();"); | |
| } | |
| foreach (var f in FieldsToEmit(type)) | |
| { | |
| EmitFieldParse(c, type, f, r, scope); | |
| } | |
| if (!type.IsDiscriminatedParent) | |
| { | |
| // prefix (passed in), own + suffix (read above), in wire order. | |
| var ctorArgs = string.Join(", ", AllValueFields(type).Select(f => Camel(f.Name)) | |
| .Concat(RetainedDiscriminators(type).Select(d => Camel(d.Name)))); | |
| c.Line($"return new {type.Name}({ctorArgs});"); | |
| } | |
| if (littleEndian) | |
| { | |
| // The type-switch case above already returned from inside the | |
| // try; finally still pops before control actually leaves. | |
| c.Outdent(); | |
| c.Line("}"); | |
| c.Line("finally"); | |
| c.Line("{"); | |
| c.Indent(); | |
| c.Line("readBuffer.PopByteOrder();"); | |
| c.Outdent(); | |
| c.Line("}"); | |
| } | |
| c.Outdent(); | |
| c.Line("}"); | |
| c.Line(); | |
| } | |
| private void EmitFieldParse( | |
| CodeWriter c, ComplexTypeDefinition type, Field f, CSharpExpressionRenderer r, FieldScope scope) | |
| { | |
| switch (f) | |
| { | |
| case DiscriminatorField d: | |
| c.Line($"var {Camel(d.Name)} = {CSharpTypeMapper.ReadCall(d.Type, d.Name)};"); | |
| break; | |
| case SimpleField: | |
| c.Line($"var {Camel(f.Name)} = {ReadFieldValue(f, r)};"); | |
| break; | |
| case EnumField ef: | |
| c.Line($"var {Camel(ef.Name)} = {EnumFieldRead(ef, r)};"); | |
| break; | |
| case ConstField cf: | |
| c.Line($"var {Camel(cf.Name)} = {CSharpTypeMapper.ReadCall(cf.Type, cf.Name)};"); | |
| c.Line($"if (!Equals({Camel(cf.Name)}, {WithType(r.Render(cf.ReferenceValue), cf.Type)}))"); | |
| c.Indent(); | |
| c.Line($"throw new ParseException($\"Expected constant {{{Pascal(cf.Name)}}} for '{cf.Name}' but got {{{Camel(cf.Name)}}}\");"); | |
| c.Outdent(); | |
| break; | |
| case ImplicitField imf: | |
| c.Line($"var {Camel(imf.Name)} = {CSharpTypeMapper.ReadCall(imf.Type, imf.Name)};"); | |
| break; | |
| case ReservedField rf: | |
| c.Line("{"); | |
| c.Indent(); | |
| c.Line($"var reserved = {CSharpTypeMapper.ReadCall(rf.Type, "reserved")};"); | |
| c.Line($"if (!Equals(reserved, {WithType(r.Render(rf.ReferenceValue), rf.Type)})) {{ /* mspec reserved: value differs from the spec default */ }}"); | |
| c.Outdent(); | |
| c.Line("}"); | |
| break; | |
| case ArrayField af: | |
| EmitArrayParse(c, af, r, scope); | |
| break; | |
| case ChecksumField ck: | |
| c.Line($"var {Camel(ck.Name)} = {CSharpTypeMapper.ReadCall(ck.Type, ck.Name)};"); | |
| c.Line($"var expected{Pascal(ck.Name)} = {WithType(r.Render(ck.ChecksumExpression), ck.Type)};"); | |
| c.Line($"if (!Equals({Camel(ck.Name)}, expected{Pascal(ck.Name)}))"); | |
| c.Indent(); | |
| c.Line($"throw new ParseException($\"Checksum '{ck.Name}' does not match: expected {{expected{Pascal(ck.Name)}}} but got {{{Camel(ck.Name)}}}\");"); | |
| c.Outdent(); | |
| break; | |
| case OptionalField opt: | |
| c.Line($"{PropertyType(opt)} {Camel(opt.Name)} = null;"); | |
| if (opt.Condition != null) | |
| { | |
| c.Line($"if ({r.Render(opt.Condition)})"); | |
| c.Line("{"); | |
| c.Indent(); | |
| } | |
| c.Line($"{Camel(opt.Name)} = {ReadFieldValue(opt, r)};"); | |
| if (opt.Condition != null) | |
| { | |
| c.Outdent(); | |
| c.Line("}"); | |
| } | |
| break; | |
| case ManualField mf: | |
| c.Line($"var {Camel(mf.Name)} = ({CSharpTypeMapper.CSharpType(mf.Type)}) ({r.Render(mf.ParseExpression)});"); | |
| break; | |
| case PaddingField pad: | |
| c.Line("{"); | |
| c.Indent(); | |
| c.Line($"var _timesPadding = (int) ({r.Render(pad.TimesPadding)});"); | |
| c.Line("while (_timesPadding-- > 0)"); | |
| c.Line("{"); | |
| c.Indent(); | |
| c.Line($"{CSharpTypeMapper.ReadCall(pad.Type, "padding")};"); | |
| c.Outdent(); | |
| c.Line("}"); | |
| c.Outdent(); | |
| c.Line("}"); | |
| break; | |
| case TypeSwitchField ts: | |
| EmitTypeSwitchParse(c, type, ts, r); | |
| break; | |
| case VirtualField vf: | |
| c.Line($"var {Camel(vf.Name)} = {WithType(r.Render(vf.ValueExpression), vf.Type)};"); | |
| break; | |
| case UnsupportedField uf: | |
| c.Line($"// TODO unsupported mspec field '{uf.MspecKeyword}': {uf.RawText}"); | |
| break; | |
| } | |
| } | |
| private void EmitArrayParse(CodeWriter c, ArrayField af, CSharpExpressionRenderer r, FieldScope scope) | |
| { | |
| var element = ElementType(af); | |
| var (elemType, isByteArray) = CSharpTypeMapper.ArrayType(element); | |
| var name = Camel(af.Name); | |
| if (isByteArray && af.LoopType is ArrayField.Loop.Count or ArrayField.Loop.Length) | |
| { | |
| c.Line($"var {name} = readBuffer.ReadByteArray(\"{af.Name}\", (int) ({r.Render(af.LoopExpression)}) * 8);"); | |
| return; | |
| } | |
| c.Line($"var {name} = new {elemType}();"); | |
| // Underscore-prefixed locals: mspec field names never start with one, | |
| // so a loop's bookkeeping cannot collide with an implicit / simple | |
| // field of the same base name (e.g. an `itemsCount` implicit beside | |
| // an `items count 'itemsCount'` array). | |
| // `_lastItem` is only knowable ahead of time for a counted loop. | |
| var lastItem = af.LoopType == ArrayField.Loop.Count && ElementUsesLastItem(af) | |
| ? $"_{name}I == _{name}Cnt - 1" | |
| : null; | |
| if (af.LoopType == ArrayField.Loop.Length) | |
| { | |
| c.Line($"var _{name}End = readBuffer.GetPos() + (int) ({r.Render(af.LoopExpression)}) * 8;"); | |
| c.Line($"while (readBuffer.GetPos() < _{name}End)"); | |
| } | |
| else | |
| { | |
| c.Line($"var _{name}Cnt = (int) ({r.Render(af.LoopExpression)});"); | |
| c.Line($"for (var _{name}I = 0; _{name}I < _{name}Cnt; _{name}I++)"); | |
| } | |
| c.Line("{"); | |
| c.Indent(); | |
| var read = CSharpTypeMapper.ReadCall(element, af.Name, ElementArgs(element, r, lastItem)); | |
| c.Line($"{name}.Add({read});"); | |
| c.Outdent(); | |
| c.Line("}"); | |
| } | |
| private void EmitTypeSwitchParse( | |
| CodeWriter c, ComplexTypeDefinition parent, TypeSwitchField ts, CSharpExpressionRenderer r) | |
| { | |
| var discriminators = DiscriminatorList(parent, r); | |
| foreach (var childName in ts.CaseNames) | |
| { | |
| // Every case name in ts.CaseNames was added to _protocol.Types | |
| // by BuildTypeSwitch in the same pass that recorded the name. | |
| var child = _protocol.FindType(childName) | |
| ?? throw new InvalidOperationException($"Type switch case '{childName}' has no matching type definition."); | |
| var values = child.DiscriminatorValues ?? Array.Empty<Term>(); | |
| var conds = new List<string>(); | |
| for (var i = 0; i < discriminators.Count && i < values.Count; i++) | |
| { | |
| if (values[i] == null) | |
| { | |
| continue; | |
| } | |
| var d = discriminators[i]; | |
| conds.Add($"Equals({d.DispatchExpr}, {RenderDiscriminatorValue(values[i], d.Type, r)})"); | |
| } | |
| var test = conds.Count == 0 ? "true" : string.Join(" && ", conds); | |
| var childArgs = ChildParseArgNames(child) | |
| .Concat(ParentContextFields(child).Select(f => Camel(f.Name))); | |
| if (UsesCurPos(child)) | |
| { | |
| childArgs = childArgs.Append("_startPos"); | |
| } | |
| // The wire discriminators this case leaves unpinned are handed down | |
| // so the child can write them back (see RetainedDiscriminators). | |
| childArgs = childArgs.Concat(RetainedDiscriminators(child).Select(d => Camel(d.Name))); | |
| c.Line($"if ({test})"); | |
| c.Line("{"); | |
| c.Indent(); | |
| c.Line($"return {childName}.StaticParse(readBuffer{Comma(childArgs)});"); | |
| c.Outdent(); | |
| c.Line("}"); | |
| } | |
| c.Line($"throw new ParseException(\"No matching subtype found for {parent.Name}\");"); | |
| } | |
| // ── Serialize ─────────────────────────────────────────── | |
| private void EmitSerialize( | |
| CodeWriter c, ComplexTypeDefinition type, CSharpExpressionRenderer r, FieldScope scope) | |
| { | |
| if (type.IsDiscriminatedChild) | |
| { | |
| c.Line($"protected override void SerializeChild(WriteBuffer writeBuffer)"); | |
| } | |
| else if (UsesLastItem(type)) | |
| { | |
| c.Line("public void Serialize(WriteBuffer writeBuffer) => Serialize(writeBuffer, false);"); | |
| c.Line(); | |
| c.Line("public void Serialize(WriteBuffer writeBuffer, bool _lastItem)"); | |
| } | |
| else | |
| { | |
| c.Line("public void Serialize(WriteBuffer writeBuffer)"); | |
| } | |
| c.Line("{"); | |
| c.Indent(); | |
| // A discriminated child never declares its own byteOrder (see | |
| // IsLittleEndian); it always serializes while still inside the | |
| // declaring parent's try, from the ambient PushByteOrder below. | |
| var littleEndian = IsLittleEndian(type); | |
| if (littleEndian) | |
| { | |
| c.Line("writeBuffer.PushByteOrder(org.apache.plc4net.spi.generation.ByteOrder.LITTLE_ENDIAN);"); | |
| c.Line("try"); | |
| c.Line("{"); | |
| c.Indent(); | |
| } | |
| var serScope = scope.ForSerialize(); | |
| var serRender = new CSharpExpressionRenderer(serScope) { StaticHelperClass = r.StaticHelperClass }; | |
| HoistParentImplicits(c, type, serRender, serScope); | |
| foreach (var f in FieldsToEmit(type)) | |
| { | |
| // `_value` inside a manual field's serialize expression is that | |
| // field's own value. | |
| serScope.CurrentValue = f is ManualField ? Pascal(f.Name) : null; | |
| EmitFieldSerialize(c, type, f, serRender); | |
| } | |
| serScope.CurrentValue = null; | |
| serScope.ClearLocals(); | |
| if (type.IsDiscriminatedParent) | |
| { | |
| c.Line("SerializeChild(writeBuffer);"); | |
| } | |
| if (littleEndian) | |
| { | |
| c.Outdent(); | |
| c.Line("}"); | |
| c.Line("finally"); | |
| c.Line("{"); | |
| c.Indent(); | |
| c.Line("writeBuffer.PopByteOrder();"); | |
| c.Outdent(); | |
| c.Line("}"); | |
| } | |
| c.Outdent(); | |
| c.Line("}"); | |
| c.Line(); | |
| if (type.IsDiscriminatedParent) | |
| { | |
| c.Line("protected abstract void SerializeChild(WriteBuffer writeBuffer);"); | |
| c.Line(); | |
| } | |
| } | |
| private void EmitFieldSerialize( | |
| CodeWriter c, ComplexTypeDefinition type, Field f, CSharpExpressionRenderer r) | |
| { | |
| switch (f) | |
| { | |
| case DiscriminatorField d: | |
| c.Line($"{CSharpTypeMapper.WriteCall(d.Type, d.Name, Pascal(d.Name))};"); | |
| break; | |
| case SimpleField when f.Type is SimpleTypeReference { BaseType: SimpleTypeReference.Base.VString, LengthExpression: { } len }: | |
| c.Line($"writeBuffer.WriteString(\"{f.Name}\", (int) ({r.Render(len)}), \"{VStringEncoding(f)}\", {Pascal(f.Name)});"); | |
| break; | |
| case SimpleField: | |
| c.Line($"{CSharpTypeMapper.WriteCall(f.Type, f.Name, Pascal(f.Name))};"); | |
| break; | |
| case EnumField ef: | |
| c.Line($"{EnumFieldWrite(ef)};"); | |
| break; | |
| case ConstField cf: | |
| c.Line($"{CSharpTypeMapper.WriteCall(cf.Type, cf.Name, Pascal(cf.Name))};"); | |
| break; | |
| case ImplicitField imf: | |
| var val = $"({CSharpTypeMapper.CSharpType(imf.Type)}) ({r.Render(imf.SerializeExpression)})"; | |
| c.Line($"{CSharpTypeMapper.WriteCall(imf.Type, imf.Name, val)};"); | |
| break; | |
| case ReservedField rf: | |
| c.Line($"{CSharpTypeMapper.WriteCall(rf.Type, "reserved", WithType(r.Render(rf.ReferenceValue), rf.Type))};"); | |
| break; | |
| case ArrayField af: | |
| EmitArraySerialize(c, af, r); | |
| break; | |
| case ChecksumField ck: | |
| var cv = $"({CSharpTypeMapper.CSharpType(ck.Type)}) ({r.Render(ck.ChecksumExpression)})"; | |
| c.Line($"{CSharpTypeMapper.WriteCall(ck.Type, ck.Name, cv)};"); | |
| break; | |
| case OptionalField opt: | |
| // .Value for a nullable value type; the reference itself | |
| // for a nullable class. | |
| var optRef = opt.Type is ComplexTypeReference | |
| ? Pascal(opt.Name) | |
| : $"{Pascal(opt.Name)}.Value"; | |
| c.Line($"if ({Pascal(opt.Name)} != null)"); | |
| c.Line("{"); | |
| c.Indent(); | |
| c.Line($"{CSharpTypeMapper.WriteCall(opt.Type, opt.Name, optRef)};"); | |
| c.Outdent(); | |
| c.Line("}"); | |
| break; | |
| case ManualField mf: | |
| c.Line($"{r.Render(mf.SerializeExpression)};"); | |
| break; | |
| case PaddingField pad: | |
| c.Line("{"); | |
| c.Indent(); | |
| c.Line($"var _timesPadding = (int) ({r.Render(pad.TimesPadding)});"); | |
| c.Line("while (_timesPadding-- > 0)"); | |
| c.Line("{"); | |
| c.Indent(); | |
| c.Line($"{CSharpTypeMapper.WriteCall(pad.Type, "padding", WithType(r.Render(pad.PaddingValue), pad.Type))};"); | |
| c.Outdent(); | |
| c.Line("}"); | |
| c.Outdent(); | |
| c.Line("}"); | |
| break; | |
| case TypeSwitchField: | |
| case VirtualField: | |
| case UnsupportedField: | |
| break; | |
| } | |
| } | |
| private void EmitArraySerialize(CodeWriter c, ArrayField af, CSharpExpressionRenderer r) | |
| { | |
| var element = ElementType(af); | |
| var (_, isByteArray) = CSharpTypeMapper.ArrayType(element); | |
| var prop = Pascal(af.Name); | |
| if (isByteArray) | |
| { | |
| c.Line($"writeBuffer.WriteByteArray(\"{af.Name}\", {prop});"); | |
| return; | |
| } | |
| if (ElementUsesLastItem(af)) | |
| { | |
| c.Line($"for (var _i = 0; _i < {prop}.Count; _i++)"); | |
| c.Line("{"); | |
| c.Indent(); | |
| c.Line($"{prop}[_i].Serialize(writeBuffer, _i == {prop}.Count - 1);"); | |
| c.Outdent(); | |
| c.Line("}"); | |
| return; | |
| } | |
| c.Line($"foreach (var _e in {prop})"); | |
| c.Line("{"); | |
| c.Indent(); | |
| c.Line($"{CSharpTypeMapper.WriteCall(element, af.Name, "_e")};"); | |
| c.Outdent(); | |
| c.Line("}"); | |
| } | |
| // ── length ────────────────────────────────────────────── | |
| private void EmitLength( | |
| CodeWriter c, ComplexTypeDefinition type, CSharpExpressionRenderer r, FieldScope scope) | |
| { | |
| var serScope = scope.ForSerialize(); | |
| var serRender = new CSharpExpressionRenderer(serScope) { StaticHelperClass = r.StaticHelperClass }; | |
| var childMethod = type.IsDiscriminatedChild; | |
| var lastItem = !childMethod && UsesLastItem(type); | |
| if (lastItem) | |
| { | |
| c.Line("public int GetLengthInBits() => GetLengthInBits(false);"); | |
| c.Line(); | |
| c.Line("public int GetLengthInBits(bool _lastItem)"); | |
| } | |
| else | |
| { | |
| c.Line(childMethod | |
| ? "protected override int GetLengthInBitsChild()" | |
| : "public int GetLengthInBits()"); | |
| } | |
| c.Line("{"); | |
| c.Indent(); | |
| c.Line("var lengthInBits = 0;"); | |
| HoistParentImplicits(c, type, serRender, serScope); | |
| foreach (var f in FieldsToEmit(type)) | |
| { | |
| var term = LengthContribution(f, serRender); | |
| if (term != null) | |
| { | |
| c.Line($"lengthInBits += {term};"); | |
| } | |
| } | |
| serScope.ClearLocals(); | |
| if (type.IsDiscriminatedParent) | |
| { | |
| c.Line("lengthInBits += GetLengthInBitsChild();"); | |
| } | |
| c.Line("return lengthInBits;"); | |
| c.Outdent(); | |
| c.Line("}"); | |
| c.Line(); | |
| if (type.IsDiscriminatedParent) | |
| { | |
| c.Line("protected abstract int GetLengthInBitsChild();"); | |
| c.Line(); | |
| } | |
| if (!childMethod) | |
| { | |
| // Round up: a type that is not byte-aligned still occupies a | |
| // whole trailing byte on the wire (plc4j and the dataIo path | |
| // both ceil). | |
| c.Line($"public int GetLengthInBytes() => (GetLengthInBits({(lastItem ? "false" : "")}) + 7) / 8;"); | |
| } | |
| } | |
| /// <summary> | |
| /// A discriminated child's serialize / length can reference the | |
| /// parent's pre-typeSwitch implicit fields, which the parent (not the | |
| /// child) writes. Recompute the ones it names as locals at the top so | |
| /// the expressions resolve. | |
| /// </summary> | |
| private void HoistParentImplicits( | |
| CodeWriter c, ComplexTypeDefinition type, CSharpExpressionRenderer r, FieldScope scope) | |
| { | |
| if (!type.IsDiscriminatedChild || _protocol.FindType(type.ParentName) is not { } p) | |
| { | |
| return; | |
| } | |
| var referenced = new HashSet<string>(); | |
| foreach (var f in FieldsToEmit(type)) | |
| { | |
| foreach (var t in SerializeTerms(f)) | |
| { | |
| CollectNames(t, referenced); | |
| } | |
| } | |
| foreach (var imf in p.PrefixImplicitFields.Where(i => referenced.Contains(i.Name))) | |
| { | |
| scope.MarkLocal(imf.Name); | |
| c.Line($"var {Camel(imf.Name)} = ({CSharpTypeMapper.CSharpType(imf.Type)}) ({r.Render(imf.SerializeExpression)});"); | |
| } | |
| } | |
| /// <summary>The expression trees a field contributes to | |
| /// <c>Serialize</c> / <c>GetLengthInBits</c>. Excludes the parse-only | |
| /// ones: an optional's condition, an array's loop expression, a manual | |
| /// field's parse expression, and a complex field's constructor | |
| /// arguments (those are passed to <c>StaticParse</c>, never re-evaluated | |
| /// on the way out).</summary> | |
| private static IEnumerable<Term> SerializeTerms(Field f) | |
| { | |
| switch (f) | |
| { | |
| case ImplicitField i: yield return i.SerializeExpression; break; | |
| case ChecksumField ck: yield return ck.ChecksumExpression; break; | |
| case ReservedField r: yield return r.ReferenceValue; break; | |
| case VirtualField v: yield return v.ValueExpression; break; | |
| case ManualField m: | |
| yield return m.SerializeExpression; | |
| yield return m.LengthExpression; | |
| break; | |
| case PaddingField p: | |
| yield return p.PaddingValue; | |
| yield return p.TimesPadding; | |
| break; | |
| } | |
| } | |
| private string? LengthContribution(Field f, CSharpExpressionRenderer r) | |
| { | |
| switch (f) | |
| { | |
| case SimpleField or ConstField or DiscriminatorField or ImplicitField | |
| or ReservedField or ChecksumField or EnumField: | |
| var bits = CSharpTypeMapper.FixedBitLength(f.Type); | |
| if (bits >= 0) | |
| { | |
| return bits.ToString(); | |
| } | |
| if (f.Type is SimpleTypeReference { BaseType: SimpleTypeReference.Base.VString, LengthExpression: { } vlen }) | |
| { | |
| return $"({r.Render(vlen)})"; | |
| } | |
| if (f.Type is ComplexTypeReference) | |
| { | |
| return $"{Pascal(f.Name)}.GetLengthInBits()"; | |
| } | |
| return null; // vstring without a length / unmodelled - not computed | |
| case VirtualField: | |
| return null; | |
| case ArrayField af: | |
| var element = ElementType(af); | |
| var (_, isByteArray) = CSharpTypeMapper.ArrayType(element); | |
| var prop = Pascal(af.Name); | |
| if (isByteArray) | |
| { | |
| return $"({prop}.Length * 8)"; | |
| } | |
| var eb = CSharpTypeMapper.FixedBitLength(element); | |
| if (eb >= 0) | |
| { | |
| return $"({prop}.Count * {eb})"; | |
| } | |
| return ElementUsesLastItem(af) | |
| ? $"System.Linq.Enumerable.Range(0, {prop}.Count).Sum(_i => {prop}[_i].GetLengthInBits(_i == {prop}.Count - 1))" | |
| : $"{prop}.Sum(_e => _e.GetLengthInBits())"; | |
| case OptionalField opt: | |
| var name = Pascal(opt.Name); | |
| var ob = CSharpTypeMapper.FixedBitLength(opt.Type); | |
| if (ob >= 0) | |
| { | |
| return $"({name} != null ? {ob} : 0)"; | |
| } | |
| return opt.Type is ComplexTypeReference | |
| ? $"({name}?.GetLengthInBits() ?? 0)" | |
| : null; | |
| case ManualField mf: | |
| return $"(({r.Render(mf.LengthExpression)}) * 8)"; | |
| case PaddingField pad: | |
| var pb = CSharpTypeMapper.FixedBitLength(pad.Type); | |
| return $"(({r.Render(pad.TimesPadding)}) * {(pb > 0 ? pb : 8)})"; | |
| default: | |
| return null; | |
| } | |
| } | |
| // ── enums ─────────────────────────────────────────────── | |
| private string EmitEnum(EnumTypeDefinition e) | |
| { | |
| var r = new CSharpExpressionRenderer(FieldScope.EnumConstantsOnly(_protocol)); | |
| var baseType = e.BaseType != null | |
| ? CSharpTypeMapper.SimpleCSharpType(e.BaseType) | |
| : "int"; | |
| var c = new CodeWriter(); | |
| Header(c); | |
| c.Line($"namespace {_namespace}.model"); | |
| c.Line("{"); | |
| c.Indent(); | |
| c.Line($"public enum {e.Name} : {baseType}"); | |
| c.Line("{"); | |
| c.Indent(); | |
| foreach (var v in e.Values) | |
| { | |
| var val = v.Value != null ? r.Render(v.Value) : null; | |
| c.Line(val != null ? $"{v.Name} = {val}," : $"{v.Name},"); | |
| } | |
| c.Outdent(); | |
| c.Line("}"); | |
| if (e.Arguments.Count > 0) | |
| { | |
| c.Line(); | |
| c.Line($"public static class {e.Name}Extensions"); | |
| c.Line("{"); | |
| c.Indent(); | |
| for (var ai = 0; ai < e.Arguments.Count; ai++) | |
| { | |
| var arg = e.Arguments[ai]; | |
| // The accessor would return an external enum whose members | |
| // the generator cannot see; nothing consumes it, so skip it. | |
| if (IsExternalEnum(arg.Type)) | |
| { | |
| continue; | |
| } | |
| var rows = e.Values.Where(v => v.ConstantValues.Count > ai).ToList(); | |
| var nullable = IsValueType(arg.Type) | |
| && rows.Any(v => v.ConstantValues[ai] is NullLiteral); | |
| var retType = CSharpTypeMapper.CSharpType(arg.Type) + (nullable ? "?" : ""); | |
| // `Get` prefix: keeps the method name off the enum type name | |
| // it may return (`DataTransportSize`), and matches the | |
| // `x.attr` -> `x.GetAttr()` the expression renderer emits. | |
| c.Line($"public static {retType} Get{Pascal(arg.Name)}(this {e.Name} value) => value switch"); | |
| c.Line("{"); | |
| c.Indent(); | |
| // C# rejects duplicate switch patterns when two enum | |
| // members share the same underlying value (S7's | |
| // TransportSize has COUNTER and DATE_AND_TIME at 0x1C). | |
| // Keep every enum member in the declaration, but make the | |
| // accessor deterministic by keeping the first member for | |
| // each underlying enum value. | |
| EmitEnumArms( | |
| c, | |
| rows, | |
| v => RenderEnumUnderlyingValue(v, r), | |
| v => $"{e.Name}.{v.Name}", | |
| v => RenderEnumParamValue(v.ConstantValues[ai], arg.Type, r)); | |
| c.Line("_ => default!,"); | |
| c.Outdent(); | |
| c.Line("};"); | |
| } | |
| foreach (var key in EnumFieldKeys(e.Name)) | |
| { | |
| var ki = e.Arguments.FindIndex(a => a.Name == key); | |
| if (ki < 0) | |
| { | |
| continue; | |
| } | |
| var keyType = e.Arguments[ki].Type; | |
| var rows = e.Values.Where(v => v.ConstantValues.Count > ki).ToList(); | |
| c.Line($"public static {e.Name}? FirstEnumForField{Pascal(key)}({CSharpTypeMapper.CSharpType(keyType)} {Camel(key)}) => {Camel(key)} switch"); | |
| c.Line("{"); | |
| c.Indent(); | |
| // "first" lookup: the earliest constant wins each key value. | |
| string PatternFor(EnumValue v) => RenderEnumParamValue(v.ConstantValues[ki], keyType, r); | |
| EmitEnumArms(c, rows, PatternFor, PatternFor, v => $"{e.Name}.{v.Name}"); | |
| // An unmapped key is null, not a bogus `(T) 0` — the caller | |
| // (a parse path) then fails loudly with a real value in hand. | |
| c.Line("_ => null,"); | |
| c.Outdent(); | |
| c.Line("};"); | |
| } | |
| c.Outdent(); | |
| c.Line("}"); | |
| } | |
| c.Outdent(); | |
| c.Line("}"); | |
| return c.ToString(); | |
| } | |
| /// <summary>Emits <c>pattern => result,</c> switch arms, skipping any | |
| /// whose <paramref name="dedupKey"/> repeats an earlier arm - two enum | |
| /// constants can share an underlying value, and an enum can map several | |
| /// constants to the same attribute value.</summary> | |
| private static void EmitEnumArms( | |
| CodeWriter c, IEnumerable<EnumValue> rows, | |
| System.Func<EnumValue, string> dedupKey, | |
| System.Func<EnumValue, string> pattern, | |
| System.Func<EnumValue, string> result) | |
| { | |
| var seen = new HashSet<string>(); | |
| foreach (var v in rows) | |
| { | |
| if (seen.Add(dedupKey(v))) | |
| { | |
| c.Line($"{pattern(v)} => {result(v)},"); | |
| } | |
| } | |
| } | |
| /// <summary>Renders one entry of an enum's parameter table, coerced to | |
| /// the parameter's declared type: a bare word is a string literal for a | |
| /// <c>vstring</c> parameter, a member of the target enum for an | |
| /// enum-typed one, and a char literal for a one-character integer.</summary> | |
| private string RenderEnumParamValue(Term term, TypeReference target, CSharpExpressionRenderer r) | |
| { | |
| switch (target) | |
| { | |
| case EnumTypeReference et: | |
| if (term is NullLiteral) | |
| { | |
| return "null"; | |
| } | |
| if (term is VariableLiteral { Child: null, IsCall: false } ev) | |
| { | |
| return $"{et.Name}.{ev.Name}"; | |
| } | |
| return r.Render(term); | |
| case SimpleTypeReference { BaseType: SimpleTypeReference.Base.String | |
| or SimpleTypeReference.Base.VString }: | |
| if (term is NullLiteral) | |
| { | |
| return "null"; | |
| } | |
| if (term is VariableLiteral { Child: null, IsCall: false } sv) | |
| { | |
| return sv.Name == MspecReader.EmptyStringSentinel | |
| ? "\"\"" | |
| : "\"" + sv.Name + "\""; | |
| } | |
| // A string-typed parameter table entry is always a literal, but | |
| // an unquoted one is parsed as an expression: a bare number | |
| // ('409') as an IntegerLiteral, a hyphenated id ('DPST-1-1') as | |
| // a '-' chain. Fold both back to the string they spell. | |
| if (term is StringLiteral strLit) | |
| { | |
| return "\"" + strLit.Value.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\""; | |
| } | |
| if (term is IntegerLiteral or FloatLiteral or BooleanLiteral) | |
| { | |
| return "\"" + term + "\""; | |
| } | |
| if (TryFlattenHyphenatedId(term, out var id)) | |
| { | |
| return "\"" + id + "\""; | |
| } | |
| return r.Render(term); | |
| case SimpleTypeReference { IsIntegerLike: true } it: | |
| if (term is VariableLiteral { Child: null, IsCall: false, Name.Length: 1 } cv) | |
| { | |
| return $"({CSharpTypeMapper.SimpleCSharpType(it)}) '{cv.Name}'"; | |
| } | |
| return r.Render(term); | |
| default: | |
| return r.Render(term); | |
| } | |
| } | |
| /// <summary>An mspec enum id like <c>'DPST-1-1'</c> lexes as | |
| /// <c>DPST - 1 - 1</c>: the grammar has no hyphen in an identifier and no | |
| /// bare string. Rebuild the original text from a subtraction chain whose | |
| /// leaves are all bare names or integers - the only way a string-typed | |
| /// parameter can carry a <c>-</c>.</summary> | |
| private static bool TryFlattenHyphenatedId(Term term, out string? id) | |
| { | |
| var parts = new List<string>(); | |
| if (!Collect(term) || parts.Count < 2) | |
| { | |
| id = null; | |
| return false; | |
| } | |
| id = string.Join("-", parts); | |
| return true; | |
| bool Collect(Term t) | |
| { | |
| switch (t) | |
| { | |
| case BinaryExpression { Operator: "-" } b: | |
| return Collect(b.Left) && Collect(b.Right); | |
| case VariableLiteral { Child: null, IsCall: false, Index.Count: 0 } v: | |
| parts.Add(v.Name); | |
| return true; | |
| case IntegerLiteral i: | |
| parts.Add(i.Value.ToString(System.Globalization.CultureInfo.InvariantCulture)); | |
| return true; | |
| default: | |
| return false; | |
| } | |
| } | |
| } | |
| private bool IsExternalEnum(TypeReference t) => | |
| t is EnumTypeReference et | |
| && _protocol.Enums.Any(e => e.Name == et.Name && e.IsExternal); | |
| private static bool IsValueType(TypeReference t) => | |
| t is EnumTypeReference | |
| || t is SimpleTypeReference s | |
| && s.BaseType is not (SimpleTypeReference.Base.String or SimpleTypeReference.Base.VString); | |
| /// <summary>The distinct <c>KeyAccessor</c>s of every <c>enum</c> field | |
| /// in the protocol that is typed as <paramref name="enumName"/> - each | |
| /// needs a <c>FirstEnumForField…</c> reverse lookup.</summary> | |
| private IEnumerable<string> EnumFieldKeys(string enumName) => | |
| _protocol.Types | |
| .SelectMany(t => t.Fields) | |
| .OfType<EnumField>() | |
| .Where(f => f.KeyAccessor != null | |
| && (f.Type as EnumTypeReference)?.Name == enumName) | |
| .Select(f => f.KeyAccessor!) | |
| .Distinct(); | |
| private string EmitConstants() | |
| { | |
| var r = new CSharpExpressionRenderer(FieldScope.EnumConstantsOnly(_protocol)); | |
| var c = new CodeWriter(); | |
| Header(c); | |
| c.Line($"namespace {_namespace}.model"); | |
| c.Line("{"); | |
| c.Indent(); | |
| c.Line($"public static class {Pascal(_protocolName)}Constants"); | |
| c.Line("{"); | |
| c.Indent(); | |
| foreach (var k in _protocol.Constants) | |
| { | |
| c.Line($"public const {CSharpTypeMapper.CSharpType(k.Type)} {Pascal(k.Name)} = {r.Render(k.Value)};"); | |
| } | |
| c.Outdent(); | |
| c.Line("}"); | |
| c.Outdent(); | |
| c.Line("}"); | |
| return c.ToString(); | |
| } | |
| // ── dataIo (parses to / from an IPlcValue) ────────────── | |
| /// <summary>IEC value wrappers whose plc4net type is exactly | |
| /// <c>Plc<caseName></c> and which round-trip through a single | |
| /// scalar accessor.</summary> | |
| private static readonly HashSet<string> ScalarPlcValueCases = new HashSet<string> | |
| { | |
| "BOOL", "BYTE", "WORD", "DWORD", "LWORD", | |
| "SINT", "USINT", "INT", "UINT", "DINT", "UDINT", "LINT", "ULINT", | |
| "REAL", "LREAL", "CHAR", "WCHAR", "STRING", "WSTRING", | |
| }; | |
| /// <summary>The TIA date / time <c>DataItem</c> cases - each maps to a | |
| /// <c>Plc{caseName}</c> built through a factory / segment call, and the | |
| /// S7 date helpers (<c>parseS5Time</c>, <c>parseTiaDate</c>, | |
| /// <c>parseSiemensYear</c>, BCD). Only the multi-value | |
| /// <c>numberOfValues > 1</c> list cases stay a throwing stub now.</summary> | |
| private static readonly HashSet<string> TemporalPlcValueCases = new HashSet<string> | |
| { | |
| "TIME", "LTIME", "DATE", "TIME_OF_DAY", "LTIME_OF_DAY", | |
| "DATE_AND_TIME", "DATE_AND_LTIME", | |
| }; | |
| private string EmitDataIo(DataIoTypeDefinition dio) | |
| { | |
| var helper = $"{Pascal(_protocolName)}StaticHelper"; | |
| var c = new CodeWriter(); | |
| Header(c); | |
| c.Line("using System;"); | |
| c.Line("using org.apache.plc4net.api.value;"); | |
| c.Line("using org.apache.plc4net.spi.generation;"); | |
| c.Line("using org.apache.plc4net.spi.model.values;"); | |
| c.Line(); | |
| c.Line($"namespace {_namespace}.model"); | |
| c.Line("{"); | |
| c.Indent(); | |
| c.Line("/// <summary>"); | |
| c.Line($"/// mspec <c>[dataIo {dio.Name}]</c> - reads and writes one"); | |
| c.Line("/// <see cref=\"IPlcValue\"/> whose wire layout the parser arguments pick."); | |
| c.Line("/// </summary>"); | |
| c.Line($"public static class {dio.Name}"); | |
| c.Line("{"); | |
| c.Indent(); | |
| var argDecls = dio.Arguments | |
| .Select(a => $"{CSharpTypeMapper.CSharpType(a.Type)} {Camel(a.Name)}") | |
| .ToList(); | |
| var argNames = dio.Arguments.Select(a => Camel(a.Name)).ToList(); | |
| // A dataIo case with no discriminator values (KNX's KnxProperty) is | |
| // the catch-all: it compiles to `else { … }`, and the trailing | |
| // fallback `return` after it would be unreachable. | |
| var caughtAll = dio.Cases.Count > 0 && IsWildcardCase(dio.Cases[^1]); | |
| c.Line($"public static IPlcValue StaticParse(ReadBuffer readBuffer{Comma(argDecls)})"); | |
| c.Line("{"); | |
| c.Indent(); | |
| for (var i = 0; i < dio.Cases.Count; i++) | |
| { | |
| EmitDataIoCase(c, dio, dio.Cases[i], helper, DataIoMode.Parse, i == 0, i == dio.Cases.Count - 1); | |
| } | |
| if (!caughtAll) | |
| { | |
| c.Line("return new PlcNULL();"); | |
| } | |
| c.Outdent(); | |
| c.Line("}"); | |
| c.Line(); | |
| c.Line($"public static void StaticSerialize(WriteBuffer writeBuffer, IPlcValue _value{Comma(argDecls)})"); | |
| c.Line("{"); | |
| c.Indent(); | |
| for (var i = 0; i < dio.Cases.Count; i++) | |
| { | |
| EmitDataIoCase(c, dio, dio.Cases[i], helper, DataIoMode.Serialize, i == 0, i == dio.Cases.Count - 1); | |
| } | |
| c.Outdent(); | |
| c.Line("}"); | |
| c.Line(); | |
| c.Line($"public static int GetLengthInBytes(IPlcValue _value{Comma(argDecls)}) =>"); | |
| c.Indent(); | |
| c.Line($"(GetLengthInBits(_value{Comma(argNames)}) + 7) / 8;"); | |
| c.Outdent(); | |
| c.Line(); | |
| c.Line($"public static int GetLengthInBits(IPlcValue _value{Comma(argDecls)})"); | |
| c.Line("{"); | |
| c.Indent(); | |
| c.Line("var lengthInBits = 0;"); | |
| for (var i = 0; i < dio.Cases.Count; i++) | |
| { | |
| EmitDataIoCase(c, dio, dio.Cases[i], helper, DataIoMode.Length, i == 0, i == dio.Cases.Count - 1); | |
| } | |
| c.Line("return lengthInBits;"); | |
| c.Outdent(); | |
| c.Line("}"); | |
| c.Outdent(); | |
| c.Line("}"); | |
| c.Outdent(); | |
| c.Line("}"); | |
| return c.ToString(); | |
| } | |
| private enum DataIoMode { Parse, Serialize, Length } | |
| /// <summary>A dataIo case that lists no discriminator values - the | |
| /// <c>[* …]</c> catch-all. KNX's <c>KnxProperty</c> ends with one.</summary> | |
| private static bool IsWildcardCase(ComplexTypeDefinition cs) => | |
| cs.DiscriminatorValues == null | |
| || cs.DiscriminatorValues.Count == 0 | |
| || cs.DiscriminatorValues.All(v => v == null); | |
| private void EmitDataIoCase( | |
| CodeWriter c, DataIoTypeDefinition dio, ComplexTypeDefinition cs, string helper, DataIoMode mode, bool first, bool last = false) | |
| { | |
| var serialize = mode != DataIoMode.Parse; | |
| var synthetic = new ComplexTypeDefinition { Name = dio.Name }; | |
| synthetic.Arguments.AddRange(dio.Arguments); | |
| synthetic.Fields.AddRange(cs.Fields); | |
| var scope = new FieldScope(_protocol, synthetic, serialize); | |
| // A dataIo's parser arguments are method parameters in every method, | |
| // never properties - keep them camelCase even while serializing. | |
| foreach (var a in dio.Arguments) | |
| { | |
| scope.MarkLocal(a.Name); | |
| } | |
| var r = new CSharpExpressionRenderer(scope) { StaticHelperClass = helper }; | |
| // The catch-all case compiles to `else { … }` (or just `{ … }` when | |
| // it is the only case) so the compiler sees no unreachable fallback. | |
| var test = DataIoCaseTest(dio, cs, r); | |
| if (last && test == "true" && !first) | |
| { | |
| c.Line("else"); | |
| } | |
| else if (last && test == "true") | |
| { | |
| // sole unconditional case: no wrapper needed | |
| } | |
| else | |
| { | |
| c.Line($"{(first ? "if" : "else if")} ({test})"); | |
| } | |
| c.Line("{"); | |
| c.Indent(); | |
| c.Line($"// {cs.Name}"); | |
| var props = cs.PropertyFields.ToList(); | |
| var valueField = props.Count == 1 ? props[0] : null; | |
| var listField = valueField as ArrayField; | |
| // The temporal bodies are a transcription of plc4j's S7 DataItem date | |
| // logic - they only fit S7's field layout, so gate them on S7's | |
| // discriminator. KNX's KnxDatapoint has DATE / TIME cases too, with a | |
| // completely different (segmented, struct-valued) shape. | |
| var temporal = TemporalPlcValueCases.Contains(cs.Name) | |
| && dio.Arguments.Any(a => a.Name == "dataProtocolId") | |
| && props.All(f => f is not ArrayField); | |
| var scalar = valueField is not (null or ArrayField) && ScalarPlcValueCases.Contains(cs.Name); | |
| // A KnxDatapoint / KnxProperty composite case ([… Struct …]): each | |
| // field is a plain simple field or a fixed-count byte array, so it | |
| // maps to a PlcStruct keyed by name. | |
| var structCase = cs.Name == "Struct" | |
| && props.Count > 0 | |
| && props.All(f => f is SimpleField { Type: SimpleTypeReference } | |
| || f is ArrayField | |
| { | |
| Type: SimpleTypeReference { BaseType: SimpleTypeReference.Base.Byte }, | |
| LoopType: ArrayField.Loop.Count, | |
| }); | |
| switch (mode) | |
| { | |
| case DataIoMode.Parse when temporal: | |
| EmitDataIoTemporalParse(c, cs, r); | |
| break; | |
| case DataIoMode.Parse when structCase: | |
| EmitDataIoStructParse(c, cs, r); | |
| break; | |
| case DataIoMode.Parse when listField != null: | |
| EmitDataIoListParse(c, listField, r); | |
| break; | |
| case DataIoMode.Parse: | |
| foreach (var f in cs.Fields) | |
| { | |
| EmitDataIoFieldParse(c, f, r); | |
| } | |
| c.Line($"return {DataIoReturnValue(cs, valueField)};"); | |
| break; | |
| case DataIoMode.Serialize when temporal: | |
| EmitDataIoTemporalSerialize(c, cs, helper); | |
| break; | |
| case DataIoMode.Serialize when structCase: | |
| EmitDataIoStructSerialize(c, cs, r); | |
| break; | |
| case DataIoMode.Serialize when listField != null: | |
| EmitDataIoListSerialize(c, listField); | |
| break; | |
| case DataIoMode.Serialize: | |
| foreach (var f in cs.Fields) | |
| { | |
| EmitDataIoFieldSerialize(c, cs, f, valueField, r); | |
| } | |
| break; | |
| case DataIoMode.Length when listField != null: | |
| c.Line($"lengthInBits += _value.GetLength() * {CSharpTypeMapper.FixedBitLength(listField.Type)};"); | |
| break; | |
| case DataIoMode.Length: | |
| foreach (var f in cs.Fields) | |
| { | |
| var bits = DataIoFieldLength(f, r); | |
| if (bits != null) | |
| { | |
| c.Line($"lengthInBits += {bits};"); | |
| } | |
| } | |
| break; | |
| } | |
| c.Outdent(); | |
| c.Line("}"); | |
| } | |
| /// <summary>Parses one TIA date / time case to its <c>Plc…</c> wrapper. | |
| /// Mirrors plc4j's <c>DataItem</c>: a factory / segment call, with the S7 | |
| /// helpers doing S5TIME, the Siemens epoch and BCD.</summary> | |
| private void EmitDataIoTemporalParse(CodeWriter c, ComplexTypeDefinition cs, CSharpExpressionRenderer r) | |
| { | |
| var field = cs.PropertyFields.FirstOrDefault(); | |
| switch (cs.Name) | |
| { | |
| case "TIME": | |
| c.Line(field is ManualField mf | |
| ? $"var milliseconds = {r.Render(RewriteTypeEncoding(mf.ParseExpression, "UTF8"))};" | |
| : "var milliseconds = readBuffer.ReadUint(\"milliseconds\", 32);"); | |
| c.Line("return PlcTIME.OfMilliseconds(milliseconds);"); | |
| break; | |
| case "LTIME": | |
| c.Line("var nanoseconds = readBuffer.ReadUlong(\"nanoseconds\", 64);"); | |
| c.Line("return PlcLTIME.OfNanoseconds(nanoseconds);"); | |
| break; | |
| case "DATE": | |
| // Unlike TIME above, DATE has no non-ManualField fallback: | |
| // every mspec DATE dataIo case is modeled as manual. | |
| c.Line($"var daysSinceEpoch = {r.Render(RewriteTypeEncoding(((ManualField) field!).ParseExpression, "UTF8"))};"); | |
| c.Line("return PlcDATE.OfDaysSinceEpoch(daysSinceEpoch);"); | |
| break; | |
| case "TIME_OF_DAY": | |
| c.Line("var millisecondsSinceMidnight = readBuffer.ReadUint(\"millisecondsSinceMidnight\", 32);"); | |
| c.Line("return PlcTIME_OF_DAY.OfMillisecondsSinceMidnight(millisecondsSinceMidnight);"); | |
| break; | |
| case "LTIME_OF_DAY": | |
| c.Line("var nanosecondsSinceMidnight = readBuffer.ReadUlong(\"nanosecondsSinceMidnight\", 64);"); | |
| c.Line("return PlcLTIME_OF_DAY.OfNanosecondsSinceMidnight(nanosecondsSinceMidnight);"); | |
| break; | |
| case "DATE_AND_TIME": | |
| c.Line($"var year = {r.StaticHelperClass}.ParseSiemensYear(readBuffer);"); | |
| foreach (var f in new[] { "month", "day", "hour", "minutes", "seconds" }) | |
| { | |
| c.Line($"var {f} = {r.StaticHelperClass}.BcdToBin(readBuffer.ReadByte(\"{f}\", 8));"); | |
| } | |
| c.Line($"var millisecondsOfSecond = {r.StaticHelperClass}.BcdToBin12(readBuffer.ReadUshort(\"millisecondsOfSecond\", 12));"); | |
| c.Line("readBuffer.ReadByte(\"dayOfWeek\", 4);"); | |
| c.Line("return PlcDATE_AND_TIME.OfSegments(year, month, day, hour, minutes, seconds, millisecondsOfSecond * 1000000);"); | |
| break; | |
| case "DATE_AND_LTIME" when cs.PropertyFields.Count() == 1: | |
| c.Line("var nanosecondsSinceEpoch = readBuffer.ReadUlong(\"nanosecondsSinceEpoch\", 64);"); | |
| c.Line("return PlcDATE_AND_LTIME.OfNanosecondsSinceEpoch(nanosecondsSinceEpoch);"); | |
| break; | |
| case "DATE_AND_LTIME": // DTL - plain-binary segments, widths from the mspec | |
| foreach (var f in cs.Fields.OfType<SimpleField>()) | |
| { | |
| var read = DataIoSimpleRead(f, r); | |
| // dayOfWeek is recomputed from the DateTime on serialize. | |
| c.Line(f.Name == "dayOfWeek" ? $"{read};" : $"var {Camel(f.Name)} = {read};"); | |
| } | |
| c.Line("return PlcDATE_AND_LTIME.OfSegments(year, month, day, hour, minutes, seconds, nanosecondsOfSecond);"); | |
| break; | |
| } | |
| } | |
| private void EmitDataIoTemporalSerialize(CodeWriter c, ComplexTypeDefinition cs, string helper) | |
| { | |
| var field = cs.PropertyFields.FirstOrDefault(); | |
| // S7 DT / DTL number the week Sunday=1 .. Saturday=7 (per s7.mspec's | |
| // own DTL comment and the Siemens DTL doc). This is a DELIBERATE | |
| // departure from plc4j, whose DataItem writes ISO-8601 | |
| // Monday=1 .. Sunday=7 (getDayOfWeek().getValue()) - the two never | |
| // agree on any weekday. Both discard the field on parse (the | |
| // DateTime carries its own weekday), so it only shows up when a | |
| // device consumes the serialized frame, or when diffing plc4net vs | |
| // plc4j serializer output. | |
| const string dow = "(byte) ((int) _dt.DayOfWeek + 1)"; | |
| switch (cs.Name) | |
| { | |
| case "TIME" when field is ManualField: | |
| c.Line($"{helper}.SerializeS5Time(writeBuffer, _value);"); | |
| break; | |
| case "TIME": | |
| c.Line("writeBuffer.WriteUint(\"milliseconds\", 32, (uint) _value.GetDuration().TotalMilliseconds);"); | |
| break; | |
| case "LTIME": | |
| c.Line("writeBuffer.WriteUlong(\"nanoseconds\", 64, ((PlcLTIME) _value).GetNanoseconds());"); | |
| break; | |
| case "DATE": | |
| c.Line($"{helper}.SerializeTiaDate(writeBuffer, _value);"); | |
| break; | |
| case "TIME_OF_DAY": | |
| c.Line("writeBuffer.WriteUint(\"millisecondsSinceMidnight\", 32, (uint) _value.GetTime().ToTimeSpan().TotalMilliseconds);"); | |
| break; | |
| case "LTIME_OF_DAY": | |
| c.Line("writeBuffer.WriteUlong(\"nanosecondsSinceMidnight\", 64, ((PlcLTIME_OF_DAY) _value).GetNanosecondsSinceMidnight());"); | |
| break; | |
| case "DATE_AND_TIME": | |
| c.Line("var _dt = _value.GetDateTime();"); | |
| c.Line($"{helper}.SerializeSiemensYear(writeBuffer, _value);"); | |
| c.Line($"writeBuffer.WriteByte(\"month\", 8, {helper}.BinToBcd(_dt.Month));"); | |
| c.Line($"writeBuffer.WriteByte(\"day\", 8, {helper}.BinToBcd(_dt.Day));"); | |
| c.Line($"writeBuffer.WriteByte(\"hour\", 8, {helper}.BinToBcd(_dt.Hour));"); | |
| c.Line($"writeBuffer.WriteByte(\"minutes\", 8, {helper}.BinToBcd(_dt.Minute));"); | |
| c.Line($"writeBuffer.WriteByte(\"seconds\", 8, {helper}.BinToBcd(_dt.Second));"); | |
| c.Line($"writeBuffer.WriteUshort(\"millisecondsOfSecond\", 12, {helper}.BinToBcd12(_dt.Millisecond));"); | |
| c.Line($"writeBuffer.WriteByte(\"dayOfWeek\", 4, {dow});"); | |
| break; | |
| case "DATE_AND_LTIME" when cs.PropertyFields.Count() == 1: | |
| c.Line("writeBuffer.WriteUlong(\"nanosecondsSinceEpoch\", 64, ((PlcDATE_AND_LTIME) _value).GetNanosecondsSinceEpoch());"); | |
| break; | |
| case "DATE_AND_LTIME": | |
| c.Line("var _dtl = (PlcDATE_AND_LTIME) _value;"); | |
| c.Line("var _dt = _dtl.GetDateTime();"); | |
| c.Line("writeBuffer.WriteUshort(\"year\", 16, (ushort) _dt.Year);"); | |
| c.Line("writeBuffer.WriteByte(\"month\", 8, (byte) _dt.Month);"); | |
| c.Line("writeBuffer.WriteByte(\"day\", 8, (byte) _dt.Day);"); | |
| c.Line($"writeBuffer.WriteByte(\"dayOfWeek\", 8, {dow});"); | |
| c.Line("writeBuffer.WriteByte(\"hour\", 8, (byte) _dt.Hour);"); | |
| c.Line("writeBuffer.WriteByte(\"minutes\", 8, (byte) _dt.Minute);"); | |
| c.Line("writeBuffer.WriteByte(\"seconds\", 8, (byte) _dt.Second);"); | |
| c.Line("writeBuffer.WriteUint(\"nanosecondsOfSecond\", 32, _dtl.GetNanosecondsOfSecond());"); | |
| break; | |
| } | |
| } | |
| /// <summary>A <c>numberOfValues > 1</c> dataIo case (Modbus's | |
| /// <c>DataItem</c> List cases) - a counted array of one primitive, | |
| /// wrapped element-by-element into a <see cref="PlcList"/>.</summary> | |
| private void EmitDataIoListParse(CodeWriter c, ArrayField af, CSharpExpressionRenderer r) | |
| { | |
| // An array field's element type is always given in the grammar. | |
| var element = (SimpleTypeReference) af.Type!; | |
| var wrap = DataIoPlcWrapper(element); | |
| c.Line("var value = new System.Collections.Generic.List<IPlcValue>();"); | |
| c.Line($"var _valueCnt = (int) ({r.Render(af.LoopExpression)});"); | |
| c.Line("for (var _i = 0; _i < _valueCnt; _i++)"); | |
| c.Line("{"); | |
| c.Indent(); | |
| if (element.BaseType is SimpleTypeReference.Base.String) | |
| { | |
| c.Line($"var _e = readBuffer.ReadString(\"value\", {element.SizeInBits}, {CSharpTypeMapper.EncodingExpr(element.Encoding)});"); | |
| c.Line($"value.Add(new {wrap}(_e.Length > 0 ? _e[0] : '\\0'));"); | |
| } | |
| else | |
| { | |
| c.Line($"value.Add(new {wrap}({CSharpTypeMapper.ReadCall(element, "value")}));"); | |
| } | |
| c.Outdent(); | |
| c.Line("}"); | |
| c.Line("return new PlcList(value);"); | |
| } | |
| private void EmitDataIoListSerialize(CodeWriter c, ArrayField af) | |
| { | |
| // An array field's element type is always given in the grammar. | |
| var element = (SimpleTypeReference) af.Type!; | |
| c.Line("foreach (var _e in _value.GetList())"); | |
| c.Line("{"); | |
| c.Indent(); | |
| var item = element.BaseType is SimpleTypeReference.Base.String | |
| ? "_e.GetString()" | |
| : $"({CSharpTypeMapper.SimpleCSharpType(element)}) _e.{PlcValueGetter(element)}"; | |
| c.Line(element.BaseType is SimpleTypeReference.Base.String | |
| ? $"writeBuffer.WriteString(\"value\", {element.SizeInBits}, \"{element.Encoding ?? "UTF8"}\", {item});" | |
| : $"{CSharpTypeMapper.WriteCall(element, "value", item)};"); | |
| c.Outdent(); | |
| c.Line("}"); | |
| } | |
| // ── dataIo struct cases (KnxDatapoint composites) ─────── | |
| /// <summary>Parses a <c>[… Struct …]</c> case: read each field, then | |
| /// bundle the simple ones into a <c>PlcStruct</c> keyed by field name. | |
| /// Mirrors plc4j's KNX <c>KnxDatapoint</c> struct branch.</summary> | |
| private void EmitDataIoStructParse(CodeWriter c, ComplexTypeDefinition cs, CSharpExpressionRenderer r) | |
| { | |
| foreach (var f in cs.Fields) | |
| { | |
| switch (f) | |
| { | |
| case ArrayField af: | |
| c.Line($"var {Camel(af.Name)} = readBuffer.ReadByteArray(\"{af.Name}\", (int) ({r.Render(af.LoopExpression)}) * 8);"); | |
| break; | |
| default: | |
| EmitDataIoFieldParse(c, f, r); | |
| break; | |
| } | |
| } | |
| c.Line("var _map = new System.Collections.Generic.Dictionary<string, IPlcValue>();"); | |
| foreach (var f in cs.Fields) | |
| { | |
| switch (f) | |
| { | |
| case ArrayField af: | |
| c.Line($"_map[\"{af.Name}\"] = new PlcRawByteArray({Camel(af.Name)});"); | |
| break; | |
| case SimpleField sf when sf.Type is SimpleTypeReference st: | |
| c.Line($"_map[\"{sf.Name}\"] = new {DataIoPlcWrapper(st)}(({CSharpTypeMapper.SimpleCSharpType(st)}) {Camel(sf.Name)});"); | |
| break; | |
| } | |
| } | |
| c.Line("return new PlcStruct(_map);"); | |
| } | |
| private void EmitDataIoStructSerialize(CodeWriter c, ComplexTypeDefinition cs, CSharpExpressionRenderer r) | |
| { | |
| foreach (var f in cs.Fields) | |
| { | |
| switch (f) | |
| { | |
| case ReservedField rf: | |
| c.Line($"{CSharpTypeMapper.WriteCall(rf.Type, "reserved", WithType(r.Render(rf.ReferenceValue), rf.Type))};"); | |
| break; | |
| case ArrayField af: | |
| c.Line($"writeBuffer.WriteByteArray(\"{af.Name}\", _value.GetValue(\"{af.Name}\")!.GetRaw());"); | |
| break; | |
| case SimpleField sf when sf.Type is SimpleTypeReference st: | |
| var v = $"({CSharpTypeMapper.SimpleCSharpType(st)}) _value.GetValue(\"{sf.Name}\")!.{PlcValueGetter(st)}"; | |
| c.Line($"{CSharpTypeMapper.WriteCall(sf.Type, sf.Name, v)};"); | |
| break; | |
| } | |
| } | |
| } | |
| /// <summary>The <c>Plc…</c> wrapper for one element of a dataIo list, | |
| /// keyed on the wire type (<c>bit</c> → <c>PlcBOOL</c>, <c>int 16</c> → | |
| /// <c>PlcINT</c>, <c>string 16</c> → <c>PlcWCHAR</c>, …).</summary> | |
| private static string DataIoPlcWrapper(SimpleTypeReference s) => s.BaseType switch | |
| { | |
| SimpleTypeReference.Base.Bit => "PlcBOOL", | |
| SimpleTypeReference.Base.Byte => "PlcUSINT", | |
| SimpleTypeReference.Base.UInt => s.SizeInBits <= 8 ? "PlcUSINT" | |
| : s.SizeInBits <= 16 ? "PlcUINT" | |
| : s.SizeInBits <= 32 ? "PlcUDINT" : "PlcULINT", | |
| SimpleTypeReference.Base.Int => s.SizeInBits <= 8 ? "PlcSINT" | |
| : s.SizeInBits <= 16 ? "PlcINT" | |
| : s.SizeInBits <= 32 ? "PlcDINT" : "PlcLINT", | |
| SimpleTypeReference.Base.Float or SimpleTypeReference.Base.UFloat => | |
| s.SizeInBits <= 32 ? "PlcREAL" : "PlcLREAL", | |
| SimpleTypeReference.Base.String or SimpleTypeReference.Base.VString => | |
| s.SizeInBits <= 8 ? "PlcCHAR" : "PlcWCHAR", | |
| _ => "PlcNULL", | |
| }; | |
| private string DataIoCaseTest( | |
| DataIoTypeDefinition dio, ComplexTypeDefinition cs, CSharpExpressionRenderer r) | |
| { | |
| // Every dataIo carries a typeSwitch; see DataIoTypeDefinition.TypeSwitch. | |
| var discs = dio.TypeSwitch!.Discriminators.OfType<VariableLiteral>().ToList(); | |
| var values = cs.DiscriminatorValues ?? Array.Empty<Term>(); | |
| var conds = new List<string>(); | |
| for (var i = 0; i < discs.Count && i < values.Count; i++) | |
| { | |
| if (values[i] == null) | |
| { | |
| continue; | |
| } | |
| var argType = dio.Arguments.FirstOrDefault(a => a.Name == discs[i].Name)?.Type; | |
| conds.Add($"Equals({Camel(discs[i].Name)}, {RenderDiscriminatorValue(values[i], argType, r)})"); | |
| } | |
| return conds.Count == 0 ? "true" : string.Join(" && ", conds); | |
| } | |
| private void EmitDataIoFieldParse(CodeWriter c, Field f, CSharpExpressionRenderer r) | |
| { | |
| switch (f) | |
| { | |
| case ReservedField rf: | |
| c.Line("{"); | |
| c.Indent(); | |
| c.Line($"var reserved = {CSharpTypeMapper.ReadCall(rf.Type, "reserved")};"); | |
| c.Line($"if (!Equals(reserved, {WithType(r.Render(rf.ReferenceValue), rf.Type)})) {{ /* mspec reserved: value differs from the spec default */ }}"); | |
| c.Outdent(); | |
| c.Line("}"); | |
| break; | |
| case ManualField mf: | |
| var parse = RewriteTypeEncoding(mf.ParseExpression, FieldEncoding(mf)); | |
| c.Line($"var {Camel(mf.Name)} = ({CSharpTypeMapper.CSharpType(mf.Type)}) ({r.Render(parse)});"); | |
| break; | |
| case SimpleField sf: | |
| c.Line($"var {Camel(sf.Name)} = {DataIoSimpleRead(sf, r)};"); | |
| break; | |
| } | |
| } | |
| private void EmitDataIoFieldSerialize( | |
| CodeWriter c, ComplexTypeDefinition cs, Field f, Field? valueField, CSharpExpressionRenderer r) | |
| { | |
| switch (f) | |
| { | |
| case ReservedField rf: | |
| c.Line($"{CSharpTypeMapper.WriteCall(rf.Type, "reserved", WithType(r.Render(rf.ReferenceValue), rf.Type))};"); | |
| break; | |
| case ManualField mf: | |
| c.Line($"{r.Render(RewriteTypeEncoding(mf.SerializeExpression, FieldEncoding(mf)))};"); | |
| break; | |
| case SimpleField sf when sf.Type is SimpleTypeReference st: | |
| var getter = st.BaseType is SimpleTypeReference.Base.String or SimpleTypeReference.Base.VString | |
| ? "_value.GetString()" | |
| : $"({CSharpTypeMapper.SimpleCSharpType(st)}) _value.{PlcValueGetter(st)}"; | |
| if (st.BaseType is SimpleTypeReference.Base.String) | |
| { | |
| c.Line($"writeBuffer.WriteString(\"{sf.Name}\", {st.SizeInBits}, \"{FieldEncoding(sf)}\", {getter});"); | |
| } | |
| else if (st.BaseType is SimpleTypeReference.Base.VString && st.LengthExpression is { } vlen) | |
| { | |
| c.Line($"writeBuffer.WriteString(\"{sf.Name}\", (int) ({r.Render(RewriteTypeEncoding(vlen, FieldEncoding(sf)))}), \"{FieldEncoding(sf)}\", {getter});"); | |
| } | |
| else | |
| { | |
| c.Line($"{CSharpTypeMapper.WriteCall(sf.Type, sf.Name, getter)};"); | |
| } | |
| break; | |
| } | |
| } | |
| private string? DataIoFieldLength(Field f, CSharpExpressionRenderer r) | |
| { | |
| switch (f) | |
| { | |
| case SimpleField when f.Type is SimpleTypeReference { BaseType: SimpleTypeReference.Base.VString, LengthExpression: { } vlen }: | |
| // vstring: the length expression is already in bits (`stringLength * 8`). | |
| return $"({r.Render(RewriteTypeEncoding(vlen, FieldEncoding((SimpleField) f)))})"; | |
| case ReservedField or SimpleField: | |
| var bits = CSharpTypeMapper.FixedBitLength(f.Type); | |
| return bits >= 0 ? bits.ToString() : null; | |
| case ArrayField { LoopType: ArrayField.Loop.Count } af: | |
| // a fixed-count byte array in a struct case | |
| return $"(({r.Render(af.LoopExpression)}) * 8)"; | |
| case ManualField mf: | |
| // A dataIo manual length expression is already in bits | |
| // (plc4j's DataItem: `(stringLength * 8) + 16`). | |
| return $"({r.Render(RewriteTypeEncoding(mf.LengthExpression, FieldEncoding(mf)))})"; | |
| default: | |
| return null; | |
| } | |
| } | |
| /// <summary>The <c>return new Plc…(value)</c> for a scalar | |
| /// <c>DataItem</c> case. The case name is the plc4net value type.</summary> | |
| private string DataIoReturnValue(ComplexTypeDefinition cs, Field? valueField) | |
| { | |
| // Only reached for the scalar branch, gated on valueField being | |
| // exactly one non-array property field (see the `scalar` check | |
| // in EmitDataIoCase). | |
| if (valueField == null) | |
| { | |
| throw new InvalidOperationException($"DataIo case '{cs.Name}' has no scalar value field."); | |
| } | |
| var v = Camel(valueField.Name); | |
| switch (cs.Name) | |
| { | |
| case "CHAR": | |
| case "WCHAR": | |
| // read as a 1- or 2-byte string; Plc(W)CHAR wraps a char. | |
| return $"new Plc{cs.Name}({v}.Length > 0 ? {v}[0] : '\\0')"; | |
| default: | |
| // BOOL / BYTE / … / STRING / WSTRING - Plc<caseName>(value). | |
| return $"new Plc{cs.Name}({v})"; | |
| } | |
| } | |
| private string DataIoSimpleRead(SimpleField sf, CSharpExpressionRenderer r) | |
| { | |
| switch (sf.Type) | |
| { | |
| case SimpleTypeReference { BaseType: SimpleTypeReference.Base.String } s: | |
| return $"readBuffer.ReadString(\"{sf.Name}\", {s.SizeInBits}, {CSharpTypeMapper.EncodingExpr(FieldEncoding(sf))})"; | |
| // vstring: the bit length is a run-time expression (modbus DataItem's | |
| // `vstring 'stringLength * 8'`), already in bits. | |
| case SimpleTypeReference { BaseType: SimpleTypeReference.Base.VString, LengthExpression: { } len }: | |
| return $"readBuffer.ReadString(\"{sf.Name}\", (int) ({r.Render(RewriteTypeEncoding(len, FieldEncoding(sf)))}), {CSharpTypeMapper.EncodingExpr(FieldEncoding(sf))})"; | |
| default: | |
| return CSharpTypeMapper.ReadCall(sf.Type, sf.Name); | |
| } | |
| } | |
| /// <summary>The <c>IPlcValue</c> accessor that yields a value of this | |
| /// wire type, for a dataIo serialize.</summary> | |
| private static string PlcValueGetter(SimpleTypeReference s) => s.BaseType switch | |
| { | |
| SimpleTypeReference.Base.Bit => "GetBool()", | |
| SimpleTypeReference.Base.Byte => "GetByte()", | |
| SimpleTypeReference.Base.UInt => s.SizeInBits <= 8 ? "GetByte()" | |
| : s.SizeInBits <= 16 ? "GetUshort()" | |
| : s.SizeInBits <= 32 ? "GetUint()" : "GetUlong()", | |
| SimpleTypeReference.Base.Int => s.SizeInBits <= 8 ? "GetSbyte()" | |
| : s.SizeInBits <= 16 ? "GetShort()" | |
| : s.SizeInBits <= 32 ? "GetInt()" : "GetLong()", | |
| SimpleTypeReference.Base.Float or SimpleTypeReference.Base.UFloat => | |
| s.SizeInBits <= 32 ? "GetFloat()" : "GetDouble()", | |
| _ => "GetString()", | |
| }; | |
| /// <summary>The string encoding for a dataIo value field: the | |
| /// <c>encoding</c> / <c>stringEncoding</c> attribute (s7.mspec spells it | |
| /// the first way, modbus.mspec the second), or the type's own resolved | |
| /// encoding, defaulting to UTF-8.</summary> | |
| private static string FieldEncoding(Field f) | |
| { | |
| if (f.Type is SimpleTypeReference { Encoding: { } typeEncoding }) | |
| { | |
| return typeEncoding; | |
| } | |
| foreach (var key in new[] { "encoding", "stringEncoding" }) | |
| { | |
| if (f.Attributes.TryGetValue(key, out var t) && t is StringLiteral s) | |
| { | |
| return s.Value; | |
| } | |
| } | |
| return "UTF8"; | |
| } | |
| /// <summary>Inlines <c>_type.encoding</c> (a dataIo case's own encoding | |
| /// attribute) as a string literal, the way plc4j's <c>DataItem</c> does - | |
| /// a dataIo has no instance for <c>_type</c> to resolve against.</summary> | |
| private static Term RewriteTypeEncoding(Term t, string encoding) | |
| { | |
| switch (t) | |
| { | |
| case VariableLiteral v when v.Name == "_type" | |
| && v.Child is { Name: "encoding", Child: null }: | |
| return new StringLiteral(encoding); | |
| case VariableLiteral v: | |
| return new VariableLiteral( | |
| v.Name, | |
| v.Args?.Select(a => RewriteTypeEncoding(a, encoding)).ToList(), | |
| v.Index?.Select(i => RewriteTypeEncoding(i, encoding)).ToList(), | |
| v.Child); | |
| case BinaryExpression b: | |
| return new BinaryExpression( | |
| RewriteTypeEncoding(b.Left, encoding), b.Operator, RewriteTypeEncoding(b.Right, encoding)); | |
| case UnaryExpression u: | |
| return new UnaryExpression(u.Operator, RewriteTypeEncoding(u.Operand, encoding)); | |
| case TernaryExpression x: | |
| return new TernaryExpression( | |
| RewriteTypeEncoding(x.Condition, encoding), | |
| RewriteTypeEncoding(x.WhenTrue, encoding), | |
| RewriteTypeEncoding(x.WhenFalse, encoding)); | |
| default: | |
| return t; | |
| } | |
| } | |
| // ── static helper (STATIC_CALL targets + array sizing) ── | |
| private string EmitStaticHelper() | |
| { | |
| // name -> the checksum/field C# type the call has to return | |
| var staticCalls = new SortedDictionary<string, string>(StringComparer.Ordinal); | |
| CollectStaticCalls(staticCalls); | |
| var cls = $"{Pascal(_protocolName)}StaticHelper"; | |
| var c = new CodeWriter(); | |
| Header(c); | |
| c.Line("using System;"); | |
| c.Line("using System.Collections.Generic;"); | |
| c.Line("using System.Linq;"); | |
| c.Line("using org.apache.plc4net.spi.drivers;"); | |
| c.Line("using org.apache.plc4net.spi.generation;"); | |
| c.Line(); | |
| c.Line($"namespace {_namespace}.model"); | |
| c.Line("{"); | |
| c.Indent(); | |
| c.Line("/// <summary>"); | |
| c.Line("/// Shared helpers required by the generated protocol model."); | |
| c.Line("/// </summary>"); | |
| c.Line($"public static partial class {cls}"); | |
| c.Line("{"); | |
| c.Indent(); | |
| c.Line("/// <summary>Serialized byte length of a sequence of messages.</summary>"); | |
| c.Line("public static int ArraySizeInBytes<T>(IEnumerable<T> items) where T : IMessage"); | |
| c.Indent(); | |
| c.Line("=> items?.Sum(i => i.GetLengthInBytes()) ?? 0;"); | |
| c.Outdent(); | |
| c.Line(); | |
| c.Line("public static byte BcdToBin(byte value) => (byte) ((value >> 4) * 10 + (value & 0x0F));"); | |
| c.Line("public static ushort BcdToBin12(ushort value) => (ushort) (((value >> 8) & 0x0F) * 100 + ((value >> 4) & 0x0F) * 10 + (value & 0x0F));"); | |
| c.Line("public static byte BinToBcd(int value) => (byte) ((value / 10) * 16 + (value % 10));"); | |
| c.Line("public static ushort BinToBcd12(int value) => (ushort) ((value / 100) * 256 + ((value / 10) % 10) * 16 + (value % 10));"); | |
| foreach (var (name, retType) in staticCalls) | |
| { | |
| if (!IsGeneratedStaticCall(name)) | |
| { | |
| continue; // supplied by the hand-written part of the partial class | |
| } | |
| c.Line(); | |
| EmitStaticCall(c, name, retType); | |
| } | |
| c.Outdent(); | |
| c.Line("}"); | |
| c.Outdent(); | |
| c.Line("}"); | |
| return c.ToString(); | |
| } | |
| private static string RenderEnumUnderlyingValue(EnumValue value, CSharpExpressionRenderer r) => | |
| value.Value == null ? value.Name : r.Render(value.Value); | |
| private void EmitStaticCall(CodeWriter c, string name, string retType) | |
| { | |
| if (_protocolName.Equals("modbus", StringComparison.OrdinalIgnoreCase) | |
| && name == "AsciiLrcCheck") | |
| { | |
| c.Line("public static ushort AsciiLrcCheck(byte address, IMessage pdu)"); | |
| c.Line("{"); | |
| c.Indent(); | |
| c.Line("var buffer = new WriteBuffer();"); | |
| c.Line("buffer.WriteByte(\"address\", 8, address);"); | |
| c.Line("pdu.Serialize(buffer);"); | |
| c.Line("var sum = 0;"); | |
| c.Line("foreach (var b in buffer.GetBytes()) sum = (sum + b) & 0xFF;"); | |
| c.Line("return (ushort) ((-sum) & 0xFF);"); | |
| c.Outdent(); | |
| c.Line("}"); | |
| return; | |
| } | |
| if (_protocolName.Equals("modbus", StringComparison.OrdinalIgnoreCase) | |
| && name == "RtuCrcCheck") | |
| { | |
| c.Line("public static ushort RtuCrcCheck(byte address, IMessage pdu)"); | |
| c.Line("{"); | |
| c.Indent(); | |
| c.Line("var buffer = new WriteBuffer();"); | |
| c.Line("buffer.WriteByte(\"address\", 8, address);"); | |
| c.Line("pdu.Serialize(buffer);"); | |
| c.Line("ushort crc = 0xFFFF;"); | |
| c.Line("foreach (var b in buffer.GetBytes())"); | |
| c.Line("{"); | |
| c.Indent(); | |
| c.Line("crc ^= b;"); | |
| c.Line("for (var bit = 0; bit < 8; bit++) crc = (crc & 1) != 0 ? (ushort) ((crc >> 1) ^ 0xA001) : (ushort) (crc >> 1);"); | |
| c.Outdent(); | |
| c.Line("}"); | |
| // ReadBuffer/WriteBuffer are MSB-first. Modbus RTU transports | |
| // the low CRC byte first, so return the swapped 16-bit value. | |
| c.Line("return (ushort) ((crc << 8) | (crc >> 8));"); | |
| c.Outdent(); | |
| c.Line("}"); | |
| return; | |
| } | |
| throw new InvalidOperationException($"No C# implementation is registered for STATIC_CALL '{name}' ({retType})."); | |
| } | |
| private void CollectStaticCalls(IDictionary<string, string> sink) | |
| { | |
| foreach (var type in _protocol.Types) | |
| { | |
| foreach (var f in type.Fields) | |
| { | |
| var retType = f.Type != null ? CSharpTypeMapper.CSharpType(f.Type) : "object"; | |
| foreach (var term in FieldTerms(f)) | |
| { | |
| WalkStaticCalls(term, retType, sink); | |
| } | |
| } | |
| } | |
| foreach (var dio in _protocol.DataIos) | |
| { | |
| foreach (var c in dio.Cases) | |
| { | |
| foreach (var f in c.Fields) | |
| { | |
| var retType = f.Type != null ? CSharpTypeMapper.CSharpType(f.Type) : "object"; | |
| foreach (var term in FieldTerms(f)) | |
| { | |
| WalkStaticCalls(term, retType, sink); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| private bool IsGeneratedStaticCall(string name) => | |
| _protocolName.Equals("modbus", StringComparison.OrdinalIgnoreCase) | |
| && (name == "RtuCrcCheck" || name == "AsciiLrcCheck"); | |
| private static bool IsSupportedDataIoCase(DataIoTypeDefinition dio, ComplexTypeDefinition cs) | |
| { | |
| var props = cs.PropertyFields.ToList(); | |
| var valueField = props.Count == 1 ? props[0] : null; | |
| var listField = valueField as ArrayField; | |
| var temporal = TemporalPlcValueCases.Contains(cs.Name) | |
| && dio.Arguments.Any(a => a.Name == "dataProtocolId") | |
| && props.All(f => f is not ArrayField); | |
| var scalar = valueField is not (null or ArrayField) && ScalarPlcValueCases.Contains(cs.Name); | |
| var structCase = cs.Name == "Struct" | |
| && props.Count > 0 | |
| && props.All(f => f is SimpleField { Type: SimpleTypeReference } | |
| || f is ArrayField | |
| { | |
| Type: SimpleTypeReference { BaseType: SimpleTypeReference.Base.Byte }, | |
| LoopType: ArrayField.Loop.Count, | |
| }); | |
| return temporal || scalar || structCase | |
| || listField?.Type is SimpleTypeReference && listField.LoopType == ArrayField.Loop.Count; | |
| } | |
| private static void WalkStaticCalls(Term t, string retType, IDictionary<string, string> sink) | |
| { | |
| switch (t) | |
| { | |
| case VariableLiteral { Name: "STATIC_CALL" } v | |
| when v.Args?.Count >= 1 && v.Args[0] is StringLiteral s: | |
| sink[Pascal(s.Value)] = retType; | |
| break; | |
| case VariableLiteral v: | |
| foreach (var a in v.Args ?? Enumerable.Empty<Term>()) | |
| { | |
| WalkStaticCalls(a, retType, sink); | |
| } | |
| if (v.Child != null) | |
| { | |
| WalkStaticCalls(v.Child, retType, sink); | |
| } | |
| break; | |
| case BinaryExpression b: | |
| WalkStaticCalls(b.Left, retType, sink); | |
| WalkStaticCalls(b.Right, retType, sink); | |
| break; | |
| case UnaryExpression u: | |
| WalkStaticCalls(u.Operand, retType, sink); | |
| break; | |
| case TernaryExpression tt: | |
| WalkStaticCalls(tt.Condition, retType, sink); | |
| WalkStaticCalls(tt.WhenTrue, retType, sink); | |
| WalkStaticCalls(tt.WhenFalse, retType, sink); | |
| break; | |
| } | |
| } | |
| private static IEnumerable<Term> FieldTerms(Field f) | |
| { | |
| switch (f) | |
| { | |
| case ImplicitField i: yield return i.SerializeExpression; break; | |
| case ChecksumField ck: yield return ck.ChecksumExpression; break; | |
| case ArrayField a: yield return a.LoopExpression; break; | |
| case VirtualField v: yield return v.ValueExpression; break; | |
| case ConstField c2: yield return c2.ReferenceValue; break; | |
| case ReservedField r: yield return r.ReferenceValue; break; | |
| case OptionalField o when o.Condition != null: yield return o.Condition; break; | |
| case ManualField m: | |
| yield return m.ParseExpression; | |
| yield return m.SerializeExpression; | |
| yield return m.LengthExpression; | |
| break; | |
| case PaddingField p: | |
| yield return p.PaddingValue; | |
| yield return p.TimesPadding; | |
| break; | |
| } | |
| } | |
| // ── helpers ───────────────────────────────────────────── | |
| private string ReadFieldValue(Field f, CSharpExpressionRenderer r) => | |
| f.Type is SimpleTypeReference { BaseType: SimpleTypeReference.Base.VString, LengthExpression: { } len } | |
| ? $"readBuffer.ReadString(\"{f.Name}\", (int) ({r.Render(len)}), {CSharpTypeMapper.EncodingExpr(VStringEncoding(f))})" | |
| : CSharpTypeMapper.ReadCall(f.Type, f.Name, ComplexArgs(f.Type, r)); | |
| private static string VStringEncoding(Field f) => | |
| (f.Type as SimpleTypeReference)?.Encoding | |
| ?? (f.Attributes.TryGetValue("encoding", out var e) && e is StringLiteral s ? s.Value : "UTF8"); | |
| /// <summary>Reads an <c>enum</c> field. With a key accessor | |
| /// (<c>[enum TransportSize transportSize code]</c>) the wire value is | |
| /// the key, mapped back through the generated reverse lookup.</summary> | |
| private string EnumFieldRead(EnumField ef, CSharpExpressionRenderer r) | |
| { | |
| if (ef.Type is EnumTypeReference et && ef.KeyAccessor != null) | |
| { | |
| var baseRead = CSharpTypeMapper.ReadCall(et.BaseType, ef.Name); | |
| return $"({et.Name}Extensions.FirstEnumForField{Pascal(ef.KeyAccessor)}({baseRead}) " | |
| + $"?? throw new ParseException(\"unrecognised {et.Name} {ef.KeyAccessor} on the wire\"))"; | |
| } | |
| return ReadFieldValue(ef, r); | |
| } | |
| private string EnumFieldWrite(EnumField ef) | |
| { | |
| if (ef.Type is EnumTypeReference et && ef.KeyAccessor != null) | |
| { | |
| var baseCs = CSharpTypeMapper.SimpleCSharpType(et.BaseType); | |
| var val = $"({baseCs}) {Pascal(ef.Name)}.Get{Pascal(ef.KeyAccessor)}()"; | |
| return CSharpTypeMapper.WriteCall(et.BaseType, ef.Name, val); | |
| } | |
| return CSharpTypeMapper.WriteCall(ef.Type, ef.Name, Pascal(ef.Name)); | |
| } | |
| /// <summary><see cref="ComplexArgs"/> plus the <c>_lastItem</c> flag when | |
| /// the array loop knows it.</summary> | |
| private string ElementArgs(TypeReference type, CSharpExpressionRenderer r, string? lastItem) | |
| { | |
| var args = ComplexArgs(type, r); | |
| if (lastItem == null) | |
| { | |
| return args; | |
| } | |
| return args.Length == 0 ? lastItem : $"{args}, {lastItem}"; | |
| } | |
| private string ComplexArgs(TypeReference? type, CSharpExpressionRenderer r) | |
| { | |
| if (type is not ComplexTypeReference c) | |
| { | |
| return ""; | |
| } | |
| // Cast each argument to the callee's declared parameter type: an | |
| // mspec count / position expression is `int`, but a `uint 8` | |
| // parameter is `byte`. | |
| var callee = _protocol.FindType(c.Name); | |
| return string.Join(", ", c.Arguments.Select((a, i) => | |
| { | |
| var rendered = r.Render(a); | |
| var pType = callee != null && i < callee.Arguments.Count | |
| ? callee.Arguments[i].Type | |
| : null; | |
| return pType != null ? WithType(rendered, pType) : rendered; | |
| })); | |
| } | |
| // An array field's element type is always given in the grammar. | |
| private static TypeReference ElementType(ArrayField af) => af.Type!; | |
| private string PropertyType(Field f) | |
| { | |
| if (f is ArrayField af) | |
| { | |
| var (t, _) = CSharpTypeMapper.ArrayType(af.Type); | |
| return t; | |
| } | |
| var t2 = CSharpTypeMapper.CSharpType(f.Type); | |
| // An optional field is absent when its condition is false. | |
| return f is OptionalField ? t2 + "?" : t2; | |
| } | |
| /// <summary>One switch discriminator, resolved against the parent.</summary> | |
| private sealed class Discriminator | |
| { | |
| /// <summary>The C# accessor name a child would override, or null | |
| /// when the value already lives in a property or is a dotted path | |
| /// (both cases: no accessor is generated).</summary> | |
| public string? AccessorName { get; init; } | |
| public required TypeReference Type { get; init; } | |
| /// <summary>The C# that reads the value during dispatch - a local | |
| /// (a discriminator or simple field the parent already read), a | |
| /// parser-argument name, or a rendered dotted path.</summary> | |
| public required string DispatchExpr { get; init; } | |
| } | |
| private List<Discriminator> DiscriminatorList( | |
| ComplexTypeDefinition parent, CSharpExpressionRenderer? render = null) | |
| { | |
| var ts = parent.TypeSwitch; | |
| var result = new List<Discriminator>(); | |
| if (ts == null) | |
| { | |
| return result; | |
| } | |
| foreach (var term in ts.Discriminators.OfType<VariableLiteral>()) | |
| { | |
| if (term.Child != null || term.IsCall) | |
| { | |
| // e.g. parameter.parameterType - used for dispatch, never an accessor. | |
| var r = render ?? new CSharpExpressionRenderer(new FieldScope(_protocol, parent)); | |
| result.Add(new Discriminator | |
| { | |
| AccessorName = null, | |
| Type = new SimpleTypeReference { BaseType = SimpleTypeReference.Base.UInt, SizeInBits = 8 }, | |
| DispatchExpr = r.Render(term), | |
| }); | |
| continue; | |
| } | |
| var name = term.Name; | |
| var field = parent.Fields.FirstOrDefault(x => x.Name == name); | |
| var arg = parent.Arguments.FirstOrDefault(a => a.Name == name); | |
| var type = field?.Type ?? arg?.Type | |
| ?? new SimpleTypeReference { BaseType = SimpleTypeReference.Base.UInt, SizeInBits = 8 }; | |
| result.Add(new Discriminator | |
| { | |
| // A property field carries the value itself; only a bare | |
| // `discriminator` / parser-arg gets an accessor. | |
| AccessorName = field is { IsProperty: true } or VirtualField ? null : name, | |
| Type = type, | |
| DispatchExpr = Camel(name), | |
| }); | |
| } | |
| return result; | |
| } | |
| /// <summary> | |
| /// The discriminators a case leaves unpinned that are fields of the parent, | |
| /// read from the wire (Modbus' <c>ModbusPDUError</c> pins <c>errorFlag</c> | |
| /// but not <c>functionFlag</c>). plc4j keeps the value as it was read, so | |
| /// the message serializes back to the same bytes; the child takes it as a | |
| /// trailing constructor and <c>StaticParse</c> parameter and returns it from | |
| /// the discriminator accessor. Without it the value would be lost, and | |
| /// anything computed from the serialized form - such as a checksum over the | |
| /// PDU - would disagree with the bytes that were received. | |
| /// A parser argument is not a wire field and is not retained. | |
| /// </summary> | |
| private List<(string Name, TypeReference Type)> RetainedDiscriminators(ComplexTypeDefinition child) | |
| { | |
| var result = new List<(string Name, TypeReference Type)>(); | |
| if (!child.IsDiscriminatedChild || _protocol.FindType(child.ParentName) is not { } parent) | |
| { | |
| return result; | |
| } | |
| var values = child.DiscriminatorValues ?? Array.Empty<Term>(); | |
| var discriminators = DiscriminatorList(parent); | |
| for (var i = 0; i < discriminators.Count; i++) | |
| { | |
| var d = discriminators[i]; | |
| if (d.AccessorName == null || (values.Count > i && values[i] != null)) | |
| { | |
| continue; | |
| } | |
| if (parent.Fields.OfType<DiscriminatorField>().Any(f => f.Name == d.AccessorName)) | |
| { | |
| result.Add((d.AccessorName, d.Type)); | |
| } | |
| } | |
| return result; | |
| } | |
| private IEnumerable<string> ParserArgDiscriminatorNames(ComplexTypeDefinition parent) => | |
| DiscriminatorList(parent) | |
| .Where(d => d.AccessorName != null | |
| && parent.Arguments.Any(a => a.Name == d.AccessorName) | |
| && parent.Fields.All(f => f.Name != d.AccessorName)) | |
| .Select(d => d.AccessorName!); | |
| private IReadOnlyList<Field> ParentPrefixFields(ComplexTypeDefinition child) => | |
| child.ParentName != null && _protocol.FindType(child.ParentName) is { } p | |
| ? p.PrefixPropertyFields.ToList() | |
| : Array.Empty<Field>(); | |
| private IReadOnlyList<Field> ParentSuffixFields(ComplexTypeDefinition child) => | |
| child.ParentName != null && _protocol.FindType(child.ParentName) is { } p | |
| ? p.SuffixPropertyFields.ToList() | |
| : Array.Empty<Field>(); | |
| /// <summary>The parent's pre-typeSwitch value-carrying fields that this | |
| /// child actually names in one of its own or trailing-field | |
| /// expressions - the child's <c>StaticParse</c> takes these as extra | |
| /// parameters so those expressions resolve. Filtered to what is | |
| /// referenced so an unrelated protocol (Modbus) gets no spurious | |
| /// parameters.</summary> | |
| private IReadOnlyList<Field> ParentContextFields(ComplexTypeDefinition child) | |
| { | |
| if (child.ParentName == null || _protocol.FindType(child.ParentName) is not { } p) | |
| { | |
| return Array.Empty<Field>(); | |
| } | |
| var referenced = new HashSet<string>(); | |
| foreach (var f in child.Fields.Concat(p.FieldsAfterSwitch)) | |
| { | |
| foreach (var t in AllFieldTerms(f)) | |
| { | |
| CollectNames(t, referenced); | |
| } | |
| } | |
| // Prefix property fields are always needed - the child's constructor | |
| // takes them (see AllValueFields). Prefix discriminators / implicits | |
| // / consts come along only when an expression names them. | |
| return p.PrefixContextFields | |
| .Where(f => f.IsProperty || referenced.Contains(f.Name)) | |
| .ToList(); | |
| } | |
| private static IEnumerable<Term> AllFieldTerms(Field f) | |
| { | |
| foreach (var t in FieldTerms(f)) | |
| { | |
| yield return t; | |
| } | |
| if (f.Type is ComplexTypeReference c) | |
| { | |
| foreach (var a in c.Arguments) | |
| { | |
| yield return a; | |
| } | |
| } | |
| } | |
| private static void CollectNames(Term? t, HashSet<string> sink) | |
| { | |
| switch (t) | |
| { | |
| case VariableLiteral v: | |
| if (v.Name != null) | |
| { | |
| sink.Add(v.Name); | |
| } | |
| foreach (var a in v.Args ?? Enumerable.Empty<Term>()) | |
| { | |
| CollectNames(a, sink); | |
| } | |
| foreach (var ix in v.Index ?? Enumerable.Empty<Term>()) | |
| { | |
| CollectNames(ix, sink); | |
| } | |
| CollectNames(v.Child, sink); | |
| break; | |
| case BinaryExpression b: | |
| CollectNames(b.Left, sink); | |
| CollectNames(b.Right, sink); | |
| break; | |
| case UnaryExpression u: | |
| CollectNames(u.Operand, sink); | |
| break; | |
| case TernaryExpression tt: | |
| CollectNames(tt.Condition, sink); | |
| CollectNames(tt.WhenTrue, sink); | |
| CollectNames(tt.WhenFalse, sink); | |
| break; | |
| } | |
| } | |
| /// <summary>Constructor parameters and property fields for a type, in | |
| /// wire order: inherited prefix, own, inherited suffix.</summary> | |
| private List<Field> AllValueFields(ComplexTypeDefinition type) => | |
| ParentPrefixFields(type) | |
| .Concat(type.PropertyFields) | |
| .Concat(ParentSuffixFields(type)) | |
| .ToList(); | |
| /// <summary>The fields this type's own parse / serialize / length | |
| /// covers. A discriminated parent stops at the typeSwitch; a child | |
| /// picks up the parent's trailing fields after its own body.</summary> | |
| private IReadOnlyList<Field> FieldsToEmit(ComplexTypeDefinition type) | |
| { | |
| if (type.IsDiscriminatedParent) | |
| { | |
| return type.FieldsThroughSwitch.ToList(); | |
| } | |
| if (type.IsDiscriminatedChild && _protocol.FindType(type.ParentName) is { } p) | |
| { | |
| return type.Fields.Concat(p.FieldsAfterSwitch).ToList(); | |
| } | |
| return type.Fields; | |
| } | |
| private string RenderDiscriminatorValue(Term? value, TypeReference? dType, CSharpExpressionRenderer r) | |
| { | |
| if (dType is EnumTypeReference e && value is VariableLiteral v && v.Child == null && !v.IsCall) | |
| { | |
| return $"{e.Name}.{v.Name}"; | |
| } | |
| return WithType(r.Render(value), dType); | |
| } | |
| private static string WithType(string rendered, TypeReference? type) | |
| { | |
| if (type is SimpleTypeReference s && s.IsIntegerLike && s.BaseType != SimpleTypeReference.Base.Bit) | |
| { | |
| return $"({CSharpTypeMapper.SimpleCSharpType(s)}) ({rendered})"; | |
| } | |
| return rendered; | |
| } | |
| private List<string> ParseSignatureArgs(ComplexTypeDefinition type) | |
| { | |
| // A child inherits the parent's parser arguments, and also takes | |
| // the parent's prefix fields (read by the parent, passed down so | |
| // the child can build a complete object). | |
| // A discriminated child's own ParentName always resolves - the | |
| // builder only sets it while lifting the child out of its parent's | |
| // typeSwitch. | |
| var owner = type.IsDiscriminatedChild | |
| ? _protocol.FindType(type.ParentName) | |
| ?? throw new InvalidOperationException($"Type '{type.ParentName}' (parent of '{type.Name}') was not found.") | |
| : type; | |
| var args = owner.Arguments | |
| .Select(a => $"{CSharpTypeMapper.CSharpType(a.Type)} {Camel(a.Name)}") | |
| .ToList(); | |
| args.AddRange(ParentContextFields(type) | |
| .Select(f => $"{PropertyType(f)} {Camel(f.Name)}")); | |
| if (type.IsDiscriminatedChild && UsesCurPos(type)) | |
| { | |
| args.Add("int _startPos"); | |
| } | |
| args.AddRange(RetainedDiscriminators(type) | |
| .Select(d => $"{CSharpTypeMapper.CSharpType(d.Type)} {Camel(d.Name)}")); | |
| if (UsesLastItem(type)) | |
| { | |
| args.Add("bool _lastItem = false"); | |
| } | |
| return args; | |
| } | |
| /// <summary>Whether an expression in this type reads <c>_lastItem</c> - | |
| /// true only for a type used as an array element whose last element is | |
| /// laid out differently (S7's <c>S7VarPayloadDataItem</c> padding).</summary> | |
| private bool UsesLastItem(ComplexTypeDefinition type) | |
| { | |
| var names = new HashSet<string>(); | |
| foreach (var f in FieldsToEmit(type)) | |
| { | |
| foreach (var t in AllFieldTerms(f)) | |
| { | |
| CollectNames(t, names); | |
| } | |
| } | |
| return names.Contains("_lastItem"); | |
| } | |
| /// <summary>Whether <paramref name="af"/>'s element type threads | |
| /// <c>_lastItem</c> - the loop then has to pass the per-iteration flag | |
| /// and use an index-aware serialize / length.</summary> | |
| private bool ElementUsesLastItem(ArrayField af) => | |
| ElementType(af) is ComplexTypeReference c | |
| && _protocol.FindType(c.Name) is { } et | |
| && UsesLastItem(et); | |
| /// <summary>Whether this type's own parse emits a field whose expression | |
| /// reads <c>curPos</c> - it then needs <c>_startPos</c> in scope.</summary> | |
| private bool UsesCurPos(ComplexTypeDefinition type) | |
| { | |
| var names = new HashSet<string>(); | |
| foreach (var f in FieldsToEmit(type)) | |
| { | |
| foreach (var t in AllFieldTerms(f)) | |
| { | |
| CollectNames(t, names); | |
| } | |
| } | |
| return names.Contains("curPos") || names.Contains("_curPos"); | |
| } | |
| /// <summary>A discriminated parent that itself does not read | |
| /// <c>curPos</c> still captures <c>_startPos</c> when a child needs it, | |
| /// to pass down.</summary> | |
| private bool NeedsStartPos(ComplexTypeDefinition type) => | |
| UsesCurPos(type) | |
| || type.IsDiscriminatedParent | |
| && type.TypeSwitch is { } ts | |
| && ts.CaseNames | |
| .Select(_protocol.FindType) | |
| .OfType<ComplexTypeDefinition>() | |
| .Any(UsesCurPos); | |
| private IEnumerable<string> ChildParseArgNames(ComplexTypeDefinition child) | |
| { | |
| // Every discriminated child's ParentName resolves - see the | |
| // comment in ParseSignatureArgs. | |
| var parent = _protocol.FindType(child.ParentName) | |
| ?? throw new InvalidOperationException($"Type '{child.ParentName}' (parent of '{child.Name}') was not found."); | |
| return parent.Arguments.Select(a => Camel(a.Name)); | |
| } | |
| private static string Comma(IEnumerable<string> parts) | |
| { | |
| var list = parts.ToList(); | |
| return list.Count == 0 ? "" : ", " + string.Join(", ", list); | |
| } | |
| private static string Comma(string s) => string.IsNullOrEmpty(s) ? "" : ", " + s; | |
| private void Header(CodeWriter c) | |
| { | |
| foreach (var line in LicenseHeader) | |
| { | |
| c.Line(line); | |
| } | |
| c.Line(); | |
| c.Line("// Code generated from the mspec by plc4net-code-gen. DO NOT EDIT."); | |
| c.Line(); | |
| } | |
| private static readonly string[] LicenseHeader = | |
| { | |
| "//", | |
| "// Licensed to the Apache Software Foundation (ASF) under one", | |
| "// or more contributor license agreements. See the NOTICE file", | |
| "// distributed with this work for additional information", | |
| "// regarding copyright ownership. The ASF licenses this file", | |
| "// to you under the Apache License, Version 2.0 (the", | |
| "// \"License\"); you may not use this file except in compliance", | |
| "// with the License. You may obtain a copy of the License at", | |
| "//", | |
| "// https://www.apache.org/licenses/LICENSE-2.0", | |
| "//", | |
| "// Unless required by applicable law or agreed to in writing,", | |
| "// software distributed under the License is distributed on an", | |
| "// \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY", | |
| "// KIND, either express or implied. See the License for the", | |
| "// specific language governing permissions and limitations", | |
| "// under the License.", | |
| "//", | |
| }; | |
| internal static string Pascal(string s) => | |
| string.IsNullOrEmpty(s) ? s : char.ToUpperInvariant(s[0]) + s.Substring(1); | |
| internal static string Camel(string s) => | |
| string.IsNullOrEmpty(s) ? s : char.ToLowerInvariant(s[0]) + s.Substring(1); | |
| } | |
| } |