blob: 945784abecc183d8d486800670ba0a057cc5f016 [file]
//
// 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));
}
}