| // | |
| // 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.Collections.Generic; | |
| using System.Linq; | |
| using Antlr4.Runtime; | |
| using Antlr4.Runtime.Misc; | |
| using org.apache.plc4net.tools.codegen.grammar; | |
| 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 | |
| { | |
| /// <summary> | |
| /// Turns a parsed mspec file into the type-model IR | |
| /// (<see cref="Protocol"/>). Walks the ANTLR parse tree with the generated | |
| /// typed context classes; expressions inside ticks are handed to | |
| /// <see cref="MspecExpressionParser"/>. | |
| /// </summary> | |
| public sealed class MspecModelBuilder | |
| { | |
| private readonly Protocol _protocol = new Protocol(); | |
| /// <summary>Set by <c>BuildFiles</c> so diagnostics and errors can | |
| /// name the mspec file they come from.</summary> | |
| private string? _currentFile; | |
| public static Protocol Build(string mspecContent) | |
| => new MspecModelBuilder().Run(MspecReader.Read(mspecContent)); | |
| public static Protocol BuildFile(string path) | |
| => BuildFiles(path); | |
| /// <summary> | |
| /// Builds one model from several mspec files - a protocol whose types | |
| /// are split across files (knxnetip.mspec references | |
| /// <c>KnxPropertyDataType</c> from device-info.mspec) is compiled the | |
| /// way the Java plugin does it: every <c>*.mspec</c> in the directory | |
| /// as one unit. | |
| /// </summary> | |
| public static Protocol BuildFiles(params string[] paths) => BuildFiles(true, paths); | |
| public static Protocol BuildFiles(bool strict, params string[] paths) | |
| { | |
| var builder = new MspecModelBuilder(); | |
| foreach (var path in paths) | |
| { | |
| builder._currentFile = path; | |
| builder.RunInto(MspecReader.ReadFile(path, strict)); | |
| } | |
| builder.ResolveEnumReferences(); | |
| return builder._protocol; | |
| } | |
| private Protocol Run(MSpecParser.FileContext file) | |
| { | |
| RunInto(file); | |
| ResolveEnumReferences(); | |
| return _protocol; | |
| } | |
| private void RunInto(MSpecParser.FileContext file) | |
| { | |
| var constants = file.contantsDefinition(); | |
| if (constants != null) | |
| { | |
| foreach (var cf in constants.constField()) | |
| { | |
| _protocol.Constants.Add(new ConstantDeclaration | |
| { | |
| Name = cf.name.GetText(), | |
| // A const's type token is mandatory in the grammar. | |
| Type = BuildTypeReference(cf.type)!, | |
| Value = ParseValueLiteral(cf.expected), | |
| }); | |
| } | |
| } | |
| foreach (var def in file.complexTypeDefinition()) | |
| { | |
| BuildComplexTypeDefinition(def.complexType()); | |
| } | |
| } | |
| // ── type definitions ───────────────────────────────────── | |
| private void BuildComplexTypeDefinition(MSpecParser.ComplexTypeContext ctx) | |
| { | |
| if (ctx.ENUM() != null) | |
| { | |
| BuildEnum(ctx); | |
| return; | |
| } | |
| if (ctx.DATAIO() != null) | |
| { | |
| BuildDataIo(ctx); | |
| return; | |
| } | |
| var type = new ComplexTypeDefinition { Name = ctx.name.GetText() }; | |
| FillArguments(ctx.argumentList(), type.Arguments); | |
| FillAttributes(ctx.attributeList(), type.Attributes); | |
| foreach (var fd in ctx.fieldDefinition()) | |
| { | |
| AddField(type, fd); | |
| } | |
| AddTypeDefinition(type); | |
| } | |
| private void BuildEnum(MSpecParser.ComplexTypeContext ctx) | |
| { | |
| var e = new EnumTypeDefinition | |
| { | |
| Name = ctx.name.GetText(), | |
| BaseType = ctx.dataType() != null ? BuildSimpleType(ctx.dataType()) : null, | |
| }; | |
| FillArguments(ctx.argumentList(), e.Arguments); | |
| FillAttributes(ctx.attributeList(), e.Attributes); | |
| foreach (var v in ctx.enumValueDefinition()) | |
| { | |
| e.Values.Add(new EnumValue | |
| { | |
| Name = v.name.Text, | |
| Value = v.valueExpression != null ? ParseExpression(v.valueExpression) : null, | |
| // Every element here comes from a real expression node in | |
| // the constant-value list, never from an absent optional. | |
| ConstantValues = v.constantValueExpressions != null | |
| ? v.constantValueExpressions.expression().Select(e => ParseExpression(e)!).ToList() | |
| : (IReadOnlyList<Term>)System.Array.Empty<Term>(), | |
| }); | |
| } | |
| AddTypeDefinition(e); | |
| } | |
| private void BuildDataIo(MSpecParser.ComplexTypeContext ctx) | |
| { | |
| var dio = new DataIoTypeDefinition { Name = ctx.name.GetText() }; | |
| FillArguments(ctx.argumentList(), dio.Arguments); | |
| FillAttributes(ctx.attributeList(), dio.Attributes); | |
| var tsCtx = ctx.dataIoDefinition()?.typeSwitchField(); | |
| if (tsCtx != null) | |
| { | |
| // dataIo case sub-types are not lifted to top-level types: the | |
| // names repeat (CHAR / STRING / TIME each appear twice) and a | |
| // dataIo parses to an IPlcValue, not a generated class. They are | |
| // kept on the DataIoTypeDefinition for the emitter. | |
| dio.TypeSwitch = BuildTypeSwitch(tsCtx, dio.Name, dio.Cases); | |
| } | |
| AddTypeDefinition(dio); | |
| } | |
| // ── fields ─────────────────────────────────────────────── | |
| private void AddField(ComplexTypeDefinition owner, MSpecParser.FieldDefinitionContext fd) | |
| { | |
| var f = fd.field(); | |
| Field field = f switch | |
| { | |
| _ when f.simpleField() is { } s => new SimpleField | |
| { | |
| Name = s.name.GetText(), | |
| Type = BuildTypeReference(s.type), | |
| }, | |
| _ when f.constField() is { } c => new ConstField | |
| { | |
| Name = c.name.GetText(), | |
| Type = BuildTypeReference(c.type), | |
| ReferenceValue = ParseValueLiteral(c.expected), | |
| }, | |
| _ when f.implicitField() is { } im => new ImplicitField | |
| { | |
| Name = im.name.GetText(), | |
| Type = BuildSimpleType(im.type), | |
| // The serialize expression is mandatory in the grammar. | |
| SerializeExpression = ParseExpression(im.serializeExpression)!, | |
| }, | |
| _ when f.reservedField() is { } r => new ReservedField | |
| { | |
| Name = "reserved", | |
| Type = BuildSimpleType(r.type), | |
| // The reserved value literal is mandatory in the grammar. | |
| ReferenceValue = ParseExpression(r.expected)!, | |
| }, | |
| _ when f.discriminatorField() is { } d => new DiscriminatorField | |
| { | |
| Name = d.name.GetText(), | |
| Type = BuildTypeReference(d.type), | |
| }, | |
| _ when f.enumField() is { } en => new EnumField | |
| { | |
| Name = en.name.GetText(), | |
| Type = BuildTypeReference(en.type), | |
| KeyAccessor = en.fieldName?.GetText(), | |
| }, | |
| _ when f.arrayField() is { } a => new ArrayField | |
| { | |
| Name = a.name.GetText(), | |
| Type = BuildTypeReference(a.type), | |
| LoopType = a.loopType.Text.Trim('\'') switch | |
| { | |
| "length" => ArrayField.Loop.Length, | |
| "terminated" => ArrayField.Loop.Terminated, | |
| _ => ArrayField.Loop.Count, | |
| }, | |
| // The loop (count / length / terminator) expression is | |
| // mandatory in the grammar. | |
| LoopExpression = ParseExpression(a.loopExpression)!, | |
| }, | |
| _ when f.checksumField() is { } ck => new ChecksumField | |
| { | |
| Name = ck.name.GetText(), | |
| Type = BuildSimpleType(ck.type), | |
| ChecksumExpression = ParseExpression(ck.checksumExpression)!, | |
| }, | |
| _ when f.virtualField() is { } vf => new VirtualField | |
| { | |
| Name = vf.name.GetText(), | |
| Type = BuildTypeReference(vf.type), | |
| ValueExpression = ParseExpression(vf.valueExpression)!, | |
| }, | |
| _ when f.optionalField() is { } of => new OptionalField | |
| { | |
| Name = of.name.GetText(), | |
| Type = BuildTypeReference(of.type), | |
| Condition = of.condition != null ? ParseExpression(of.condition) : null, | |
| }, | |
| // All three manual expressions are mandatory in the grammar. | |
| _ when f.manualField() is { } mf => new ManualField | |
| { | |
| Name = mf.name.GetText(), | |
| Type = BuildTypeReference(mf.type), | |
| ParseExpression = ParseExpression(mf.parseExpression)!, | |
| SerializeExpression = ParseExpression(mf.serializeExpression)!, | |
| LengthExpression = ParseExpression(mf.lengthExpression)!, | |
| }, | |
| // Both padding expressions are mandatory in the grammar. | |
| _ when f.paddingField() is { } pf => new PaddingField | |
| { | |
| Name = pf.name.GetText(), | |
| Type = BuildSimpleType(pf.type), | |
| PaddingValue = ParseExpression(pf.paddingValue)!, | |
| TimesPadding = ParseExpression(pf.timesPadding)!, | |
| }, | |
| _ when f.typeSwitchField() is { } ts => BuildTypeSwitch(ts, owner.Name, _protocol.Types), | |
| // The field context always has at least the keyword token as | |
| // its first child. | |
| _ => new UnsupportedField | |
| { | |
| Name = "unsupported", | |
| MspecKeyword = f.GetChild(0)?.GetText() ?? f.GetType().Name, | |
| RawText = SourceText(f), | |
| }, | |
| }; | |
| FillAttributes(fd.attributeList(), field.Attributes); | |
| ApplyStringEncoding(field); | |
| owner.Fields.Add(field); | |
| } | |
| private TypeSwitchField BuildTypeSwitch( | |
| MSpecParser.TypeSwitchFieldContext ts, string parentName, | |
| List<ComplexTypeDefinition> childSink) | |
| { | |
| var field = new TypeSwitchField | |
| { | |
| Name = "typeSwitch", | |
| Discriminators = ts.multipleVariableLiterals().variableLiteral() | |
| .Select(v => | |
| { | |
| try { return MspecExpressionParser.Parse(SourceText(v)); } | |
| catch (MspecParseException) { return new VariableLiteral(SourceText(v)); } | |
| }) | |
| .ToList(), | |
| }; | |
| foreach (var cs in ts.caseStatement()) | |
| { | |
| var child = new ComplexTypeDefinition | |
| { | |
| Name = cs.name.Text, | |
| ParentName = parentName, | |
| DiscriminatorValues = cs.discriminatorValues != null | |
| ? cs.discriminatorValues.expression().Select(ParseExpression).ToList() | |
| : (IReadOnlyList<Term?>)System.Array.Empty<Term>(), | |
| }; | |
| FillArguments(cs.argumentList(), child.Arguments); | |
| foreach (var fd in cs.fieldDefinition()) | |
| { | |
| AddField(child, fd); | |
| } | |
| field.CaseNames.Add(child.Name); | |
| if (ReferenceEquals(childSink, _protocol.Types)) | |
| { | |
| // A lifted child becomes its own output file; a name | |
| // shared with any other type would silently collide. | |
| AddTypeDefinition(child); | |
| } | |
| else | |
| { | |
| // dataIo cases stay local and may repeat names. | |
| childSink.Add(child); | |
| } | |
| } | |
| return field; | |
| } | |
| // ── type references ────────────────────────────────────── | |
| private TypeReference? BuildTypeReference(MSpecParser.TypeReferenceContext ctx) | |
| { | |
| if (ctx == null) | |
| { | |
| return null; | |
| } | |
| if (ctx.simpleTypeReference != null) | |
| { | |
| return BuildSimpleType(ctx.simpleTypeReference); | |
| } | |
| return new ComplexTypeReference | |
| { | |
| Name = ctx.complexTypeReference.Text, | |
| // Every element here is a real parser-argument expression, | |
| // never from an absent optional. | |
| Arguments = ctx.@params != null | |
| ? ctx.@params.expression().Select(e => ParseExpression(e)!).ToList() | |
| : (IReadOnlyList<Term>)System.Array.Empty<Term>(), | |
| }; | |
| } | |
| /// <summary>Registers a type / enum / dataIo, rejecting a name that | |
| /// already exists anywhere in the model: output files are keyed by | |
| /// type name, so a second definition would silently win the file.</summary> | |
| private void AddTypeDefinition(TypeDefinition definition) | |
| { | |
| var clash = _protocol.Types.FirstOrDefault(t => t.Name == definition.Name) | |
| ?? (TypeDefinition?)_protocol.Enums.FirstOrDefault(e => e.Name == definition.Name) | |
| ?? _protocol.DataIos.FirstOrDefault(d => d.Name == definition.Name); | |
| if (clash != null) | |
| { | |
| throw new System.InvalidOperationException( | |
| $"duplicate type name '{definition.Name}' in {_currentFile ?? "<unknown file>"}; " | |
| + "output is keyed by type name, so the second definition would overwrite the first"); | |
| } | |
| switch (definition) | |
| { | |
| case EnumTypeDefinition e: | |
| _protocol.Enums.Add(e); | |
| break; | |
| case DataIoTypeDefinition d: | |
| _protocol.DataIos.Add(d); | |
| break; | |
| default: | |
| _protocol.Types.Add((ComplexTypeDefinition)definition); | |
| break; | |
| } | |
| } | |
| private SimpleTypeReference BuildSimpleType(MSpecParser.DataTypeContext dt) | |
| { | |
| var baseText = dt.@base.Text; | |
| var size = dt.size != null | |
| ? int.Parse(dt.size.Text, System.Globalization.CultureInfo.InvariantCulture) | |
| : 0; | |
| var (baseType, bits) = baseText switch | |
| { | |
| "bit" => (SimpleTypeReference.Base.Bit, 1), | |
| "byte" => (SimpleTypeReference.Base.Byte, 8), | |
| "uint" => (SimpleTypeReference.Base.UInt, size), | |
| "int" => (SimpleTypeReference.Base.Int, size), | |
| "float" => (SimpleTypeReference.Base.Float, size), | |
| "ufloat" => (SimpleTypeReference.Base.UFloat, size), | |
| "string" => (SimpleTypeReference.Base.String, size), | |
| "vstring" => (SimpleTypeReference.Base.VString, size), | |
| "time" => (SimpleTypeReference.Base.Time, 32), | |
| "date" => (SimpleTypeReference.Base.Date, 32), | |
| "dateTime" => (SimpleTypeReference.Base.DateTime, 64), | |
| _ => throw new System.NotSupportedException( | |
| $"base type '{baseText}' is not modelled (vint/vuint have no C# mapping yet)"), | |
| }; | |
| return new SimpleTypeReference | |
| { | |
| BaseType = baseType, | |
| SizeInBits = bits, | |
| // `vstring 'expr'` - the length in bits is a run-time expression | |
| // (ADS: `vstring 'stringLength * 8' value`). | |
| LengthExpression = baseType == SimpleTypeReference.Base.VString && dt.length != null | |
| ? ParseExpression(dt.length) | |
| : null, | |
| }; | |
| } | |
| /// <summary>Promotes a <see cref="ComplexTypeReference"/> to an | |
| /// <see cref="EnumTypeReference"/> once every enum name is known.</summary> | |
| private void ResolveEnumReferences() | |
| { | |
| var enums = _protocol.Enums.ToDictionary(e => e.Name); | |
| EnumTypeReference? Promote(ComplexTypeReference c) => | |
| enums.TryGetValue(c.Name, out var e) | |
| ? new EnumTypeReference | |
| { | |
| Name = e.Name, | |
| BaseType = e.BaseType | |
| ?? new SimpleTypeReference { BaseType = SimpleTypeReference.Base.UInt, SizeInBits = 8 }, | |
| } | |
| : null; | |
| void Fix(System.Func<TypeReference> get, System.Action<TypeReference> set) | |
| { | |
| if (get() is ComplexTypeReference c && Promote(c) is { } er) | |
| { | |
| set(er); | |
| } | |
| } | |
| foreach (var t in _protocol.Types) | |
| { | |
| foreach (var a in t.Arguments) | |
| { | |
| Fix(() => a.Type, v => a.Type = v); | |
| } | |
| // Field.Type is nullable (null for typeSwitch), unlike | |
| // Argument.Type above, so it is handled separately rather | |
| // than widening Fix's own delegate types for one caller. | |
| foreach (var f in t.Fields) | |
| { | |
| if (f.Type is ComplexTypeReference fc && Promote(fc) is { } fer) | |
| { | |
| f.Type = fer; | |
| } | |
| } | |
| } | |
| foreach (var en in _protocol.Enums) | |
| { | |
| foreach (var a in en.Arguments) | |
| { | |
| Fix(() => a.Type, v => a.Type = v); | |
| } | |
| } | |
| foreach (var dio in _protocol.DataIos) | |
| { | |
| foreach (var a in dio.Arguments) | |
| { | |
| Fix(() => a.Type, v => a.Type = v); | |
| } | |
| } | |
| foreach (var c in _protocol.Constants) | |
| { | |
| Fix(() => c.Type, v => c.Type = v); | |
| } | |
| } | |
| // ── expressions, attributes, arguments ─────────────────── | |
| private Term? ParseExpression(MSpecParser.ExpressionContext exprCtx) | |
| { | |
| if (exprCtx == null) | |
| { | |
| return null; | |
| } | |
| var raw = SourceText(exprCtx).Trim(); | |
| if (raw == "*") | |
| { | |
| return new VariableLiteral("*"); | |
| } | |
| var text = raw.Length >= 2 && raw[0] == '\'' && raw[^1] == '\'' | |
| ? raw.Substring(1, raw.Length - 2) | |
| : raw; | |
| try | |
| { | |
| return MspecExpressionParser.Parse(text); | |
| } | |
| catch (MspecParseException) | |
| { | |
| // Expression positions (count/length/serialize/...) never | |
| // legitimately hold prose, so a failure here is a broken | |
| // mspec; record it rather than silently emitting a string | |
| // literal that breaks generated code far from the cause. | |
| _protocol.Diagnostics.Add( | |
| $"{_currentFile ?? "<model>"}: expression '{text}' did not parse; kept as a string literal"); | |
| return new StringLiteral(text); | |
| } | |
| } | |
| private Term ParseValueLiteral(MSpecParser.ValueLiteralContext ctx) | |
| { | |
| var text = SourceText(ctx).Trim(); | |
| try | |
| { | |
| return MspecExpressionParser.Parse(text.Trim('\'')); | |
| } | |
| catch (MspecParseException) | |
| { | |
| // Deliberate: value-table cells legitimately hold quoted | |
| // strings that are not expressions (KNX's 'DPST-1-1'). | |
| return new StringLiteral(text); | |
| } | |
| } | |
| private void FillArguments(MSpecParser.ArgumentListContext list, List<Argument> target) | |
| { | |
| if (list == null) | |
| { | |
| return; | |
| } | |
| foreach (var a in list.argument()) | |
| { | |
| target.Add(new Argument | |
| { | |
| Name = a.name.GetText(), | |
| // A parser argument's type token is mandatory in the grammar. | |
| Type = BuildTypeReference(a.type)!, | |
| }); | |
| } | |
| } | |
| private void FillAttributes( | |
| MSpecParser.AttributeListContext list, Dictionary<string, Term> target) | |
| { | |
| if (list == null) | |
| { | |
| return; | |
| } | |
| foreach (var attr in list.attribute()) | |
| { | |
| // An attribute's value is mandatory in the grammar (name=value). | |
| target[attr.name.Text] = ParseExpression(attr.value)!; | |
| } | |
| } | |
| private static void ApplyStringEncoding(Field field) | |
| { | |
| if (field.Type is SimpleTypeReference s | |
| && field.Attributes.TryGetValue("stringEncoding", out var enc) | |
| && enc is StringLiteral sl) | |
| { | |
| s.Encoding = sl.Value; | |
| } | |
| } | |
| private static string SourceText(ParserRuleContext ctx) => | |
| ctx.Start.InputStream.GetText(Interval.Of(ctx.Start.StartIndex, ctx.Stop.StopIndex)); | |
| } | |
| } |