| // | |
| // 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.Globalization; | |
| using System.Linq; | |
| using org.apache.plc4net.tools.codegen.model.terms; | |
| namespace org.apache.plc4net.tools.codegen.output; | |
| /// <summary>Resolves MSpec names in the context of generated C# code.</summary> | |
| public interface IExpressionScope | |
| { | |
| string? ResolveReference(string name); | |
| string? ResolveCount(string name) => null; | |
| string? ResolveMember(string name) => null; | |
| } | |
| /// <summary>Renders an MSpec expression tree as a C# expression.</summary> | |
| public sealed class CSharpExpressionRenderer(IExpressionScope? scope = null) | |
| { | |
| private static readonly IReadOnlyDictionary<string, string> MemberAccessors = | |
| new Dictionary<string, string> | |
| { | |
| ["lengthInBytes"] = "GetLengthInBytes()", | |
| ["lengthInBits"] = "GetLengthInBits()" | |
| }; | |
| public string StaticHelperClass { get; set; } = "StaticHelper"; | |
| public string Render(Term? term) => term switch | |
| { | |
| null => "null", | |
| IntegerLiteral literal => literal.Text, | |
| HexadecimalLiteral literal => literal.Text, | |
| FloatLiteral literal => RenderFloat(literal), | |
| BooleanLiteral literal => literal.Value ? "true" : "false", | |
| NullLiteral => "null", | |
| StringLiteral literal => RenderString(literal.Value), | |
| UnaryExpression expression => RenderUnary(expression), | |
| IndexExpression expression => RenderIndex(expression), | |
| TernaryExpression expression => | |
| $"({Render(expression.Condition)} ? {Render(expression.WhenTrue)} : {Render(expression.WhenFalse)})", | |
| BinaryExpression expression => RenderBinary(expression), | |
| VariableLiteral variable => RenderVariable(variable), | |
| _ => throw new NotSupportedException($"Cannot render term {term.GetType().Name}") | |
| }; | |
| private static string RenderFloat(FloatLiteral literal) | |
| { | |
| string text = literal.Value.ToString("R", CultureInfo.InvariantCulture); | |
| return text.IndexOfAny(['.', 'e', 'E']) < 0 ? text + ".0" : text; | |
| } | |
| private static string RenderString(string value) => | |
| $"\"{value.Replace("\\", "\\\\").Replace("\"", "\\\"")}\""; | |
| private string RenderUnary(UnaryExpression expression) | |
| { | |
| string operand = Render(expression.Operand); | |
| return expression.Operator + | |
| (expression.Operand is UnaryExpression ? $"({operand})" : operand); | |
| } | |
| private string RenderIndex(IndexExpression expression) | |
| { | |
| string target = Render(expression.Target); | |
| if (expression.Target is UnaryExpression) | |
| { | |
| target = $"({target})"; | |
| } | |
| return target + string.Concat(expression.Indexes.Select(index => $"[{Render(index)}]")); | |
| } | |
| private string RenderBinary(BinaryExpression expression) => | |
| expression.Operator == "^" | |
| ? $"System.Math.Pow({Render(expression.Left)}, {Render(expression.Right)})" | |
| : $"({Render(expression.Left)} {expression.Operator} {Render(expression.Right)})"; | |
| private string RenderVariable(VariableLiteral variable) | |
| { | |
| if (variable.Name == MspecReader.EmptyStringSentinel && | |
| !variable.IsCall && variable.Index.Count == 0 && variable.Child == null) | |
| { | |
| return "\"\""; | |
| } | |
| string head; | |
| if (variable.IsCall && RenderBuiltin(variable) is { } builtin) | |
| { | |
| head = builtin; | |
| } | |
| else if (variable.IsCall) | |
| { | |
| head = (Resolve(variable.Name) ?? variable.Name) + | |
| $"({RenderArguments(variable.Args)})"; | |
| } | |
| else | |
| { | |
| head = Resolve(variable.Name) ?? variable.Name; | |
| } | |
| head += RenderIndexes(variable.Index); | |
| for (VariableLiteral? child = variable.Child; child != null; child = child.Child) | |
| { | |
| head += "." + RenderMember(child); | |
| } | |
| return head; | |
| } | |
| private string RenderMember(VariableLiteral member) | |
| { | |
| string value; | |
| if (member.IsCall) | |
| { | |
| value = $"{Pascal(member.Name)}({RenderArguments(member.Args)})"; | |
| } | |
| else if (MemberAccessors.TryGetValue(member.Name, out string? accessor)) | |
| { | |
| value = accessor; | |
| } | |
| else | |
| { | |
| value = scope?.ResolveMember(member.Name) ?? Pascal(member.Name); | |
| } | |
| return value + RenderIndexes(member.Index); | |
| } | |
| private string RenderIndexes(IReadOnlyList<Term> indexes) => | |
| string.Concat(indexes.Select(index => $"[{Render(index)}]")); | |
| private string RenderArguments(IReadOnlyList<Term>? arguments) => | |
| string.Join(", ", (arguments ?? []).Select(Render)); | |
| private string? RenderBuiltin(VariableLiteral variable) | |
| { | |
| if (variable.Args is not { } arguments) | |
| { | |
| return null; | |
| } | |
| return variable.Name switch | |
| { | |
| "COUNT" when arguments.Count == 1 => RenderCount(arguments[0]), | |
| "CEIL" when arguments.Count == 1 => | |
| $"(int) System.Math.Ceiling((double) ({Render(arguments[0])}))", | |
| "ARRAY_SIZE_IN_BYTES" when arguments.Count == 1 => | |
| $"{StaticHelperClass}.ArraySizeInBytes({Render(arguments[0])})", | |
| "STATIC_CALL" when arguments.Count >= 1 && arguments[0] is StringLiteral function => | |
| $"{StaticHelperClass}.{Pascal(function.Value)}({string.Join(", ", arguments.Skip(1).Select(Render))})", | |
| "CAST" when arguments.Count == 2 && RenderCastType(arguments[1]) is { } type => | |
| $"(({type}) {Render(arguments[0])})", | |
| _ => null | |
| }; | |
| } | |
| private string RenderCount(Term argument) | |
| { | |
| if (argument is VariableLiteral { Child: null, Args: null } variable && | |
| scope?.ResolveCount(variable.Name) is { } count) | |
| { | |
| return count; | |
| } | |
| return Render(argument) + ".Count"; | |
| } | |
| private static string? RenderCastType(Term term) => term switch | |
| { | |
| StringLiteral literal => literal.Value, | |
| VariableLiteral variable => RenderTypeName(variable), | |
| _ => null | |
| }; | |
| private static string? RenderTypeName(VariableLiteral variable) | |
| { | |
| var parts = new List<string>(); | |
| for (VariableLiteral? part = variable; part != null; part = part.Child) | |
| { | |
| if (part.IsCall || part.Index.Count != 0) | |
| { | |
| return null; | |
| } | |
| parts.Add(part.Name); | |
| } | |
| return string.Join(".", parts); | |
| } | |
| private string? Resolve(string name) => scope?.ResolveReference(name); | |
| private static string Pascal(string name) => string.IsNullOrEmpty(name) | |
| ? name | |
| : char.ToUpperInvariant(name[0]) + name[1..]; | |
| } |