// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Threading; using EpicGames.Core; using EpicGames.UHT.Tables; using EpicGames.UHT.Tokenizer; using EpicGames.UHT.Utils; namespace EpicGames.UHT.Parsers { /// /// Class responsible for parsing specifiers and the field meta data. To reduce allocations, one specifier parser is shared between all objects in /// a given header file. This makes the Action pattern being used a bit more obtuse, but it does help performance by reducing the allocations fairly /// significantly. /// public class UhtSpecifierParser : IUhtMessageExtraContext { struct DeferredSpecifier { public UhtSpecifier _specifier; public object? _value; } /// /// For a given header file, we share a common specifier parser to reduce the number of allocations. /// Before the parser can be reused, the ParseDeferred method must be called to dispatch that list. /// private static readonly ThreadLocal s_tls = new(() => null); private static readonly List> s_emptyKVPValues = new(); private UhtSpecifierContext _specifierContext; private IUhtTokenReader _tokenReader; private UhtSpecifierTable _table; private StringView _context; private StringView _currentSpecifier = new(); private List _deferredSpecifiers = new(); private bool _isParsingFieldMetaData = false; private List>? _currentKVPValues = null; private List? _currentStringValues = null; private int _umetaElementsParsed = 0; private readonly Action _parseAction; private readonly Action _parseFieldMetaDataAction; private readonly Action _parseKVPValueAction; private readonly Action _parseStringViewListAction; /// /// Get the cached specifier parser /// /// Specifier context /// User facing context /// Specifier table /// Specifier parser public static UhtSpecifierParser GetThreadInstance(UhtSpecifierContext specifierContext, StringView context, UhtSpecifierTable table) { if (s_tls.Value == null) { s_tls.Value = new UhtSpecifierParser(specifierContext, context, table); } else { s_tls.Value.Reset(specifierContext, context, table); } return s_tls.Value; } /// /// Construct a new specifier parser /// /// Specifier context /// User facing context added to messages /// Specifier table public UhtSpecifierParser(UhtSpecifierContext specifierContext, StringView context, UhtSpecifierTable table) { _specifierContext = specifierContext; _tokenReader = specifierContext.TokenReader; _context = context; _table = table; _parseAction = ParseInternal; _parseFieldMetaDataAction = ParseFieldMetaDataInternal; _parseKVPValueAction = ParseKVPValueInternal; _parseStringViewListAction = ParseStringViewListInternal; } /// /// Reset an existing parser to parse a new specifier block /// /// Specifier context /// User facing context added to messages /// Specifier table public void Reset(UhtSpecifierContext specifierContext, StringView context, UhtSpecifierTable table) { _specifierContext = specifierContext; _tokenReader = specifierContext.TokenReader; _context = context; _table = table; _deferredSpecifiers.Clear(); } /// /// Perform the specify parsing /// /// The parser public UhtSpecifierParser ParseSpecifiers() { _isParsingFieldMetaData = false; _specifierContext.MetaData.LineNumber = _tokenReader.InputLine; using UhtMessageContext tokenContext = new(this); _tokenReader.RequireList('(', ')', ',', false, _parseAction); return this; } /// /// Parse field meta data /// /// Specifier parser public UhtSpecifierParser ParseFieldMetaData() { _tokenReader = _specifierContext.TokenReader; _isParsingFieldMetaData = true; using UhtMessageContext tokenContext = new(this); if (_tokenReader.TryOptional("UMETA")) { _umetaElementsParsed = 0; _tokenReader.RequireList('(', ')', ',', false, _parseFieldMetaDataAction); if (_umetaElementsParsed == 0) { _tokenReader.LogError($"No metadata specified while parsing {UhtMessage.FormatContext(this)}"); } } return this; } /// /// Parse any deferred specifiers /// public void ParseDeferred() { foreach (DeferredSpecifier deferred in _deferredSpecifiers) { Dispatch(deferred._specifier, deferred._value); } _deferredSpecifiers.Clear(); } #region IMessageExtraContext implementation /// public IEnumerable? MessageExtraContext { get { Stack extraContext = new(1); string what = _isParsingFieldMetaData ? "metadata" : "specifiers"; if (_context.Span.Length > 0) { extraContext.Push($"{_context} {what}"); } else { extraContext.Push(what); } return extraContext; } } #endregion private void ParseInternal() { UhtToken identifier = _tokenReader.GetIdentifier(); _currentSpecifier = identifier.Value; if (_table.TryGetValue(_currentSpecifier, out UhtSpecifier? specifier)) { if (TryParseValue(specifier.ValueType, out object? value)) { if (specifier.When == UhtSpecifierWhen.Deferred) { _deferredSpecifiers ??= new List(); _deferredSpecifiers.Add(new DeferredSpecifier { _specifier = specifier, _value = value }); } else { Dispatch(specifier, value); } } } else { _tokenReader.LogError($"Unknown specifier '{_currentSpecifier}' found while parsing {UhtMessage.FormatContext(this)}"); } } private void ParseFieldMetaDataInternal() { if (!_tokenReader.TryOptionalIdentifier(out UhtToken key)) { throw new UhtException(_tokenReader, $"UMETA expects a key and optional value", this); } StringViewBuilder builder = new(); if (_tokenReader.TryOptional('=')) { if (!ReadValue(_tokenReader, builder, true)) { throw new UhtException(_tokenReader, $"UMETA key '{key.Value}' expects a value", this); } } ++_umetaElementsParsed; _specifierContext.MetaData.CheckedAdd(key.Value.ToString(), _specifierContext.MetaNameIndex, builder.ToString()); } private void ParseKVPValueInternal() { _currentKVPValues ??= new List>(); _currentKVPValues.Add(ReadKVP()); } private void ParseStringViewListInternal() { _currentStringValues ??= new List(); _currentStringValues.Add(ReadValue()); } private void Dispatch(UhtSpecifier specifier, object? value) { UhtSpecifierDispatchResults results = specifier.Dispatch(_specifierContext, value); if (results == UhtSpecifierDispatchResults.Unknown) { _tokenReader.LogError($"Unknown specifier '{specifier.Name}' found while parsing {UhtMessage.FormatContext(this)}"); } } private bool TryParseValue(UhtSpecifierValueType valueType, out object? value) { value = null; switch (valueType) { case UhtSpecifierValueType.NotSet: throw new UhtIceException("NotSet is an invalid value for value types"); case UhtSpecifierValueType.None: if (_tokenReader.TryOptional('=')) { ReadValue(); // consume the value; _tokenReader.LogError($"The specifier '{_currentSpecifier}' found a value when none was expected", this); return false; } return true; case UhtSpecifierValueType.String: if (!_tokenReader.TryOptional('=')) { _tokenReader.LogError($"The specifier '{_currentSpecifier}' expects a value", this); return false; } value = ReadValue(); return true; case UhtSpecifierValueType.OptionalString: { List? stringList = ReadValueList(); if (stringList != null && stringList.Count > 0) { value = stringList[0]; } return true; } case UhtSpecifierValueType.SingleString: { List? stringList = ReadValueList(); if (stringList == null || stringList.Count != 1) { _tokenReader.LogError($"The specifier '{_currentSpecifier}' expects a single value", this); return false; } value = stringList[0]; return true; } case UhtSpecifierValueType.StringList: value = ReadValueList(); return true; case UhtSpecifierValueType.Legacy: value = ReadValueList(); return true; case UhtSpecifierValueType.NonEmptyStringList: { List? stringList = ReadValueList(); if (stringList == null || stringList.Count == 0) { _tokenReader.LogError($"The specifier '{_currentSpecifier}' expects at list one value", this); return false; } value = stringList; return true; } case UhtSpecifierValueType.KeyValuePairList: { _currentKVPValues = null; _tokenReader .Require('=') .RequireList('(', ')', ',', false, _parseKVPValueAction); List> kvps = _currentKVPValues ?? s_emptyKVPValues; _currentKVPValues = null; value = kvps; return true; } case UhtSpecifierValueType.OptionalEqualsKeyValuePairList: { _currentKVPValues = null; // This parser isn't as strict as the other parsers... if (_tokenReader.TryOptional('=')) { if (!_tokenReader.TryOptionalList('(', ')', ',', false, _parseKVPValueAction)) { _parseKVPValueAction(); } } else { _tokenReader.TryOptionalList('(', ')', ',', false, _parseKVPValueAction); } List> kvps = _currentKVPValues ?? s_emptyKVPValues; _currentKVPValues = null; value = kvps; return true; } default: throw new UhtIceException("Unknown value type"); } } private KeyValuePair ReadKVP() { if (!_tokenReader.TryOptionalIdentifier(out UhtToken key)) { throw new UhtException(_tokenReader, $"The specifier '{_currentSpecifier}' expects a key and optional value", this); } StringView value = ""; if (_tokenReader.TryOptional('=')) { value = ReadValue(); } return new KeyValuePair(key.Value, value); } private List? ReadValueList() { _currentStringValues = null; // This parser isn't as strict as the other parsers... if (_tokenReader.TryOptional('=')) { if (!_tokenReader.TryOptionalList('(', ')', ',', false, _parseStringViewListAction)) { _parseStringViewListAction(); } } else { _tokenReader.TryOptionalList('(', ')', ',', false, _parseStringViewListAction); } List? stringValues = _currentStringValues; _currentStringValues = null; return stringValues; } private StringView ReadValue() { StringViewBuilder builder = new(); if (!ReadValue(_tokenReader, builder, false)) { throw new UhtException(_tokenReader, $"The specifier '{_currentSpecifier}' expects a value", this); } return builder.ToStringView(); } /// /// Parse the sequence of meta data /// /// Input token reader /// Output string builder /// If true, do not convert \" to " in string constants. This is required for UMETA data /// True if data was read private static bool ReadValue(IUhtTokenReader tokenReader, StringViewBuilder builder, bool respectQuotes) { UhtToken token = tokenReader.GetToken(); switch (token.TokenType) { case UhtTokenType.EndOfFile: case UhtTokenType.EndOfDefault: case UhtTokenType.EndOfType: case UhtTokenType.EndOfDeclaration: return false; case UhtTokenType.Identifier: // We handle true/false differently for compatibility with old UHT if (token.IsValue("true", true)) { builder.Append("TRUE"); } else if (token.IsValue("false", true)) { builder.Append("FALSE"); } else { builder.Append(token.Value); } if (tokenReader.TryOptional('=')) { builder.Append('='); if (!ReadValue(tokenReader, builder, respectQuotes)) { return false; } } break; case UhtTokenType.Symbol: builder.Append(token.Value); if (tokenReader.TryOptional('=')) { builder.Append('='); if (!ReadValue(tokenReader, builder, respectQuotes)) { return false; } } break; case UhtTokenType.FloatConst: // TODO: C++ UHT compat if (token.Value.Span.Contains('e') || token.Value.Span.Contains('E')) { tokenReader.LogError($"Metadata and specifier values can not contain exponential notion at this time"); } builder.Append(token.GetConstantValue(respectQuotes)); break; default: builder.Append(token.GetConstantValue(respectQuotes)); break; } return true; } } }