Files
UnrealEngine/Engine/Source/Programs/UnrealBuildTool/Preprocessor/Preprocessor.cs
2025-05-18 13:04:45 +08:00

1473 lines
51 KiB
C#

// 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
{
/// <summary>
/// Exception class for the preprocessor, which contains the file and position of the code causing an error
/// </summary>
class PreprocessorException : Exception
{
/// <summary>
/// The context when the error was encountered
/// </summary>
public readonly PreprocessorContext? Context;
/// <summary>
/// Constructor
/// </summary>
/// <param name="context">The current preprocesor context</param>
/// <param name="format">Format string, to be passed to String.Format</param>
/// <param name="args">Optional argument list for the format string</param>
public PreprocessorException(PreprocessorContext? context, string format, params object[] args)
: base(String.Format(format, args))
{
Context = context;
}
}
/// <summary>
/// Implementation of a C++ preprocessor.
/// </summary>
class Preprocessor
{
/// <summary>
/// Type of an include path
/// </summary>
public enum IncludePathType
{
/// <summary>
/// Regular include path, enclosed by quotes
/// </summary>
Normal,
/// <summary>
/// System include path, enclosed by angle brackets
/// </summary>
System,
}
/// <summary>
/// Include paths to look in
/// </summary>
readonly List<DirectoryItem> _includeDirectories = new();
/// <summary>
/// Framework paths to look in
/// </summary>
readonly List<DirectoryItem> _frameworkDirectories = new();
/// <summary>
/// Set of all included files with the #pragma once directive
/// </summary>
readonly HashSet<FileItem> _pragmaOnceFiles = new();
/// <summary>
/// Set of any files that has been processed
/// </summary>
readonly HashSet<FileItem> _processedFiles = new();
/// <summary>
/// The current state of the preprocessor
/// </summary>
readonly PreprocessorState _state = new();
/// <summary>
/// Predefined token containing the constant "0"
/// </summary>
static readonly byte[] s_zeroLiteral = Encoding.UTF8.GetBytes("0");
/// <summary>
/// Predefined token containing the constant "1"
/// </summary>
static readonly byte[] s_oneLiteral = Encoding.UTF8.GetBytes("1");
/// <summary>
/// Value of the __COUNTER__ variable
/// </summary>
int _counter;
/// <summary>
/// List of files included by the preprocessor
/// </summary>
/// <returns>Enumerable of processed files</returns>
public IEnumerable<FileItem> GetProcessedFiles()
{
return _processedFiles.AsEnumerable();
}
/// <summary>
/// Default constructor
/// </summary>
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, "\"<unknown>\"");
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
}
/// <summary>
/// Determines whether the current preprocessor branch is active
/// </summary>
/// <returns>True if the current branch is active</returns>
public bool IsCurrentBranchActive()
{
return _state.IsCurrentBranchActive();
}
/// <summary>
/// Defines a macro. May have an optional '=Value' suffix.
/// </summary>
/// <param name="definition">Macro to define</param>
public void AddDefinition(string definition)
{
List<Token> 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<Token> 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);
}
/// <summary>
/// Defines a macro
/// </summary>
/// <param name="name">Name of the macro</param>
/// <param name="value">String to be parsed for the macro's value</param>
public void AddDefinition(string name, string value)
{
List<Token> tokens = new();
TokenReader reader = new(value);
while (reader.MoveNext())
{
tokens.Add(reader.Current);
}
PreprocessorMacro macro = new(Identifier.FindOrAdd(name), null, tokens);
_state.DefineMacro(macro);
}
/// <summary>
/// Defines a macro
/// </summary>
/// <param name="macro">The macro definition</param>
public void AddDefinition(PreprocessorMacro macro)
{
_state.DefineMacro(macro);
}
/// <summary>
/// Adds an include path to the preprocessor
/// </summary>
/// <param name="directory">The include path</param>
public void AddIncludePath(DirectoryItem directory)
{
if (!_includeDirectories.Contains(directory))
{
_includeDirectories.Add(directory);
}
}
/// <summary>
/// Adds an include path to the preprocessor
/// </summary>
/// <param name="location">The include path</param>
public void AddIncludePath(DirectoryReference location)
{
DirectoryItem directory = DirectoryItem.GetItemByDirectoryReference(location);
if (!directory.Exists)
{
throw new FileNotFoundException("Unable to find " + location.FullName);
}
AddIncludePath(directory);
}
/// <summary>
/// Adds an include path to the preprocessor
/// </summary>
/// <param name="directoryName">The include path</param>
public void AddIncludePath(string directoryName)
{
AddIncludePath(new DirectoryReference(directoryName));
}
/// <summary>
/// Adds a framework path to the preprocessor
/// </summary>
/// <param name="directory">The framework path</param>
public void AddFrameworkPath(DirectoryItem directory)
{
if (!_frameworkDirectories.Contains(directory))
{
_frameworkDirectories.Add(directory);
}
}
/// <summary>
/// Adds a framework path to the preprocessor
/// </summary>
/// <param name="location">The framework path</param>
public void AddFrameworkPath(DirectoryReference location)
{
DirectoryItem directory = DirectoryItem.GetItemByDirectoryReference(location);
if (!directory.Exists)
{
throw new FileNotFoundException("Unable to find " + location.FullName);
}
AddFrameworkPath(directory);
}
/// <summary>
/// Adds a framework path to the preprocessor
/// </summary>
/// <param name="directoryName">The framework path</param>
public void AddFrameworkPath(string directoryName)
{
AddFrameworkPath(new DirectoryReference(directoryName));
}
/// <summary>
/// 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.
/// </summary>
/// <param name="context">The current preprocessor context</param>
/// <param name="includePath">The path appearing in an #include directive</param>
/// <param name="type">Specifies rules for how to resolve the include path (normal/system)</param>
/// <param name="file">If found, receives the resolved file</param>
/// <returns>True if the The resolved file</returns>
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;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="baseDirectory">The base directory to search from</param>
/// <param name="fragments">Fragments of the relative path to follow</param>
/// <param name="file">The file that was found, if successful</param>
/// <returns>True if the The resolved file</returns>
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);
}
/// <summary>
/// Parses a file recursively
/// </summary>
/// <param name="file">File to parse</param>
/// <param name="fragments">Lists of fragments that are parsed</param>
/// <param name="outerContext">Outer context information, for error messages</param>
/// <param name="sourceFileCache">Cache for source files</param>
/// <param name="logger">Logger for output</param>
/// <param name="showIncludes">Show all the included files, in order</param>
/// <param name="ignoreMissingIncludes">Suppress exceptions if an include path can not be resolved</param>
public void ParseFile(FileItem file, List<SourceFileFragment> 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++;
}
}
}
/// <summary>
/// Parse an include directive and resolve the file it references
/// </summary>
/// <param name="markup">Markup for the include directive</param>
/// <param name="context">Current preprocessor context</param>
/// <param name="ignoreMissingIncludes">Suppress exceptions if an include path can not be resolved</param>
/// <returns>Included file</returns>
FileItem? ParseIncludeDirective(SourceFileMarkup markup, PreprocessorFileContext context, bool ignoreMissingIncludes = false)
{
// Expand macros in the given tokens
List<Token> 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;
}
/// <summary>
/// Parse a source file fragment, using cached transforms if possible
/// </summary>
/// <param name="sourceFile">The source file being parsed</param>
/// <param name="fragment">Fragment to parse</param>
/// <param name="context">Current preprocessor context</param>
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;
}
}
}
}
/// <summary>
/// Validate and add a macro using the given parameter and token list
/// </summary>
/// <param name="context">The current preprocessor context</param>
/// <param name="name">Name of the macro</param>
/// <param name="parameters">Parameter list for the macro</param>
/// <param name="tokens">List of tokens</param>
void AddMacro(PreprocessorContext context, Identifier name, List<Identifier>? parameters, List<Token> 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));
}
/// <summary>
/// Set a predefined macro to a given value
/// </summary>
/// <param name="name">Name of the macro</param>
/// <param name="type">Type of the macro token</param>
/// <param name="value">Value of the macro</param>
/// <returns>The created macro</returns>
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> { token });
_state.DefineMacro(macro);
}
/// <summary>
/// Parse a marked up directive from a file
/// </summary>
/// <param name="type">The markup type</param>
/// <param name="tokens">Tokens for the directive</param>
/// <param name="context">The context that this markup is being parsed in</param>
public void ParseMarkup(SourceFileMarkupType type, List<Token> 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;
}
}
/// <summary>
/// Read a macro definition
/// </summary>
/// <param name="tokens">List of tokens in the directive</param>
/// <param name="context">The context that this directive is being parsed in</param>
public void ParseDefineDirective(List<Token> 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<Identifier>? parameters = null;
if (tokenIdx < tokens.Count && !tokens[tokenIdx].HasLeadingSpace && tokens[tokenIdx].Type == TokenType.LeftParen)
{
parameters = new List<Identifier>();
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());
}
}
/// <summary>
/// Parse an #undef directive
/// </summary>
/// <param name="tokens">List of tokens in the directive</param>
/// <param name="context">The context that this directive is being parsed in</param>
public void ParseUndefDirective(List<Token> 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!);
}
}
/// <summary>
/// Parse an #if directive
/// </summary>
/// <param name="tokens">List of tokens in the directive</param>
/// <param name="context">The context that this directive is being parsed in</param>
public void ParseIfDirective(List<Token> tokens, PreprocessorContext context)
{
PreprocessorBranch branch = PreprocessorBranch.HasIfDirective;
if (_state.IsCurrentBranchActive())
{
// Read a line into the buffer and expand the macros in it
List<Token> 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);
}
/// <summary>
/// Parse an #ifdef directive
/// </summary>
/// <param name="tokens">List of tokens in the directive</param>
/// <param name="context">The context that this directive is being parsed in</param>
public void ParseIfdefDirective(List<Token> 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);
}
/// <summary>
/// Parse an #ifndef directive
/// </summary>
/// <param name="tokens">List of tokens for this directive</param>
/// <param name="context">The context that this directive is being parsed in</param>
public void ParseIfndefDirective(List<Token> 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);
}
/// <summary>
/// Parse an #elif directive
/// </summary>
/// <param name="tokens">List of tokens for this directive</param>
/// <param name="context">The context that this directive is being parsed in</param>
public void ParseElifDirective(List<Token> 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<Token> 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);
}
/// <summary>
/// Parse an #else directive
/// </summary>
/// <param name="tokens">List of tokens in the directive</param>
/// <param name="context">The context that this directive is being parsed in</param>
public void ParseElseDirective(List<Token> 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);
}
/// <summary>
/// Parse an #endif directive
/// </summary>
/// <param name="tokens">List of tokens in the directive</param>
/// <param name="context">The context that this directive is being parsed in</param>
[SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "<Pending>")]
public void ParseEndifDirective(List<Token> 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");
}
}
/// <summary>
/// Parse a #pragma directive
/// </summary>
/// <param name="tokens">List of tokens in the directive</param>
/// <param name="context">The context that this directive is being parsed in</param>
public void ParsePragmaDirective(List<Token> 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();
}
}
}
/// <summary>
/// Expand macros in the given sequence.
/// </summary>
/// <param name="inputTokens">Sequence of input tokens</param>
/// <param name="outputTokens">List to receive the expanded tokens</param>
/// <param name="isConditional">Whether a conditional expression is being evaluated (and 'defined' expressions are valid)</param>
/// <param name="context">The context that this directive is being parsed in</param>
public void ExpandMacros(IEnumerable<Token> inputTokens, List<Token> outputTokens, bool isConditional, PreprocessorContext context)
{
List<PreprocessorMacro> ignoreMacros = new();
ExpandMacrosRecursively(inputTokens, outputTokens, isConditional, ignoreMacros, context);
}
/// <summary>
/// Expand macros in the given sequence, ignoring previously expanded macro names from a list.
/// </summary>
/// <param name="inputTokens">Sequence of input tokens</param>
/// <param name="outputTokens">List to receive the expanded tokens</param>
/// <param name="isConditional">Whether a conditional expression is being evaluated (and 'defined' expressions are valid)</param>
/// <param name="ignoreMacros">List of macros to ignore</param>
/// <param name="context">The context that this directive is being parsed in</param>
void ExpandMacrosRecursively(IEnumerable<Token> inputTokens, List<Token> outputTokens, bool isConditional, List<PreprocessorMacro> ignoreMacros, PreprocessorContext context)
{
IEnumerator<Token> inputEnumerator = inputTokens.GetEnumerator();
if (inputEnumerator.MoveNext())
{
for (; ; )
{
if (!ReadExpandedToken(inputEnumerator, outputTokens, isConditional, ignoreMacros, context))
{
break;
}
}
}
}
/// <summary>
/// Merges an optional leading space flag into the given token (recycling the original token if possible).
/// </summary>
/// <param name="token">The token to merge a leading space into</param>
/// <param name="hasLeadingSpace">The leading space flag</param>
/// <returns>New token with the leading space flag set, or the existing token</returns>
static Token MergeLeadingSpace(Token token, bool hasLeadingSpace)
{
Token result = token;
if (hasLeadingSpace && !result.HasLeadingSpace)
{
result = result.AddFlags(TokenFlags.HasLeadingSpace);
}
return result;
}
/// <summary>
/// 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).
/// </summary>
/// <param name="inputEnumerator">The enumerator of input tokens</param>
/// <param name="outputTokens">List to receive the expanded tokens</param>
/// <param name="isConditional">Whether a conditional expression is being evaluated (and 'defined' expressions are valid)</param>
/// <param name="ignoreMacros">List of macros to ignore</param>
/// <param name="context">The context that this directive is being parsed in</param>
/// <returns>Result from calling the enumerator's MoveNext() method</returns>
bool ReadExpandedToken(IEnumerator<Token> inputEnumerator, List<Token> outputTokens, bool isConditional, List<PreprocessorMacro> 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<Token> 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;
}
/// <summary>
/// Gets a string for the __FILE__ macro
/// </summary>
/// <param name="context">Context to scan to find the current file</param>
/// <returns>String representing the current context</returns>
static string GetCurrentFileMacroValue(PreprocessorContext context)
{
SourceFile? sourceFile = GetCurrentSourceFile(context);
if (sourceFile == null)
{
return "<unknown>";
}
else
{
return sourceFile.Location.FullName;
}
}
/// <summary>
/// Gets a string for the current file
/// </summary>
/// <param name="context">Context to scan to find the current file</param>
/// <returns>Current source file being parsed</returns>
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;
}
/// <summary>
/// Gets the current line number
/// </summary>
/// <param name="context">Context to scan to find the current file</param>
/// <returns>Line number in the first file encountered</returns>
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;
}
/// <summary>
/// Expand an object macro
/// </summary>
/// <param name="macro">The functional macro</param>
/// <param name="outputTokens">The list to receive the output tokens</param>
/// <param name="isConditional">Whether the macro is being expanded in a conditional context, allowing use of the 'defined' keyword</param>
/// <param name="ignoreMacros">List of macros currently being expanded, which should be ignored for recursion</param>
/// <param name="context">The context that this directive is being parsed in</param>
void ExpandObjectMacro(PreprocessorMacro macro, List<Token> outputTokens, bool isConditional, List<PreprocessorMacro> 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--;
}
}
}
}
/// <summary>
/// Expand a function macro
/// </summary>
/// <param name="macro">The functional macro</param>
/// <param name="argumentListTokens">Identifiers for each argument token</param>
/// <param name="outputTokens">The list to receive the output tokens</param>
/// <param name="isConditional">Whether the macro is being expanded in a conditional context, allowing use of the 'defined' keyword</param>
/// <param name="ignoreMacros">List of macros currently being expanded, which should be ignored for recursion</param>
/// <param name="context">The context that this macro is being expanded</param>
void ExpandFunctionMacro(PreprocessorMacro macro, List<Token> argumentListTokens, List<Token> outputTokens, bool isConditional, List<PreprocessorMacro> 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<List<Token>> arguments = new();
if (argumentListTokens.Count > 2)
{
for (int idx = 1; ; idx++)
{
if (!macro.HasVariableArgumentList || arguments.Count < macro.Parameters!.Count)
{
arguments.Add(new List<Token>());
}
List<Token> 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<Token> { 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<List<Token>> expandedArguments = new();
for (int idx = 0; idx < arguments.Count; idx++)
{
List<Token> newArguments = new();
ExpandMacrosRecursively(arguments[idx], newArguments, isConditional, ignoreMacros, context);
expandedArguments.Add(newArguments);
}
// Substitute all the argument tokens
List<Token> 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<Token> 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);
}
/// <summary>
/// Appends a list of tokens to another list, setting the leading whitespace flag to the given value
/// </summary>
/// <param name="outputTokens">List to receive the appended tokens</param>
/// <param name="inputTokens">List of tokens to append</param>
/// <param name="hasLeadingSpace">Whether there is space before the first token</param>
static void AppendTokensWithWhitespace(List<Token> outputTokens, List<Token> inputTokens, bool hasLeadingSpace)
{
if (inputTokens.Count > 0)
{
outputTokens.Add(MergeLeadingSpace(inputTokens[0], hasLeadingSpace));
outputTokens.AddRange(inputTokens.Skip(1));
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="inputTokens">The input token list</param>
/// <param name="inputIdx">First token index in the input token list. Set to the last uncopied token index on return.</param>
/// <param name="outputTokens">List to recieve the output tokens</param>
/// <returns>True if a balanced expression was read, or false if the end of the list was encountered before finding a matching token</returns>
static bool ReadBalancedToken(List<Token> inputTokens, ref int inputIdx, List<Token> 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;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="inputEnumerator">The input token list</param>
/// <param name="outputTokens">List to recieve the output tokens</param>
/// <param name="context">The context that the parser is in</param>
/// <returns>True if a balanced expression was read, or false if the end of the list was encountered before finding a matching token</returns>
static bool ReadBalancedToken(IEnumerator<Token> inputEnumerator, List<Token> 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;
}
}
}