blob: e45010d3c49c1c561d3c0a2c6f23e8e1150f89ce [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;
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..];
}