// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Text; using EpicGames.Core; using Microsoft.Extensions.Logging; using UnrealBuildBase; namespace UnrealBuildTool { /// /// Exception class for the preprocessor, which contains the file and position of the code causing an error /// class PreprocessorException : Exception { /// /// The context when the error was encountered /// public readonly PreprocessorContext? Context; /// /// Constructor /// /// The current preprocesor context /// Format string, to be passed to String.Format /// Optional argument list for the format string public PreprocessorException(PreprocessorContext? context, string format, params object[] args) : base(String.Format(format, args)) { Context = context; } } /// /// Implementation of a C++ preprocessor. /// class Preprocessor { /// /// Type of an include path /// public enum IncludePathType { /// /// Regular include path, enclosed by quotes /// Normal, /// /// System include path, enclosed by angle brackets /// System, } /// /// Include paths to look in /// readonly List _includeDirectories = new(); /// /// Framework paths to look in /// readonly List _frameworkDirectories = new(); /// /// Set of all included files with the #pragma once directive /// readonly HashSet _pragmaOnceFiles = new(); /// /// Set of any files that has been processed /// readonly HashSet _processedFiles = new(); /// /// The current state of the preprocessor /// readonly PreprocessorState _state = new(); /// /// Predefined token containing the constant "0" /// static readonly byte[] s_zeroLiteral = Encoding.UTF8.GetBytes("0"); /// /// Predefined token containing the constant "1" /// static readonly byte[] s_oneLiteral = Encoding.UTF8.GetBytes("1"); /// /// Value of the __COUNTER__ variable /// int _counter; /// /// List of files included by the preprocessor /// /// Enumerable of processed files public IEnumerable GetProcessedFiles() { return _processedFiles.AsEnumerable(); } /// /// Default constructor /// public Preprocessor() { DateTime now = DateTime.Now; AddLiteralMacro("__DATE__", TokenType.String, String.Format("\"{0} {1,2} {2}\"", now.ToString("MMM"), now.Day, now.Year)); AddLiteralMacro("__TIME__", TokenType.String, "\"" + now.ToString("HH:mm:ss") + "\""); AddLiteralMacro("__FILE__", TokenType.String, "\"\""); AddLiteralMacro("__LINE__", TokenType.Number, "-1"); AddLiteralMacro("__COUNTER__", TokenType.Number, "-1"); AddLiteralMacro("CHAR_BIT", TokenType.Number, "8"); // Workaround for #include_next not being supported on Linux for limit.h } /// /// Determines whether the current preprocessor branch is active /// /// True if the current branch is active public bool IsCurrentBranchActive() { return _state.IsCurrentBranchActive(); } /// /// Defines a macro. May have an optional '=Value' suffix. /// /// Macro to define public void AddDefinition(string definition) { List tokens = TokenReader.Tokenize(definition); if (tokens.Count == 0) { throw new PreprocessorException(null, "Missing macro name"); } if (tokens[0].Type != TokenType.Identifier) { throw new PreprocessorException(null, "'{0}' is not a valid macro name", tokens[0].ToString()!); } List valueTokens = new(); if (tokens.Count == 1) { valueTokens.Add(new Token(TokenType.Number, TokenFlags.None, s_oneLiteral)); } else if (tokens[1].Type != TokenType.Equals) { throw new PreprocessorException(null, "Unable to parse macro definition '{0}'", definition); } else { valueTokens.AddRange(tokens.Skip(2)); } PreprocessorMacro macro = new(tokens[0].Identifier!, null, valueTokens); _state.DefineMacro(macro); } /// /// Defines a macro /// /// Name of the macro /// String to be parsed for the macro's value public void AddDefinition(string name, string value) { List tokens = new(); TokenReader reader = new(value); while (reader.MoveNext()) { tokens.Add(reader.Current); } PreprocessorMacro macro = new(Identifier.FindOrAdd(name), null, tokens); _state.DefineMacro(macro); } /// /// Defines a macro /// /// The macro definition public void AddDefinition(PreprocessorMacro macro) { _state.DefineMacro(macro); } /// /// Adds an include path to the preprocessor /// /// The include path public void AddIncludePath(DirectoryItem directory) { if (!_includeDirectories.Contains(directory)) { _includeDirectories.Add(directory); } } /// /// Adds an include path to the preprocessor /// /// The include path public void AddIncludePath(DirectoryReference location) { DirectoryItem directory = DirectoryItem.GetItemByDirectoryReference(location); if (!directory.Exists) { throw new FileNotFoundException("Unable to find " + location.FullName); } AddIncludePath(directory); } /// /// Adds an include path to the preprocessor /// /// The include path public void AddIncludePath(string directoryName) { AddIncludePath(new DirectoryReference(directoryName)); } /// /// Adds a framework path to the preprocessor /// /// The framework path public void AddFrameworkPath(DirectoryItem directory) { if (!_frameworkDirectories.Contains(directory)) { _frameworkDirectories.Add(directory); } } /// /// Adds a framework path to the preprocessor /// /// The framework path public void AddFrameworkPath(DirectoryReference location) { DirectoryItem directory = DirectoryItem.GetItemByDirectoryReference(location); if (!directory.Exists) { throw new FileNotFoundException("Unable to find " + location.FullName); } AddFrameworkPath(directory); } /// /// Adds a framework path to the preprocessor /// /// The framework path public void AddFrameworkPath(string directoryName) { AddFrameworkPath(new DirectoryReference(directoryName)); } /// /// Try to resolve an quoted include against the list of include directories. Uses search order described by https://msdn.microsoft.com/en-us/library/36k2cdd4.aspx. /// /// The current preprocessor context /// The path appearing in an #include directive /// Specifies rules for how to resolve the include path (normal/system) /// If found, receives the resolved file /// True if the The resolved file public bool TryResolveIncludePath(PreprocessorContext context, string includePath, IncludePathType type, [NotNullWhen(true)] out FileItem? file) { // From MSDN (https://msdn.microsoft.com/en-us/library/36k2cdd4.aspx?f=255&MSPPError=-2147217396) // // The preprocessor searches for include files in this order: // // Quoted form: // 1) In the same directory as the file that contains the #include statement. // 2) In the directories of the currently opened include files, in the reverse order in which they were opened. // The search begins in the directory of the parent include file and continues upward through the directories of any grandparent include files. // 3) Along the path that's specified by each /I compiler option. // 4) Along the paths that are specified by the INCLUDE environment variable. // // Angle-bracket form: // 1) Along the path that's specified by each /I compiler option. // 2) Along the paths that are specified by the INCLUDE environment variable. // If it's an absolute path, return it immediately if (Path.IsPathRooted(includePath)) { FileItem fileItem = FileItem.GetItemByPath(includePath); if (fileItem.Exists) { file = fileItem; return true; } else { file = null; return false; } } // Split the path into fragments string[] fragments = includePath.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); // Try to match the include path against any of the included directories if (type == IncludePathType.Normal) { for (PreprocessorContext? outerContext = context; outerContext != null; outerContext = outerContext.Outer) { if (outerContext is PreprocessorFileContext outerFileContext) { if (TryResolveRelativeIncludePath(outerFileContext.Directory, fragments, out FileItem? resolvedFile)) { file = resolvedFile; return true; } } } } // Try to match the include path against any of the system directories foreach (DirectoryItem baseDirectory in _includeDirectories) { if (TryResolveRelativeIncludePath(baseDirectory, fragments, out FileItem? resolvedFile)) { file = resolvedFile; return true; } } // Try to match the include path against any of the MacOS framework Header paths if (fragments.Length > 1) { foreach (DirectoryItem baseDirectory in _frameworkDirectories) { if (baseDirectory.TryGetDirectory($"{fragments[0]}.framework", out DirectoryItem? frameworkBaseDirectory) && frameworkBaseDirectory.TryGetDirectory("Headers", out DirectoryItem? headerDirectory) && TryResolveRelativeIncludePath(headerDirectory, fragments.Skip(1).ToArray(), out FileItem? resolvedFile)) { file = resolvedFile; return true; } } } // Failed to find the file file = null; return false; } /// /// Try to resolve an quoted include against the list of include directories. Uses search order described by https://msdn.microsoft.com/en-us/library/36k2cdd4.aspx. /// /// The base directory to search from /// Fragments of the relative path to follow /// The file that was found, if successful /// True if the The resolved file public static bool TryResolveRelativeIncludePath(DirectoryItem baseDirectory, string[] fragments, [NotNullWhen(true)] out FileItem? file) { DirectoryItem? directory = baseDirectory; for (int idx = 0; idx < fragments.Length - 1; idx++) { if (!directory.TryGetDirectory(fragments[idx], out directory)) { file = null; return false; } } return directory.TryGetFile(fragments[^1], out file); } /// /// Parses a file recursively /// /// File to parse /// Lists of fragments that are parsed /// Outer context information, for error messages /// Cache for source files /// Logger for output /// Show all the included files, in order /// Suppress exceptions if an include path can not be resolved public void ParseFile(FileItem file, List fragments, PreprocessorContext? outerContext, SourceFileMetadataCache sourceFileCache, ILogger logger, bool showIncludes = false, bool ignoreMissingIncludes = false) { // If the file has already been included and had a #pragma once directive, don't include it again if (_pragmaOnceFiles.Contains(file)) { return; } _processedFiles.Add(file); // Output a trace of the included files if (showIncludes) { logger.LogInformation("Note: including file: {FileLocation}", file.Location); } // If the file had a header guard, and the macro is still defined, don't include it again SourceFile sourceFile = sourceFileCache.GetSourceFile(file); if (sourceFile.HeaderGuardMacro != null && _state.IsMacroDefined(sourceFile.HeaderGuardMacro)) { return; } // Create a context for this file PreprocessorFileContext context = new(sourceFile, outerContext); // Parse the markup for this file while (context.MarkupIdx < sourceFile.Markup.Length) { SourceFileMarkup markup = sourceFile.Markup[context.MarkupIdx]; if (markup.Type == SourceFileMarkupType.Include) { if (_state.IsCurrentBranchActive()) { // Parse the directive FileItem? includedFile = ParseIncludeDirective(markup, context, ignoreMissingIncludes); // Parse the included file if (includedFile != null) { ParseFile(includedFile, fragments, context, sourceFileCache, logger, showIncludes, ignoreMissingIncludes); } } context.MarkupIdx++; } else { // Get the next fragment SourceFileFragment fragment = sourceFile.Fragments[context.FragmentIdx]; Debug.Assert(fragment.MarkupMin == context.MarkupIdx); // Parse this fragment ParseFragment(sourceFile, fragment, context); // Add this fragment to the list fragments.Add(fragment); context.FragmentIdx++; } } } /// /// Parse an include directive and resolve the file it references /// /// Markup for the include directive /// Current preprocessor context /// Suppress exceptions if an include path can not be resolved /// Included file FileItem? ParseIncludeDirective(SourceFileMarkup markup, PreprocessorFileContext context, bool ignoreMissingIncludes = false) { // Expand macros in the given tokens List expandedTokens = new(); ExpandMacros(markup.Tokens!, expandedTokens, false, context); // Convert the string to a single token string includeToken = Token.Format(expandedTokens); // Expand any macros in them and resolve it IncludePathType type; if (includeToken.Length >= 2 && includeToken[0] == '\"' && includeToken[^1] == '\"') { type = IncludePathType.Normal; } else if (includeToken.Length >= 2 && includeToken[0] == '<' && includeToken[^1] == '>') { type = IncludePathType.System; } else { throw new PreprocessorException(context, "Couldn't resolve include '{0}'", includeToken); } // Get the include path string includePath = includeToken[1..^1]; // Resolve the included file if (!TryResolveIncludePath(context, includePath, type, out FileItem? includedFile)) { if (ignoreMissingIncludes) { Log.TraceWarningOnce("Couldn't resolve include '{0}' ({1})", includePath, context.SourceFile.Location); } else { throw new PreprocessorException(context, "Couldn't resolve include '{0}' ({1})", includePath, context.SourceFile.Location); } } return includedFile; } /// /// Parse a source file fragment, using cached transforms if possible /// /// The source file being parsed /// Fragment to parse /// Current preprocessor context void ParseFragment(SourceFile sourceFile, SourceFileFragment fragment, PreprocessorFileContext context) { // Check if there's a valid transform that matches the current state int transformIdx = 0; for (; ; ) { PreprocessorTransform[] transforms; lock (fragment) { transforms = fragment.Transforms; if (transformIdx == transforms.Length) { // Attach a new transform to the current state PreprocessorTransform transform = _state.BeginCapture(); for (; context.MarkupIdx < fragment.MarkupMax; context.MarkupIdx++) { SourceFileMarkup markup = sourceFile.Markup[context.MarkupIdx]; ParseMarkup(markup.Type, markup.Tokens!, context); } transform = _state.EndCapture()!; // Add it to the fragment for future fragments PreprocessorTransform[] newTransforms = new PreprocessorTransform[fragment.Transforms.Length + 1]; for (int idx = 0; idx < transforms.Length; idx++) { newTransforms[idx] = transforms[idx]; } newTransforms[transforms.Length] = transform; // Assign it to the fragment fragment.Transforms = newTransforms; return; } } for (; transformIdx < transforms.Length; transformIdx++) { PreprocessorTransform transform = transforms[transformIdx]; if (_state.TryToApply(transform)) { // Update the pragma once state if (transform.HasPragmaOnce) { _pragmaOnceFiles.Add(sourceFile.File); } // Move to the end of the fragment context.MarkupIdx = fragment.MarkupMax; return; } } } } /// /// Validate and add a macro using the given parameter and token list /// /// The current preprocessor context /// Name of the macro /// Parameter list for the macro /// List of tokens void AddMacro(PreprocessorContext context, Identifier name, List? parameters, List tokens) { if (tokens.Count == 0) { tokens.Add(new Token(TokenType.Placemarker, TokenFlags.None)); } else { if (tokens[0].HasLeadingSpace) { tokens[0] = tokens[0].RemoveFlags(TokenFlags.HasLeadingSpace); } if (tokens[0].Type == TokenType.HashHash || tokens[^1].Type == TokenType.HashHash) { throw new PreprocessorException(context, "Invalid use of concatenation at start or end of token sequence"); } if (parameters == null || parameters.Count == 0 || parameters[^1] != Identifiers.__VA_ARGS__) { if (tokens.Any(x => x.Identifier == Identifiers.__VA_ARGS__)) { throw new PreprocessorException(context, "Invalid reference to {0}", Identifiers.__VA_ARGS__); } } } _state.DefineMacro(new PreprocessorMacro(name, parameters, tokens)); } /// /// Set a predefined macro to a given value /// /// Name of the macro /// Type of the macro token /// Value of the macro /// The created macro void AddLiteralMacro(string name, TokenType type, string value) { Token token = new(type, TokenFlags.None, value); PreprocessorMacro macro = new(Identifier.FindOrAdd(name), null, new List { token }); _state.DefineMacro(macro); } /// /// Parse a marked up directive from a file /// /// The markup type /// Tokens for the directive /// The context that this markup is being parsed in public void ParseMarkup(SourceFileMarkupType type, List tokens, PreprocessorContext context) { switch (type) { case SourceFileMarkupType.Include: throw new PreprocessorException(context, "Include directives should be handled by the caller."); case SourceFileMarkupType.Define: ParseDefineDirective(tokens, context); break; case SourceFileMarkupType.Undef: ParseUndefDirective(tokens, context); break; case SourceFileMarkupType.If: ParseIfDirective(tokens, context); break; case SourceFileMarkupType.Ifdef: ParseIfdefDirective(tokens, context); break; case SourceFileMarkupType.Ifndef: ParseIfndefDirective(tokens, context); break; case SourceFileMarkupType.Elif: ParseElifDirective(tokens, context); break; case SourceFileMarkupType.Else: ParseElseDirective(tokens, context); break; case SourceFileMarkupType.Endif: ParseEndifDirective(tokens, context); break; case SourceFileMarkupType.Pragma: ParsePragmaDirective(tokens, context); break; } } /// /// Read a macro definition /// /// List of tokens in the directive /// The context that this directive is being parsed in public void ParseDefineDirective(List tokens, PreprocessorContext context) { if (_state.IsCurrentBranchActive()) { // Check there's a name token if (tokens.Count < 1 || tokens[0].Type != TokenType.Identifier || tokens[0].Identifier == Identifiers.Defined) { throw new PreprocessorException(context, "Invalid macro name"); } // Read the macro name Identifier name = tokens[0].Identifier!; int tokenIdx = 1; // Read the macro parameter list, if there is one List? parameters = null; if (tokenIdx < tokens.Count && !tokens[tokenIdx].HasLeadingSpace && tokens[tokenIdx].Type == TokenType.LeftParen) { parameters = new List(); if (++tokenIdx == tokens.Count) { throw new PreprocessorException(context, "Unexpected end of macro parameter list"); } if (tokens[tokenIdx].Type != TokenType.RightParen) { for (; ; tokenIdx++) { // Check there's enough tokens left for a parameter name, plus ',' or ')' if (tokenIdx + 2 > tokens.Count) { throw new PreprocessorException(context, "Unexpected end of macro parameter list"); } // Check it's a valid name, and add it to the list Token nameToken = tokens[tokenIdx++]; if (nameToken.Type == TokenType.Ellipsis) { if (tokens[tokenIdx].Type != TokenType.RightParen) { throw new PreprocessorException(context, "Variadic macro arguments must be last in list"); } else { nameToken = new Token(Identifiers.__VA_ARGS__, nameToken.Flags & TokenFlags.HasLeadingSpace); } } else { if (nameToken.Type != TokenType.Identifier || nameToken.Identifier == Identifiers.__VA_ARGS__) { throw new PreprocessorException(context, "Invalid preprocessor token: {0}", nameToken); } if (parameters.Contains(nameToken.Identifier!)) { throw new PreprocessorException(context, "'{0}' has already been used as an argument name", nameToken); } } parameters.Add(nameToken.Identifier!); // Read the separator Token separatorToken = tokens[tokenIdx]; if (separatorToken.Type == TokenType.RightParen) { break; } if (separatorToken.Type != TokenType.Comma) { throw new PreprocessorException(context, "Expected ',' or ')'"); } } } tokenIdx++; } // Read the macro tokens AddMacro(context, name, parameters, tokens.Skip(tokenIdx).ToList()); } } /// /// Parse an #undef directive /// /// List of tokens in the directive /// The context that this directive is being parsed in public void ParseUndefDirective(List tokens, PreprocessorContext context) { if (_state.IsCurrentBranchActive()) { // Check there's a name token if (tokens.Count != 1) { throw new PreprocessorException(context, "Expected a single token after #undef"); } if (tokens[0].Type != TokenType.Identifier) { throw new PreprocessorException(context, "Invalid macro name '{0}'", tokens[0]); } // Remove the macro from the list of definitions _state.UndefMacro(tokens[0].Identifier!); } } /// /// Parse an #if directive /// /// List of tokens in the directive /// The context that this directive is being parsed in public void ParseIfDirective(List tokens, PreprocessorContext context) { PreprocessorBranch branch = PreprocessorBranch.HasIfDirective; if (_state.IsCurrentBranchActive()) { // Read a line into the buffer and expand the macros in it List expandedTokens = new(); ExpandMacros(tokens, expandedTokens, true, context); // Evaluate the condition long result = PreprocessorExpression.Evaluate(context, expandedTokens); if (result != 0) { branch |= PreprocessorBranch.Active | PreprocessorBranch.Taken; } } _state.PushBranch(branch); } /// /// Parse an #ifdef directive /// /// List of tokens in the directive /// The context that this directive is being parsed in public void ParseIfdefDirective(List tokens, PreprocessorContext context) { PreprocessorBranch branch = PreprocessorBranch.HasIfdefDirective; if (_state.IsCurrentBranchActive()) { // Make sure there's only one token if (tokens.Count != 1 || tokens[0].Type != TokenType.Identifier) { throw new PreprocessorException(context, "Missing or invalid macro name for #ifdef directive"); } // Check if the macro is defined if (_state.IsMacroDefined(tokens[0].Identifier!)) { branch |= PreprocessorBranch.Active | PreprocessorBranch.Taken; } } _state.PushBranch(branch); } /// /// Parse an #ifndef directive /// /// List of tokens for this directive /// The context that this directive is being parsed in public void ParseIfndefDirective(List tokens, PreprocessorContext context) { PreprocessorBranch branch = PreprocessorBranch.HasIfndefDirective; if (_state.IsCurrentBranchActive()) { // Make sure there's only one token if (tokens.Count != 1 || tokens[0].Type != TokenType.Identifier) { throw new PreprocessorException(context, "Missing or invalid macro name for #ifndef directive"); } // Check if the macro is defined if (!_state.IsMacroDefined(tokens[0].Identifier!)) { branch |= PreprocessorBranch.Active | PreprocessorBranch.Taken; } } _state.PushBranch(branch); } /// /// Parse an #elif directive /// /// List of tokens for this directive /// The context that this directive is being parsed in public void ParseElifDirective(List tokens, PreprocessorContext context) { // Check we're in a branch, and haven't already read an #else directive if (!_state.TryPopBranch(out PreprocessorBranch branch)) { throw new PreprocessorException(context, "#elif directive outside conditional block"); } if (branch.HasFlag(PreprocessorBranch.Complete)) { throw new PreprocessorException(context, "#elif directive cannot appear after #else"); } // Pop the current branch state at this depth, so we can test against whether the parent state is enabled branch = (branch | PreprocessorBranch.HasElifDirective) & ~PreprocessorBranch.Active; if (_state.IsCurrentBranchActive()) { // Read a line into the buffer and expand the macros in it List expandedTokens = new(); ExpandMacros(tokens, expandedTokens, true, context); // Check we're at the end of a conditional block if (!branch.HasFlag(PreprocessorBranch.Taken)) { long result = PreprocessorExpression.Evaluate(context, expandedTokens); if (result != 0) { branch |= PreprocessorBranch.Active | PreprocessorBranch.Taken; } } } _state.PushBranch(branch); } /// /// Parse an #else directive /// /// List of tokens in the directive /// The context that this directive is being parsed in public void ParseElseDirective(List tokens, PreprocessorContext context) { // Make sure there's nothing else on the line if (tokens.Count > 0) { throw new PreprocessorException(context, "Garbage after #else directive"); } // Check we're in a branch, and haven't already read an #else directive if (!_state.TryPopBranch(out PreprocessorBranch branch)) { throw new PreprocessorException(context, "#else directive without matching #if directive"); } if ((branch & PreprocessorBranch.Complete) != 0) { throw new PreprocessorException(context, "Only one #else directive can appear in a conditional block"); } // Check whether to take this branch, but only allow activating if the parent state is active. branch &= ~PreprocessorBranch.Active; if (_state.IsCurrentBranchActive() && !branch.HasFlag(PreprocessorBranch.Taken)) { branch |= PreprocessorBranch.Active | PreprocessorBranch.Taken; } _state.PushBranch(branch | PreprocessorBranch.Complete); } /// /// Parse an #endif directive /// /// List of tokens in the directive /// The context that this directive is being parsed in [SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "")] public void ParseEndifDirective(List tokens, PreprocessorContext context) { // Pop the branch off the stack if (!_state.TryPopBranch(out _)) { throw new PreprocessorException(context, "#endif directive without matching #if/#ifdef/#ifndef directive"); } } /// /// Parse a #pragma directive /// /// List of tokens in the directive /// The context that this directive is being parsed in public void ParsePragmaDirective(List tokens, PreprocessorContext context) { if (_state.IsCurrentBranchActive()) { if (tokens.Count == 1 && tokens[0].Identifier == Identifiers.Once) { SourceFile sourceFile = GetCurrentSourceFile(context)!; _pragmaOnceFiles.Add(sourceFile.File); _state.MarkPragmaOnce(); } } } /// /// Expand macros in the given sequence. /// /// Sequence of input tokens /// List to receive the expanded tokens /// Whether a conditional expression is being evaluated (and 'defined' expressions are valid) /// The context that this directive is being parsed in public void ExpandMacros(IEnumerable inputTokens, List outputTokens, bool isConditional, PreprocessorContext context) { List ignoreMacros = new(); ExpandMacrosRecursively(inputTokens, outputTokens, isConditional, ignoreMacros, context); } /// /// Expand macros in the given sequence, ignoring previously expanded macro names from a list. /// /// Sequence of input tokens /// List to receive the expanded tokens /// Whether a conditional expression is being evaluated (and 'defined' expressions are valid) /// List of macros to ignore /// The context that this directive is being parsed in void ExpandMacrosRecursively(IEnumerable inputTokens, List outputTokens, bool isConditional, List ignoreMacros, PreprocessorContext context) { IEnumerator inputEnumerator = inputTokens.GetEnumerator(); if (inputEnumerator.MoveNext()) { for (; ; ) { if (!ReadExpandedToken(inputEnumerator, outputTokens, isConditional, ignoreMacros, context)) { break; } } } } /// /// Merges an optional leading space flag into the given token (recycling the original token if possible). /// /// The token to merge a leading space into /// The leading space flag /// New token with the leading space flag set, or the existing token static Token MergeLeadingSpace(Token token, bool hasLeadingSpace) { Token result = token; if (hasLeadingSpace && !result.HasLeadingSpace) { result = result.AddFlags(TokenFlags.HasLeadingSpace); } return result; } /// /// Read a token from an enumerator and substitute it if it's a macro or 'defined' expression (reading more tokens as necessary to complete the expression). /// /// The enumerator of input tokens /// List to receive the expanded tokens /// Whether a conditional expression is being evaluated (and 'defined' expressions are valid) /// List of macros to ignore /// The context that this directive is being parsed in /// Result from calling the enumerator's MoveNext() method bool ReadExpandedToken(IEnumerator inputEnumerator, List outputTokens, bool isConditional, List ignoreMacros, PreprocessorContext context) { // Capture the first token, then move to the next outputTokens.Add(inputEnumerator.Current); bool moveNext = inputEnumerator.MoveNext(); // If it's an identifier, try to resolve it as a macro if (outputTokens[^1].Identifier == Identifiers.Defined && isConditional) { // Remove the 'defined' keyword outputTokens.RemoveAt(outputTokens.Count - 1); // Make sure there's another token if (!moveNext) { throw new PreprocessorException(context, "Invalid syntax for 'defined' expression"); } // Check for the form 'defined identifier' Token nameToken; if (inputEnumerator.Current.Type == TokenType.Identifier) { nameToken = inputEnumerator.Current; } else { // Otherwise assume the form 'defined ( identifier )' if (inputEnumerator.Current.Type != TokenType.LeftParen || !inputEnumerator.MoveNext() || inputEnumerator.Current.Type != TokenType.Identifier) { throw new PreprocessorException(context, "Invalid syntax for 'defined' expression"); } nameToken = inputEnumerator.Current; if (!inputEnumerator.MoveNext() || inputEnumerator.Current.Type != TokenType.RightParen) { throw new PreprocessorException(context, "Invalid syntax for 'defined' expression"); } } // Insert a token for whether it's defined or not outputTokens.Add(new Token(TokenType.Number, TokenFlags.None, _state.IsMacroDefined(nameToken.Identifier!) ? s_oneLiteral : s_zeroLiteral)); moveNext = inputEnumerator.MoveNext(); } else { // Repeatedly try to expand the last token into the list while (outputTokens[^1].Type == TokenType.Identifier && !outputTokens[^1].Flags.HasFlag(TokenFlags.DisableExpansion)) { // Try to get a macro for the current token if (!_state.TryGetMacro(outputTokens[^1].Identifier!, out PreprocessorMacro? macro) || ignoreMacros.Contains(macro)) { break; } if (macro.IsFunctionMacro && (!moveNext || inputEnumerator.Current.Type != TokenType.LeftParen)) { break; } // Remove the macro name from the output list bool hasLeadingSpace = outputTokens[^1].HasLeadingSpace; outputTokens.RemoveAt(outputTokens.Count - 1); // Save the initial number of tokens in the list, so we can tell if it expanded int numTokens = outputTokens.Count; // If it's an object macro, expand it immediately into the output buffer if (macro.IsObjectMacro) { // Expand the macro tokens into the output buffer ExpandObjectMacro(macro, outputTokens, isConditional, ignoreMacros, context); } else { // Read balanced token for argument list List argumentTokens = new(); moveNext = ReadBalancedToken(inputEnumerator, argumentTokens, context); // Expand the macro tokens into the output buffer ExpandFunctionMacro(macro, argumentTokens, outputTokens, isConditional, ignoreMacros, context); } // If the macro expanded to nothing, quit if (outputTokens.Count <= numTokens) { break; } // Make sure the space is propagated to the expanded macro outputTokens[numTokens] = MergeLeadingSpace(outputTokens[numTokens], hasLeadingSpace); // Mark any tokens matching the macro name as not to be expanded again. This can happen with recursive object macros, eg. #define DWORD ::DWORD for (int idx = numTokens; idx < outputTokens.Count; idx++) { if (outputTokens[idx].Type == TokenType.Identifier && outputTokens[idx].Identifier == macro.Name) { outputTokens[idx] = outputTokens[idx].AddFlags(TokenFlags.DisableExpansion); } } } } return moveNext; } /// /// Gets a string for the __FILE__ macro /// /// Context to scan to find the current file /// String representing the current context static string GetCurrentFileMacroValue(PreprocessorContext context) { SourceFile? sourceFile = GetCurrentSourceFile(context); if (sourceFile == null) { return ""; } else { return sourceFile.Location.FullName; } } /// /// Gets a string for the current file /// /// Context to scan to find the current file /// Current source file being parsed static SourceFile? GetCurrentSourceFile(PreprocessorContext context) { SourceFile? sourceFile = null; for (PreprocessorContext? outerContext = context; outerContext != null; outerContext = outerContext.Outer) { if (outerContext is PreprocessorFileContext outerFileContext) { sourceFile = outerFileContext.SourceFile; break; } } return sourceFile; } /// /// Gets the current line number /// /// Context to scan to find the current file /// Line number in the first file encountered static int GetCurrentLine(PreprocessorContext context) { for (PreprocessorContext? outerContext = context; outerContext != null; outerContext = outerContext.Outer) { if (outerContext is PreprocessorFileContext outerFileContext) { return outerFileContext.SourceFile.Markup[outerFileContext.MarkupIdx].LineNumber; } } return 0; } /// /// Expand an object macro /// /// The functional macro /// The list to receive the output tokens /// Whether the macro is being expanded in a conditional context, allowing use of the 'defined' keyword /// List of macros currently being expanded, which should be ignored for recursion /// The context that this directive is being parsed in void ExpandObjectMacro(PreprocessorMacro macro, List outputTokens, bool isConditional, List ignoreMacros, PreprocessorContext context) { // Special handling for the __LINE__ directive, since we need an updated line number for the current token if (macro.Name == Identifiers.__FILE__) { Token token = new(TokenType.String, TokenFlags.None, String.Format("\"{0}\"", GetCurrentFileMacroValue(context).Replace("\\", "\\\\"))); outputTokens.Add(token); } else if (macro.Name == Identifiers.__LINE__) { Token token = new(TokenType.Number, TokenFlags.None, GetCurrentLine(context).ToString()); outputTokens.Add(token); } else if (macro.Name == Identifiers.__COUNTER__) { Token token = new(TokenType.Number, TokenFlags.None, (_counter++).ToString()); outputTokens.Add(token); } else { int outputTokenCount = outputTokens.Count; // Expand all the macros ignoreMacros.Add(macro); ExpandMacrosRecursively(macro.Tokens, outputTokens, isConditional, ignoreMacros, context); ignoreMacros.RemoveAt(ignoreMacros.Count - 1); // Concatenate any adjacent tokens for (int idx = outputTokenCount + 1; idx < outputTokens.Count - 1; idx++) { if (outputTokens[idx].Type == TokenType.HashHash) { outputTokens[idx - 1] = Token.Concatenate(outputTokens[idx - 1], outputTokens[idx + 1], context); outputTokens.RemoveRange(idx, 2); idx--; } } } } /// /// Expand a function macro /// /// The functional macro /// Identifiers for each argument token /// The list to receive the output tokens /// Whether the macro is being expanded in a conditional context, allowing use of the 'defined' keyword /// List of macros currently being expanded, which should be ignored for recursion /// The context that this macro is being expanded void ExpandFunctionMacro(PreprocessorMacro macro, List argumentListTokens, List outputTokens, bool isConditional, List ignoreMacros, PreprocessorContext context) { // Replace any newlines with spaces, and merge them with the following token for (int idx = 0; idx < argumentListTokens.Count; idx++) { if (argumentListTokens[idx].Type == TokenType.Newline) { if (idx + 1 < argumentListTokens.Count) { argumentListTokens[idx + 1] = MergeLeadingSpace(argumentListTokens[idx + 1], true); } argumentListTokens.RemoveAt(idx--); } } // Split the arguments out into separate lists List> arguments = new(); if (argumentListTokens.Count > 2) { for (int idx = 1; ; idx++) { if (!macro.HasVariableArgumentList || arguments.Count < macro.Parameters!.Count) { arguments.Add(new List()); } List argument = arguments[^1]; int initialIdx = idx; while (idx < argumentListTokens.Count - 1 && argumentListTokens[idx].Type != TokenType.Comma) { if (!ReadBalancedToken(argumentListTokens, ref idx, argument)) { throw new PreprocessorException(context, "Invalid argument"); } } if (argument.Count > 0 && arguments[^1][0].HasLeadingSpace) { argument[0] = argument[0].RemoveFlags(TokenFlags.HasLeadingSpace); } bool hasLeadingSpace = false; for (int tokenIdx = 0; tokenIdx < argument.Count; tokenIdx++) { if (argument[tokenIdx].Text.Length == 0) { hasLeadingSpace |= argument[tokenIdx].HasLeadingSpace; argument.RemoveAt(tokenIdx--); } else { argument[tokenIdx] = MergeLeadingSpace(argument[tokenIdx], hasLeadingSpace); hasLeadingSpace = false; } } if (argument.Count == 0) { argument.Add(new Token(TokenType.Placemarker, TokenFlags.None)); argument.Add(new Token(TokenType.Placemarker, hasLeadingSpace ? TokenFlags.HasLeadingSpace : TokenFlags.None)); } if (idx == argumentListTokens.Count - 1) { break; } if (argumentListTokens[idx].Type != TokenType.Comma) { throw new PreprocessorException(context, "Expected ',' between arguments"); } if (macro.HasVariableArgumentList && arguments.Count == macro.Parameters!.Count && idx < argumentListTokens.Count - 1) { arguments[^1].Add(argumentListTokens[idx]); } } } // Add an empty variable argument if one was not specified if (macro.HasVariableArgumentList && arguments.Count == macro.Parameters!.Count - 1) { arguments.Add(new List { new(TokenType.Placemarker, TokenFlags.None) }); } // Validate the argument list if (arguments.Count != macro.Parameters!.Count) { throw new PreprocessorException(context, "Incorrect number of arguments to macro"); } // Expand each one of the arguments List> expandedArguments = new(); for (int idx = 0; idx < arguments.Count; idx++) { List newArguments = new(); ExpandMacrosRecursively(arguments[idx], newArguments, isConditional, ignoreMacros, context); expandedArguments.Add(newArguments); } // Substitute all the argument tokens List expandedTokens = new(); for (int idx = 0; idx < macro.Tokens.Count; idx++) { Token token = macro.Tokens[idx]; if (token.Type == TokenType.Hash && idx + 1 < macro.Tokens.Count) { // Stringizing operator int paramIdx = macro.FindParameterIndex(macro.Tokens[++idx].Identifier!); if (paramIdx == -1) { throw new PreprocessorException(context, "{0} is not an argument name", macro.Tokens[idx].Text); } expandedTokens.Add(new Token(TokenType.String, token.Flags & TokenFlags.HasLeadingSpace, String.Format("\"{0}\"", Token.Format(arguments[paramIdx]).Replace("\\", "\\\\").Replace("\"", "\\\"")))); } else if (macro.HasVariableArgumentList && idx + 2 < macro.Tokens.Count && token.Type == TokenType.Comma && macro.Tokens[idx + 1].Type == TokenType.HashHash && macro.Tokens[idx + 2].Identifier == Identifiers.__VA_ARGS__) { // Special MSVC/GCC extension: ', ## __VA_ARGS__' removes the comma if __VA_ARGS__ is empty. MSVC seems to format the result with a forced space. List expandedArgument = expandedArguments[^1]; if (expandedArgument.Any(x => x.Text.Length > 0)) { expandedTokens.Add(token); AppendTokensWithWhitespace(expandedTokens, expandedArgument, false); idx += 2; } else { expandedTokens.Add(new Token(TokenType.Placemarker, token.Flags & TokenFlags.HasLeadingSpace)); expandedTokens.Add(new Token(TokenType.Placemarker, TokenFlags.HasLeadingSpace)); idx += 2; } } else if (token.Type == TokenType.Identifier) { // Expand a parameter int paramIdx = macro.FindParameterIndex(token.Identifier!); if (paramIdx == -1) { expandedTokens.Add(token); } else if (idx > 0 && macro.Tokens[idx - 1].Type == TokenType.HashHash) { AppendTokensWithWhitespace(expandedTokens, arguments[paramIdx], token.HasLeadingSpace); } else if (idx + 1 < macro.Tokens.Count && macro.Tokens[idx + 1].Type == TokenType.HashHash) { AppendTokensWithWhitespace(expandedTokens, arguments[paramIdx], token.HasLeadingSpace); } else { AppendTokensWithWhitespace(expandedTokens, expandedArguments[paramIdx], token.HasLeadingSpace); } } else { expandedTokens.Add(token); } } // Concatenate adjacent tokens for (int idx = 1; idx < expandedTokens.Count - 1; idx++) { if (expandedTokens[idx].Type == TokenType.HashHash) { Token concatenatedToken = Token.Concatenate(expandedTokens[idx - 1], expandedTokens[idx + 1], context); expandedTokens.RemoveRange(idx, 2); expandedTokens[--idx] = concatenatedToken; } } // Finally, return the expansion of this ignoreMacros.Add(macro); ExpandMacrosRecursively(expandedTokens, outputTokens, isConditional, ignoreMacros, context); ignoreMacros.RemoveAt(ignoreMacros.Count - 1); } /// /// Appends a list of tokens to another list, setting the leading whitespace flag to the given value /// /// List to receive the appended tokens /// List of tokens to append /// Whether there is space before the first token static void AppendTokensWithWhitespace(List outputTokens, List inputTokens, bool hasLeadingSpace) { if (inputTokens.Count > 0) { outputTokens.Add(MergeLeadingSpace(inputTokens[0], hasLeadingSpace)); outputTokens.AddRange(inputTokens.Skip(1)); } } /// /// Copies a single token from one list of tokens to another, or if it's an opening parenthesis, the entire subexpression until the closing parenthesis. /// /// The input token list /// First token index in the input token list. Set to the last uncopied token index on return. /// List to recieve the output tokens /// True if a balanced expression was read, or false if the end of the list was encountered before finding a matching token static bool ReadBalancedToken(List inputTokens, ref int inputIdx, List outputTokens) { // Copy a single token to the output list Token token = inputTokens[inputIdx++]; outputTokens.Add(token); // If it was the start of a subexpression, copy until the closing parenthesis if (token.Type == TokenType.LeftParen) { // Copy the contents of the subexpression for (; ; ) { if (inputIdx == inputTokens.Count) { return false; } if (inputTokens[inputIdx].Type == TokenType.RightParen) { break; } if (!ReadBalancedToken(inputTokens, ref inputIdx, outputTokens)) { return false; } } // Copy the closing parenthesis token = inputTokens[inputIdx++]; outputTokens.Add(token); } return true; } /// /// Copies a single token from one list of tokens to another, or if it's an opening parenthesis, the entire subexpression until the closing parenthesis. /// /// The input token list /// List to recieve the output tokens /// The context that the parser is in /// True if a balanced expression was read, or false if the end of the list was encountered before finding a matching token static bool ReadBalancedToken(IEnumerator inputEnumerator, List outputTokens, PreprocessorContext context) { // Copy a single token to the output list Token token = inputEnumerator.Current; bool moveNext = inputEnumerator.MoveNext(); outputTokens.Add(token); // If it was the start of a subexpression, copy until the closing parenthesis if (token.Type == TokenType.LeftParen) { // Copy the contents of the subexpression for (; ; ) { if (!moveNext) { throw new PreprocessorException(context, "Unbalanced token sequence"); } if (inputEnumerator.Current.Type == TokenType.RightParen) { outputTokens.Add(inputEnumerator.Current); moveNext = inputEnumerator.MoveNext(); break; } moveNext = ReadBalancedToken(inputEnumerator, outputTokens, context); } } return moveNext; } } }