| // |
| // 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; |
| |
| public CSharpGenerator(Protocol protocol, string protocolName, string namespaceName) |
| { |
| _protocol = protocol; |
| _protocolName = protocolName; |
| _namespace = namespaceName; |
| } |
| |
| /// <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 non-big-endian |
| /// <c>byteOrder</c> (no C# read/write path emits the swaps yet) 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.Cast<TypeDefinition>() |
| .Concat<TypeDefinition>(_protocol.Enums) |
| .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 not implemented yet"); |
| } |
| } |
| |
| 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 (!IsGeneratedStaticCall(name)) |
| { |
| throw new NotSupportedException( |
| $"{_protocolName}.mspec uses STATIC_CALL '{name}', but no C# implementation is available"); |
| } |
| } |
| } |
| |
| 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 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 |
| } |
| var pinned = values.Count > i && values[i] != null; |
| // An unpinned discriminator (the case listed fewer values, |
| // or a null slot) takes the type's default, matching plc4j: |
| // ModbusPDUError leaves functionFlag / response unspecified |
| // and its accessors return 0 / false. |
| 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); |
| if (all.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)}")); |
| 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)};"); |
| } |
| c.Outdent(); |
| c.Line("}"); |
| c.Line(); |
| } |
| |
| // ── StaticParse ───────────────────────────────────────── |
| |
| 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 |
| ? "new " |
| : ""; |
| c.Line($"public static {hide}{type.Name} StaticParse(ReadBuffer readBuffer{Comma(args)})"); |
| 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))); |
| c.Line($"return new {type.Name}({ctorArgs});"); |
| } |
| |
| 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"); |
| } |
| 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(); |
| |
| 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);"); |
| } |
| |
| 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) |
| { |
| 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; |
| } |
| |
| 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"); |
| } |
| 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); |
| } |
| } |