// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Diagnostics; using System.IO; using System.Collections; using System.Collections.Concurrent; using EpicGames.Core; using System.Diagnostics.CodeAnalysis; using UnrealBuildBase; using Microsoft.Extensions.Logging; 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 /// 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)) { this.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 /// List IncludeDirectories = new List(); /// /// Framework paths to look in /// List FrameworkDirectories = new List(); /// /// Set of all included files with the #pragma once directive /// HashSet PragmaOnceFiles = new HashSet(); /// /// Set of any files that has been processed /// HashSet ProcessedFiles = new HashSet(); /// /// The current state of the preprocessor /// PreprocessorState State = new PreprocessorState(); /// /// Predefined token containing the constant "0" /// static readonly byte[] ZeroLiteral = Encoding.UTF8.GetBytes("0"); /// /// Predefined token containing the constant "1" /// static readonly byte[] 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 List(); if(Tokens.Count == 1) { ValueTokens.Add(new Token(TokenType.Number, TokenFlags.None, 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 PreprocessorMacro(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 List(); TokenReader Reader = new TokenReader(Value); while(Reader.MoveNext()) { Tokens.Add(Reader.Current); } PreprocessorMacro Macro = new PreprocessorMacro(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) { PreprocessorFileContext? OuterFileContext = OuterContext as PreprocessorFileContext; if(OuterFileContext != null) { FileItem? ResolvedFile; if(TryResolveRelativeIncludePath(OuterFileContext.Directory, Fragments, out ResolvedFile)) { File = ResolvedFile; return true; } } } } // Try to match the include path against any of the system directories foreach(DirectoryItem BaseDirectory in IncludeDirectories) { FileItem? ResolvedFile; if(TryResolveRelativeIncludePath(BaseDirectory, Fragments, out 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 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[Fragments.Length - 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 bShowIncludes = false, bool bIgnoreMissingIncludes = false) { // If the file has already been included and had a #pragma once directive, don't include it again if(PragmaOnceFiles.Contains(File)) { return; } if (!ProcessedFiles.Contains(File)) { ProcessedFiles.Add(File); } // Output a trace of the included files if(bShowIncludes) { 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 PreprocessorFileContext(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, bIgnoreMissingIncludes); // Parse the included file if (IncludedFile != null) { ParseFile(IncludedFile, Fragments, Context, SourceFileCache, Logger, bShowIncludes, bIgnoreMissingIncludes); } } 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 bIgnoreMissingIncludes = false) { // Expand macros in the given tokens List ExpandedTokens = new List(); 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[IncludeToken.Length - 1] == '\"') { Type = IncludePathType.Normal; } else if(IncludeToken.Length >= 2 && IncludeToken[0] == '<' && IncludeToken[IncludeToken.Length - 1] == '>') { Type = IncludePathType.System; } else { throw new PreprocessorException(Context, "Couldn't resolve include '{0}'", IncludeToken); } // Get the include path string IncludePath = IncludeToken.Substring(1, IncludeToken.Length - 2); // Resolve the included file FileItem? IncludedFile; if(!TryResolveIncludePath(Context, IncludePath, Type, out IncludedFile)) { if (bIgnoreMissingIncludes) { 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.bHasPragmaOnce) { 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[Tokens.Count - 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[Parameters.Count - 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 Token(Type, TokenFlags.None, Value); PreprocessorMacro Macro = new PreprocessorMacro(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 List(); 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 PreprocessorBranch Branch; if (!State.TryPopBranch(out 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 List(); 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 PreprocessorBranch Branch; if (!State.TryPopBranch(out 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 public void ParseEndifDirective(List Tokens, PreprocessorContext Context) { // Pop the branch off the stack PreprocessorBranch Branch; if(!State.TryPopBranch(out Branch)) { 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 bIsConditional, PreprocessorContext Context) { List IgnoreMacros = new List(); ExpandMacrosRecursively(InputTokens, OutputTokens, bIsConditional, 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 bIsConditional, List IgnoreMacros, PreprocessorContext Context) { IEnumerator InputEnumerator = InputTokens.GetEnumerator(); if(InputEnumerator.MoveNext()) { for(;;) { if(!ReadExpandedToken(InputEnumerator, OutputTokens, bIsConditional, 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 Token MergeLeadingSpace(Token Token, bool bHasLeadingSpace) { Token Result = Token; if(bHasLeadingSpace && !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 bIsConditional, List IgnoreMacros, PreprocessorContext Context) { // Capture the first token, then move to the next OutputTokens.Add(InputEnumerator.Current); bool bMoveNext = InputEnumerator.MoveNext(); // If it's an identifier, try to resolve it as a macro if (OutputTokens[OutputTokens.Count - 1].Identifier == Identifiers.Defined && bIsConditional) { // Remove the 'defined' keyword OutputTokens.RemoveAt(OutputTokens.Count - 1); // Make sure there's another token if (!bMoveNext) { 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!)? OneLiteral : ZeroLiteral)); bMoveNext = InputEnumerator.MoveNext(); } else { // Repeatedly try to expand the last token into the list while(OutputTokens[OutputTokens.Count - 1].Type == TokenType.Identifier && !OutputTokens[OutputTokens.Count - 1].Flags.HasFlag(TokenFlags.DisableExpansion)) { // Try to get a macro for the current token PreprocessorMacro? Macro; if (!State.TryGetMacro(OutputTokens[OutputTokens.Count - 1].Identifier!, out Macro) || IgnoreMacros.Contains(Macro)) { break; } if(Macro.IsFunctionMacro && (!bMoveNext || InputEnumerator.Current.Type != TokenType.LeftParen)) { break; } // Remove the macro name from the output list bool bHasLeadingSpace = OutputTokens[OutputTokens.Count - 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, bIsConditional, IgnoreMacros, Context); } else { // Read balanced token for argument list List ArgumentTokens = new List(); bMoveNext = ReadBalancedToken(InputEnumerator, ArgumentTokens, Context); // Expand the macro tokens into the output buffer ExpandFunctionMacro(Macro, ArgumentTokens, OutputTokens, bIsConditional, 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], bHasLeadingSpace); // 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 bMoveNext; } /// /// Gets a string for the __FILE__ macro /// /// Context to scan to find the current file /// String representing the current context 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 SourceFile? GetCurrentSourceFile(PreprocessorContext Context) { SourceFile? SourceFile = null; for(PreprocessorContext? OuterContext = Context; OuterContext != null; OuterContext = OuterContext.Outer) { PreprocessorFileContext? OuterFileContext = OuterContext as PreprocessorFileContext; if(OuterFileContext != null) { 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 int GetCurrentLine(PreprocessorContext Context) { for(PreprocessorContext? OuterContext = Context; OuterContext != null; OuterContext = OuterContext.Outer) { PreprocessorFileContext? OuterFileContext = OuterContext as PreprocessorFileContext; if(OuterFileContext != null) { 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 bIsConditional, 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 Token(TokenType.String, TokenFlags.None, String.Format("\"{0}\"", GetCurrentFileMacroValue(Context).Replace("\\", "\\\\"))); OutputTokens.Add(Token); } else if(Macro.Name == Identifiers.__LINE__) { Token Token = new Token(TokenType.Number, TokenFlags.None, GetCurrentLine(Context).ToString()); OutputTokens.Add(Token); } else if(Macro.Name == Identifiers.__COUNTER__) { Token Token = new Token(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, bIsConditional, 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 bIsConditional, 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 List>(); if(ArgumentListTokens.Count > 2) { for(int Idx = 1;;Idx++) { if (!Macro.HasVariableArgumentList || Arguments.Count < Macro.Parameters!.Count) { Arguments.Add(new List()); } List Argument = Arguments[Arguments.Count - 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[Arguments.Count - 1][0].HasLeadingSpace) { Argument[0] = Argument[0].RemoveFlags(TokenFlags.HasLeadingSpace); } bool bHasLeadingSpace = false; for(int TokenIdx = 0; TokenIdx < Argument.Count; TokenIdx++) { if(Argument[TokenIdx].Text.Length == 0) { bHasLeadingSpace |= Argument[TokenIdx].HasLeadingSpace; Argument.RemoveAt(TokenIdx--); } else { Argument[TokenIdx] = MergeLeadingSpace(Argument[TokenIdx], bHasLeadingSpace); bHasLeadingSpace = false; } } if (Argument.Count == 0) { Argument.Add(new Token(TokenType.Placemarker, TokenFlags.None)); Argument.Add(new Token(TokenType.Placemarker, bHasLeadingSpace? 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[Arguments.Count - 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 Token(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 List>(); for (int Idx = 0; Idx < Arguments.Count; Idx++) { List NewArguments = new List(); ExpandMacrosRecursively(Arguments[Idx], NewArguments, bIsConditional, IgnoreMacros, Context); ExpandedArguments.Add(NewArguments); } // Substitute all the argument tokens List ExpandedTokens = new List(); 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[ExpandedArguments.Count - 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, bIsConditional, 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 void AppendTokensWithWhitespace(List OutputTokens, List InputTokens, bool bHasLeadingSpace) { if(InputTokens.Count > 0) { OutputTokens.Add(MergeLeadingSpace(InputTokens[0], bHasLeadingSpace)); 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 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 bool ReadBalancedToken(IEnumerator InputEnumerator, List OutputTokens, PreprocessorContext Context) { // Copy a single token to the output list Token Token = InputEnumerator.Current; bool bMoveNext = 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 (!bMoveNext) { throw new PreprocessorException(Context, "Unbalanced token sequence"); } if (InputEnumerator.Current.Type == TokenType.RightParen) { OutputTokens.Add(InputEnumerator.Current); bMoveNext = InputEnumerator.MoveNext(); break; } bMoveNext = ReadBalancedToken(InputEnumerator, OutputTokens, Context); } } return bMoveNext; } } }