Path: blob/master/dep/reshadefx/src/effect_preprocessor.cpp
4246 views
/*1* Copyright (C) 2014 Patrick Mours2* SPDX-License-Identifier: BSD-3-Clause3*/45#include "effect_lexer.hpp"6#include "effect_preprocessor.hpp"7#include <cstdio> // fclose, fopen, fread, fseek8#include <cassert>9#include <algorithm> // std::find_if1011#ifndef _WIN3212// On Linux systems the native path encoding is UTF-8 already, so no conversion necessary13#define u8path(p) path(p)14#define u8string() string()15#endif1617enum op_type18{19op_none = -1,2021op_or,22op_and,23op_bitor,24op_bitxor,25op_bitand,26op_not_equal,27op_equal,28op_less,29op_greater,30op_less_equal,31op_greater_equal,32op_leftshift,33op_rightshift,34op_add,35op_subtract,36op_modulo,37op_divide,38op_multiply,39op_plus,40op_negate,41op_not,42op_bitnot,43op_parentheses44};4546enum macro_replacement47{48macro_replacement_start = '\x00',49macro_replacement_argument = '\xFD',50macro_replacement_concat = '\xFF',51macro_replacement_stringize = '\xFE',52};5354static const int s_precedence_lookup[] = {550, 1, 2, 3, 4, // bitwise operators565, 6, 7, 7, 7, 7, // logical operators578, 8, // left shift, right shift589, 9, // add, subtract5910, 10, 10, // modulo, divide, multiply6011, 11, 11, 11 // unary operators61};6263static bool read_file(const std::string &path, std::string &file_data, reshadefx::preprocessor::include_read_file_callback &cb)64{65if (!cb(path, file_data))66return false;6768// Append a new line feed to the end of the input string to avoid issues with parsing69file_data.push_back('\n');7071// Remove BOM (0xefbbbf means 0xfeff)72if (file_data.size() >= 3 &&73static_cast<unsigned char>(file_data[0]) == 0xef &&74static_cast<unsigned char>(file_data[1]) == 0xbb &&75static_cast<unsigned char>(file_data[2]) == 0xbf)76file_data.erase(0, 3);7778return true;79}8081bool reshadefx::preprocessor::stdfs_read_file_callback(const std::string &path, std::string &data)82{83// Read file contents into memory84const std::filesystem::path fspath(path);85#ifndef _WIN3286FILE *const file = fopen(fspath.c_str(), "rb");87#else88FILE *const file = _wfsopen(fspath.generic_wstring().c_str(), L"rb", SH_DENYWR);89#endif90if (file == nullptr)91return false;9293fseek(file, 0, SEEK_END);94const size_t file_size = ftell(file);95fseek(file, 0, SEEK_SET);9697const size_t file_size_read = fread(data.data(), 1, file_size, file);9899// No longer need to have a handle open to the file, since all data was read, so can safely close it100fclose(file);101102if (file_size_read != file_size)103return false;104105return true;106}107108bool reshadefx::preprocessor::stdfs_file_exists_callback(const std::string &path)109{110return std::filesystem::exists(std::filesystem::path(path));111}112113template <char ESCAPE_CHAR = '\\'>114static std::string escape_string(std::string s)115{116for (size_t offset = 0; (offset = s.find(ESCAPE_CHAR, offset)) != std::string::npos; offset += 2)117s.insert(offset, "\\", 1);118return '\"' + s + '\"';119}120121reshadefx::preprocessor::preprocessor()122: _file_exists_cb(stdfs_file_exists_callback)123, _read_file_cb(stdfs_read_file_callback)124{125}126reshadefx::preprocessor::~preprocessor()127{128}129130void reshadefx::preprocessor::set_include_callbacks(include_file_exists_callback file_exists,131include_read_file_callback read_file)132{133_file_exists_cb = file_exists;134_read_file_cb = read_file;135}136137void reshadefx::preprocessor::add_include_path(const std::string &path)138{139assert(!path.empty());140_include_paths.push_back(std::filesystem::path(path));141}142bool reshadefx::preprocessor::add_macro_definition(const std::string &name, const macro ¯o)143{144assert(!name.empty());145return _macros.emplace(name, macro).second;146}147148bool reshadefx::preprocessor::append_file(const std::string &path)149{150std::string source_code;151if (!read_file(path, source_code, _read_file_cb))152return false;153154return append_string(std::move(source_code), path);155}156bool reshadefx::preprocessor::append_string(std::string source_code, const std::string &path /* = std::string() */)157{158// Enforce all input strings to end with a line feed159assert(!source_code.empty() && source_code.back() == '\n');160161// Only consider new errors added below for the success of this call162const size_t errors_offset = _errors.length();163164// Give this push a name, so that lexer location starts at a new line165// This is necessary in case this string starts with a preprocessor directive, since the lexer only reports those as such if they appear at the beginning of a new line166// But without a name, the lexer location is set to the last token location, which most likely will not be at the start of the line167push(std::move(source_code), path.empty() ? "unknown" : path);168parse();169170return _errors.find(": preprocessor error: ", errors_offset) == std::string::npos;171}172173std::vector<std::filesystem::path> reshadefx::preprocessor::included_files() const174{175std::vector<std::filesystem::path> files;176files.reserve(_file_cache.size());177for (const std::pair<std::string, std::string> &cache_entry : _file_cache)178files.push_back(std::filesystem::u8path(cache_entry.first));179return files;180}181std::vector<std::pair<std::string, std::string>> reshadefx::preprocessor::used_macro_definitions() const182{183std::vector<std::pair<std::string, std::string>> defines;184defines.reserve(_used_macros.size());185for (const std::string &name : _used_macros)186if (const auto it = _macros.find(name);187// Do not include function-like macros, since they are more likely to contain a complex replacement list188it != _macros.end() && !it->second.is_function_like)189defines.emplace_back(name, it->second.replacement_list);190return defines;191}192193void reshadefx::preprocessor::error(const location &location, const std::string &message)194{195_errors += location.source;196_errors += '(' + std::to_string(location.line) + ", " + std::to_string(location.column) + ')';197_errors += ": preprocessor error: ";198_errors += message;199_errors += '\n';200}201void reshadefx::preprocessor::warning(const location &location, const std::string &message)202{203_errors += location.source;204_errors += '(' + std::to_string(location.line) + ", " + std::to_string(location.column) + ')';205_errors += ": preprocessor warning: ";206_errors += message;207_errors += '\n';208}209210void reshadefx::preprocessor::push(std::string input, const std::string &name)211{212location start_location = !name.empty() ?213// Start at the beginning of the file when pushing a new file214location(name, 1) :215// Start with last known token location when pushing an unnamed string216_token.location;217218input_level level = { name };219level.lexer.reset(new lexer(220std::move(input),221true /* ignore_comments */,222false /* ignore_whitespace */,223false /* ignore_pp_directives */,224false /* ignore_line_directives */,225true /* ignore_keywords */,226false /* escape_string_literals */,227start_location));228level.next_token.id = tokenid::unknown;229level.next_token.location = start_location; // This is used in 'consume' to initialize the output location230231// Inherit hidden macros from parent232if (!_input_stack.empty())233level.hidden_macros = _input_stack.back().hidden_macros;234235_input_stack.push_back(std::move(level));236_next_input_index = _input_stack.size() - 1;237238// Advance into the input stack to update next token239consume();240}241242bool reshadefx::preprocessor::peek(tokenid tokid) const243{244if (_input_stack.empty())245return tokid == tokenid::end_of_file;246247return _input_stack[_next_input_index].next_token == tokid;248}249void reshadefx::preprocessor::consume()250{251_current_input_index = _next_input_index;252253if (_input_stack.empty())254{255// End of input has been reached already (this can happen when the input text is not terminated with a new line)256assert(_current_input_index == 0);257return;258}259260// Clear out input stack, now that the current token is overwritten261while (_input_stack.size() > (_current_input_index + 1))262_input_stack.pop_back();263264// Update location information after switching input levels265input_level &input = _input_stack[_current_input_index];266if (!input.name.empty() && input.name != _output_location.source)267{268_output += "#line " + std::to_string(input.next_token.location.line) + " \"" + input.name + "\"\n";269// Line number is increased before checking against next token in 'tokenid::end_of_line' handling in 'parse' function below, so compensate for that here270_output_location.line = input.next_token.location.line - 1;271_output_location.source = input.name;272}273274// Set current token275_token = std::move(input.next_token);276_current_token_raw_data = input.lexer->input_string().substr(_token.offset, _token.length);277278// Get the next token279input.next_token = input.lexer->lex();280281// Verify string literals (since the lexer cannot throw errors itself)282if (_token == tokenid::string_literal && _current_token_raw_data.back() != '\"')283error(_token.location, "unterminated string literal");284285// Pop input level if lexical analysis has reached the end of it286// This ensures the EOF token is not consumed until the very last file287while (peek(tokenid::end_of_file))288{289// Remove any unterminated blocks from the stack290for (; !_if_stack.empty() && _if_stack.back().input_index >= _next_input_index; _if_stack.pop_back())291error(_if_stack.back().pp_token.location, "unterminated #if");292293if (_next_input_index == 0)294{295// End of input has been reached, so cannot pop further and this is the last token296_input_stack.pop_back();297return;298}299else300{301_next_input_index -= 1;302}303}304}305void reshadefx::preprocessor::consume_until(tokenid tokid)306{307while (!accept(tokid) && !peek(tokenid::end_of_file))308{309consume();310}311}312313bool reshadefx::preprocessor::accept(tokenid tokid, bool ignore_whitespace)314{315if (ignore_whitespace)316{317while (peek(tokenid::space))318{319consume();320}321}322323if (peek(tokid))324{325consume();326return true;327}328329return false;330}331bool reshadefx::preprocessor::expect(tokenid tokid)332{333if (!accept(tokid))334{335if (_input_stack.empty())336return tokid == tokenid::end_of_line || tokid == tokenid::end_of_file;337338token actual_token = _input_stack[_next_input_index].next_token;339actual_token.location.source = _output_location.source;340341if (actual_token == tokenid::end_of_line)342error(actual_token.location, "syntax error: unexpected new line");343else344error(actual_token.location, "syntax error: unexpected token '" +345_input_stack[_next_input_index].lexer->input_string().substr(actual_token.offset, actual_token.length) + '\'');346347return false;348}349350return true;351}352353void reshadefx::preprocessor::parse()354{355std::string line;356357// Consume all tokens in the input358while (!peek(tokenid::end_of_file))359{360consume();361362_recursion_count = 0;363364const bool skip = !_if_stack.empty() && _if_stack.back().skipping;365366switch (_token)367{368case tokenid::hash_if:369parse_if();370if (!skip && !expect(tokenid::end_of_line))371consume_until(tokenid::end_of_line);372continue;373case tokenid::hash_ifdef:374parse_ifdef();375if (!skip && !expect(tokenid::end_of_line))376consume_until(tokenid::end_of_line);377continue;378case tokenid::hash_ifndef:379parse_ifndef();380if (!skip && !expect(tokenid::end_of_line))381consume_until(tokenid::end_of_line);382continue;383case tokenid::hash_else:384parse_else();385if (!skip && !expect(tokenid::end_of_line))386consume_until(tokenid::end_of_line);387continue;388case tokenid::hash_elif:389parse_elif();390if (!skip && !expect(tokenid::end_of_line))391consume_until(tokenid::end_of_line);392continue;393case tokenid::hash_endif:394parse_endif();395if (!skip && !expect(tokenid::end_of_line))396consume_until(tokenid::end_of_line);397continue;398default:399// All other tokens are handled below400break;401}402403if (skip)404// Ignore token since the current section is disabled405continue;406407switch (_token)408{409case tokenid::hash_def:410parse_def();411if (!expect(tokenid::end_of_line))412consume_until(tokenid::end_of_line);413continue;414case tokenid::hash_undef:415parse_undef();416if (!expect(tokenid::end_of_line))417consume_until(tokenid::end_of_line);418continue;419case tokenid::hash_error:420parse_error();421if (!expect(tokenid::end_of_line))422consume_until(tokenid::end_of_line);423continue;424case tokenid::hash_warning:425parse_warning();426if (!expect(tokenid::end_of_line))427consume_until(tokenid::end_of_line);428continue;429case tokenid::hash_pragma:430parse_pragma();431if (!expect(tokenid::end_of_line))432consume_until(tokenid::end_of_line);433continue;434case tokenid::hash_include:435parse_include();436continue;437case tokenid::hash_unknown:438// Standalone "#" is valid and should be ignored439if (_token.length != 0)440error(_token.location, "unrecognized preprocessing directive '" + _token.literal_as_string + '\'');441if (!expect(tokenid::end_of_line))442consume_until(tokenid::end_of_line);443continue;444case tokenid::end_of_line:445if (line.empty())446continue; // Do not append empty lines to output, instead emit "#line" statements447_output_location.line++;448if (_token.location.line != _output_location.line)449{450_output += "#line " + std::to_string(_token.location.line) + '\n';451_output_location.line = _token.location.line;452}453_output += line;454_output += '\n';455line.clear();456continue;457case tokenid::identifier:458if (evaluate_identifier_as_macro())459continue;460[[fallthrough]];461default:462line += _current_token_raw_data;463break;464}465}466467// Append the last line after the EOF token was reached to the output468_output += line;469_output += '\n';470}471472void reshadefx::preprocessor::parse_def()473{474if (!expect(tokenid::identifier))475return;476if (_token.literal_as_string == "defined")477return warning(_token.location, "macro name 'defined' is reserved");478479macro m;480const location location = std::move(_token.location);481const std::string macro_name = std::move(_token.literal_as_string);482483// Only create function-like macro if the parenthesis follows the macro name without any whitespace between484if (accept(tokenid::parenthesis_open, false))485{486m.is_function_like = true;487488while (accept(tokenid::identifier))489{490m.parameters.push_back(_token.literal_as_string);491492if (!accept(tokenid::comma))493break;494}495496if (accept(tokenid::ellipsis))497m.is_variadic = true;498499if (!expect(tokenid::parenthesis_close))500return;501}502503create_macro_replacement_list(m);504505if (!add_macro_definition(macro_name, m))506return error(location, "redefinition of '" + macro_name + "'");507}508void reshadefx::preprocessor::parse_undef()509{510if (!expect(tokenid::identifier))511return;512if (_token.literal_as_string == "defined")513return warning(_token.location, "macro name 'defined' is reserved");514515_macros.erase(_token.literal_as_string);516}517518void reshadefx::preprocessor::parse_if()519{520if_level level;521level.pp_token = _token;522level.input_index = _current_input_index;523524const bool parent_skipping = !_if_stack.empty() && _if_stack.back().skipping;525if (parent_skipping)526{527level.value = false;528level.skipping = true;529}530else531{532// Evaluate expression after updating 'pp_token', so that it points at the beginning # token533level.value = evaluate_expression();534level.skipping = !level.value;535}536537_if_stack.push_back(std::move(level));538}539void reshadefx::preprocessor::parse_ifdef()540{541if_level level;542level.pp_token = _token;543level.input_index = _current_input_index;544545if (!expect(tokenid::identifier))546return;547548const bool parent_skipping = !_if_stack.empty() && _if_stack.back().skipping;549if (parent_skipping)550{551level.value = false;552level.skipping = true;553}554else555{556level.value = is_defined(_token.literal_as_string);557level.skipping = !level.value;558559// Only add to used macro list if this #ifdef is active and the macro was not defined before560if (const auto it = _macros.find(_token.literal_as_string); it == _macros.end() || it->second.is_predefined)561_used_macros.emplace(_token.literal_as_string);562}563564_if_stack.push_back(std::move(level));565}566void reshadefx::preprocessor::parse_ifndef()567{568if_level level;569level.pp_token = _token;570level.input_index = _current_input_index;571572if (!expect(tokenid::identifier))573return;574575const bool parent_skipping = !_if_stack.empty() && _if_stack.back().skipping;576if (parent_skipping)577{578level.value = false;579level.skipping = true;580}581else582{583level.value = !is_defined(_token.literal_as_string);584level.skipping = !level.value;585586// Only add to used macro list if this #ifndef is active and the macro was not defined before587if (const auto it = _macros.find(_token.literal_as_string); it == _macros.end() || it->second.is_predefined)588_used_macros.emplace(_token.literal_as_string);589}590591_if_stack.push_back(std::move(level));592}593void reshadefx::preprocessor::parse_elif()594{595if (_if_stack.empty())596return error(_token.location, "missing #if for #elif");597598if_level &level = _if_stack.back();599if (level.pp_token == tokenid::hash_else)600return error(_token.location, "#elif is not allowed after #else");601602// Update 'pp_token' before evaluating expression, so that it points at the beginning # token603level.pp_token = _token;604level.input_index = _current_input_index;605606const bool parent_skipping = _if_stack.size() > 1 && _if_stack[_if_stack.size() - 2].skipping;607if (parent_skipping)608{609level.value = false;610level.skipping = true;611}612else613{614const bool condition_result = evaluate_expression();615level.skipping = level.value || !condition_result;616617if (!level.value)618level.value = condition_result;619}620}621void reshadefx::preprocessor::parse_else()622{623if (_if_stack.empty())624return error(_token.location, "missing #if for #else");625626if_level &level = _if_stack.back();627if (level.pp_token == tokenid::hash_else)628return error(_token.location, "#else is not allowed after #else");629630level.pp_token = _token;631level.input_index = _current_input_index;632633const bool parent_skipping = _if_stack.size() > 1 && _if_stack[_if_stack.size() - 2].skipping;634if (parent_skipping)635{636level.value = false;637level.skipping = true;638}639else640{641level.skipping = parent_skipping || level.value;642643if (!level.value)644level.value = true;645}646}647void reshadefx::preprocessor::parse_endif()648{649if (_if_stack.empty())650return error(_token.location, "missing #if for #endif");651652_if_stack.pop_back();653}654655void reshadefx::preprocessor::parse_error()656{657const location keyword_location = std::move(_token.location);658659if (!expect(tokenid::string_literal))660return;661662error(keyword_location, _token.literal_as_string);663}664void reshadefx::preprocessor::parse_warning()665{666const location keyword_location = std::move(_token.location);667668if (!expect(tokenid::string_literal))669return;670671warning(keyword_location, _token.literal_as_string);672}673674void reshadefx::preprocessor::parse_pragma()675{676const location keyword_location = std::move(_token.location);677678if (!expect(tokenid::identifier))679return;680681std::string pragma = std::move(_token.literal_as_string);682std::string pragma_args;683684// Ignore whitespace preceding the argument list685accept(tokenid::space);686687while (!peek(tokenid::end_of_line) && !peek(tokenid::end_of_file))688{689consume();690691if (_token == tokenid::identifier && evaluate_identifier_as_macro())692continue;693694// Collapse all whitespace down to a single space695if (_token == tokenid::space)696pragma_args += ' ';697else698pragma_args += _current_token_raw_data;699}700701if (pragma == "once")702{703// Clear file contents, so that future include statements simply push an empty string instead of these file contents again704if (const auto it = _file_cache.find(_output_location.source); it != _file_cache.end())705it->second.clear();706return;707}708709if (pragma == "warning" || pragma == "reshade")710{711_used_pragmas.emplace_back(std::move(pragma), std::move(pragma_args));712return;713}714715warning(keyword_location, "unknown pragma ignored");716}717718void reshadefx::preprocessor::parse_include()719{720const location keyword_location = std::move(_token.location);721722while (accept(tokenid::identifier))723{724if (!evaluate_identifier_as_macro())725{726error(_token.location, "syntax error: unexpected identifier in #include");727consume_until(tokenid::end_of_line);728return;729}730}731732if (!expect(tokenid::string_literal))733{734consume_until(tokenid::end_of_line);735return;736}737738std::filesystem::path file_name = std::filesystem::u8path(_token.literal_as_string);739std::filesystem::path file_path = std::filesystem::u8path(_output_location.source);740file_path.replace_filename(file_name);741742if (!_file_exists_cb(file_path.u8string()))743for (const std::filesystem::path &include_path : _include_paths)744if (_file_exists_cb((file_path = include_path / file_name).u8string()))745break;746747const std::string file_path_string = file_path.u8string();748749// Detect recursive include and abort to avoid infinite loop750if (std::find_if(_input_stack.begin(), _input_stack.end(),751[&file_path_string](const input_level &level) { return level.name == file_path_string; }) != _input_stack.end())752return error(_token.location, "recursive #include");753754std::string input;755if (const auto it = _file_cache.find(file_path_string); it != _file_cache.end())756{757input = it->second;758}759else760{761if (!read_file(file_path_string, input, _read_file_cb))762return error(keyword_location, "could not open included file '" + file_name.u8string() + '\'');763764_file_cache.emplace(file_path_string, input);765}766767// Skip end of line character following the include statement before pushing, so that the line number is already pointing to the next line when popping out of it again768if (!expect(tokenid::end_of_line))769consume_until(tokenid::end_of_line);770771// Clear out input stack before pushing include, so that hidden macros do not bleed into the include772while (_input_stack.size() > (_next_input_index + 1))773_input_stack.pop_back();774775push(std::move(input), file_path_string);776}777778bool reshadefx::preprocessor::evaluate_expression()779{780struct rpn_token781{782int value;783bool is_op;784};785786size_t rpn_index = 0;787size_t stack_index = 0;788const size_t STACK_SIZE = 128;789rpn_token rpn[STACK_SIZE];790int stack[STACK_SIZE];791792// Keep track of previous token to figure out data type of expression793tokenid previous_token = _token;794795// Run shunting-yard algorithm796while (!peek(tokenid::end_of_line) && !peek(tokenid::end_of_file))797{798if (stack_index >= STACK_SIZE || rpn_index >= STACK_SIZE)799return error(_token.location, "expression evaluator ran out of stack space"), false;800801consume();802803auto op = op_none;804bool left_associative = true;805bool parenthesis_matched = false;806807switch (_token)808{809case tokenid::space:810continue;811case tokenid::exclaim:812op = op_not;813left_associative = false;814break;815case tokenid::percent:816op = op_modulo;817break;818case tokenid::ampersand:819op = op_bitand;820break;821case tokenid::star:822op = op_multiply;823break;824case tokenid::plus:825left_associative =826previous_token == tokenid::int_literal ||827previous_token == tokenid::uint_literal ||828previous_token == tokenid::identifier ||829previous_token == tokenid::parenthesis_close;830op = left_associative ? op_add : op_plus;831break;832case tokenid::minus:833left_associative =834previous_token == tokenid::int_literal ||835previous_token == tokenid::uint_literal ||836previous_token == tokenid::identifier ||837previous_token == tokenid::parenthesis_close;838op = left_associative ? op_subtract : op_negate;839break;840case tokenid::slash:841op = op_divide;842break;843case tokenid::less:844op = op_less;845break;846case tokenid::greater:847op = op_greater;848break;849case tokenid::caret:850op = op_bitxor;851break;852case tokenid::pipe:853op = op_bitor;854break;855case tokenid::tilde:856op = op_bitnot;857left_associative = false;858break;859case tokenid::exclaim_equal:860op = op_not_equal;861break;862case tokenid::ampersand_ampersand:863op = op_and;864break;865case tokenid::less_less:866op = op_leftshift;867break;868case tokenid::less_equal:869op = op_less_equal;870break;871case tokenid::equal_equal:872op = op_equal;873break;874case tokenid::greater_greater:875op = op_rightshift;876break;877case tokenid::greater_equal:878op = op_greater_equal;879break;880case tokenid::pipe_pipe:881op = op_or;882break;883default:884// This is not an operator token885break;886}887888switch (_token)889{890case tokenid::parenthesis_open:891stack[stack_index++] = op_parentheses;892break;893case tokenid::parenthesis_close:894parenthesis_matched = false;895while (stack_index > 0)896{897const int op2 = stack[--stack_index];898if (op2 == op_parentheses)899{900parenthesis_matched = true;901break;902}903904rpn[rpn_index++] = { op2, true };905}906907if (!parenthesis_matched)908return error(_token.location, "unmatched ')'"), false;909break;910case tokenid::identifier:911if (evaluate_identifier_as_macro())912continue;913914if (_token.literal_as_string == "exists")915{916const bool has_parentheses = accept(tokenid::parenthesis_open);917918while (accept(tokenid::identifier))919{920if (!evaluate_identifier_as_macro())921{922error(_token.location, "syntax error: unexpected identifier after 'exists'");923return false;924}925}926927if (!expect(tokenid::string_literal))928return false;929930std::filesystem::path file_name = std::filesystem::u8path(_token.literal_as_string);931std::filesystem::path file_path = std::filesystem::u8path(_output_location.source);932file_path.replace_filename(file_name);933934if (has_parentheses && !expect(tokenid::parenthesis_close))935return false;936937if (!_file_exists_cb(file_path.u8string()))938for (const std::filesystem::path &include_path : _include_paths)939if (_file_exists_cb((file_path = include_path / file_name).u8string()))940break;941942rpn[rpn_index++] = { _file_exists_cb(file_path.u8string()) ? 1 : 0, false };943continue;944}945if (_token.literal_as_string == "defined")946{947const bool has_parentheses = accept(tokenid::parenthesis_open);948949if (!expect(tokenid::identifier))950return false;951952const std::string macro_name = std::move(_token.literal_as_string);953954if (has_parentheses && !expect(tokenid::parenthesis_close))955return false;956957rpn[rpn_index++] = { is_defined(macro_name) ? 1 : 0, false };958continue;959}960961// An identifier that cannot be replaced with a number becomes zero962rpn[rpn_index++] = { 0, false };963break;964case tokenid::int_literal:965case tokenid::uint_literal:966rpn[rpn_index++] = { _token.literal_as_int, false };967break;968default:969if (op == op_none)970return error(_token.location, "invalid expression"), false;971972while (stack_index > 0)973{974const int prev_op = stack[stack_index - 1];975if (prev_op == op_parentheses)976break;977978if (left_associative ?979(s_precedence_lookup[op] > s_precedence_lookup[prev_op]) :980(s_precedence_lookup[op] >= s_precedence_lookup[prev_op]))981break;982983stack_index--;984rpn[rpn_index++] = { prev_op, true };985}986987stack[stack_index++] = op;988break;989}990991previous_token = _token;992}993994while (stack_index > 0)995{996const int op = stack[--stack_index];997if (op == op_parentheses)998return error(_token.location, "unmatched ')'"), false;9991000rpn[rpn_index++] = { op, true };1001}10021003#define UNARY_OPERATION(op) { \1004if (stack_index < 1) \1005return error(_token.location, "invalid expression"), 0; \1006stack[stack_index - 1] = op stack[stack_index - 1]; \1007}1008#define BINARY_OPERATION(op) { \1009if (stack_index < 2) \1010return error(_token.location, "invalid expression"), 0; \1011stack[stack_index - 2] = stack[stack_index - 2] op stack[stack_index - 1]; \1012stack_index--; \1013}10141015// Evaluate reverse polish notation output1016for (rpn_token *token = rpn; rpn_index--; token++)1017{1018if (token->is_op)1019{1020switch (token->value)1021{1022case op_or:1023BINARY_OPERATION(||);1024break;1025case op_and:1026BINARY_OPERATION(&&);1027break;1028case op_bitor:1029BINARY_OPERATION(|);1030break;1031case op_bitxor:1032BINARY_OPERATION(^);1033break;1034case op_bitand:1035BINARY_OPERATION(&);1036break;1037case op_not_equal:1038BINARY_OPERATION(!=);1039break;1040case op_equal:1041BINARY_OPERATION(==);1042break;1043case op_less:1044BINARY_OPERATION(<);1045break;1046case op_greater:1047BINARY_OPERATION(>);1048break;1049case op_less_equal:1050BINARY_OPERATION(<=);1051break;1052case op_greater_equal:1053BINARY_OPERATION(>=);1054break;1055case op_leftshift:1056BINARY_OPERATION(<<);1057break;1058case op_rightshift:1059BINARY_OPERATION(>>);1060break;1061case op_add:1062BINARY_OPERATION(+);1063break;1064case op_subtract:1065BINARY_OPERATION(-);1066break;1067case op_modulo:1068if (stack[stack_index - 1] == 0)1069return error(_token.location, "right operand of '%' is zero"), 0;1070BINARY_OPERATION(%);1071break;1072case op_divide:1073if (stack[stack_index - 1] == 0)1074return error(_token.location, "division by zero"), 0;1075BINARY_OPERATION(/);1076break;1077case op_multiply:1078BINARY_OPERATION(*);1079break;1080case op_plus:1081UNARY_OPERATION(+);1082break;1083case op_negate:1084UNARY_OPERATION(-);1085break;1086case op_not:1087UNARY_OPERATION(!);1088break;1089case op_bitnot:1090UNARY_OPERATION(~);1091break;1092}1093}1094else1095{1096stack[stack_index++] = token->value;1097}1098}10991100if (stack_index != 1)1101return error(_token.location, "invalid expression"), false;11021103return stack[0] != 0;1104}11051106bool reshadefx::preprocessor::evaluate_identifier_as_macro()1107{1108if (_token.literal_as_string == "__LINE__")1109{1110push(std::to_string(_token.location.line));1111return true;1112}1113if (_token.literal_as_string == "__FILE__")1114{1115push(escape_string(_token.location.source));1116return true;1117}1118if (_token.literal_as_string == "__FILE_STEM__")1119{1120const std::filesystem::path file_stem = std::filesystem::u8path(_token.location.source).stem();1121push(escape_string(file_stem.u8string()));1122return true;1123}1124if (_token.literal_as_string == "__FILE_STEM_HASH__")1125{1126const std::filesystem::path file_stem = std::filesystem::u8path(_token.location.source).stem();1127push(std::to_string(std::hash<std::string>()(file_stem.u8string()) & 0xFFFFFFFF));1128return true;1129}1130if (_token.literal_as_string == "__FILE_NAME__")1131{1132const std::filesystem::path file_name = std::filesystem::u8path(_token.location.source).filename();1133push(escape_string(file_name.u8string()));1134return true;1135}1136if (_token.literal_as_string == "__FILE_NAME_HASH__")1137{1138const std::filesystem::path file_name = std::filesystem::u8path(_token.location.source).filename();1139push(std::to_string(std::hash<std::string>()(file_name.u8string()) & 0xFFFFFFFF));1140return true;1141}11421143const auto it = _macros.find(_token.literal_as_string);1144if (it == _macros.end())1145return false;11461147if (!_input_stack.empty())1148{1149const std::unordered_set<std::string> &hidden_macros = _input_stack[_current_input_index].hidden_macros;1150if (hidden_macros.find(_token.literal_as_string) != hidden_macros.end())1151return false;1152}11531154const location macro_location = _token.location;1155if (_recursion_count++ >= 256)1156return error(macro_location, "macro recursion too high"), false;11571158std::vector<std::string> arguments;1159if (it->second.is_function_like)1160{1161if (!accept(tokenid::parenthesis_open))1162return false; // Function like macro used without arguments, handle that like a normal identifier instead11631164while (true)1165{1166int parentheses_level = 0;1167std::string argument;11681169// Ignore whitespace preceding the argument1170accept(tokenid::space);11711172if (accept(tokenid::parenthesis_close))1173break; // Special case for when there are no arguments11741175while (true)1176{1177if (peek(tokenid::end_of_file))1178return error(macro_location, "unexpected end of file in macro expansion"), false;11791180// Consume all tokens of the argument1181consume();11821183if (_token == tokenid::comma && parentheses_level == 0 && !(it->second.is_variadic && arguments.size() == it->second.parameters.size()))1184break; // Comma marks end of an argument (unless this is the last argument in a variadic macro invocation)1185if (_token == tokenid::parenthesis_open)1186parentheses_level++;1187if (_token == tokenid::parenthesis_close && --parentheses_level < 0)1188break;11891190// Collapse all whitespace down to a single space1191if (_token == tokenid::space)1192argument += ' ';1193else1194argument += _current_token_raw_data;1195}11961197// Trim whitespace following the argument1198if (argument.size() && argument.back() == ' ')1199argument.pop_back();12001201arguments.push_back(std::move(argument));12021203if (parentheses_level < 0)1204break;1205}1206}12071208expand_macro(it->first, it->second, arguments);12091210return true;1211}12121213bool reshadefx::preprocessor::is_defined(const std::string &name) const1214{1215return _macros.find(name) != _macros.end() ||1216// Check built-in macros as well1217name == "__LINE__" ||1218name == "__FILE__" ||1219name == "__FILE_NAME__" ||1220name == "__FILE_STEM__";1221}12221223void reshadefx::preprocessor::expand_macro(const std::string &name, const macro ¯o, const std::vector<std::string> &arguments)1224{1225if (macro.replacement_list.empty())1226return;12271228// Verify argument count for function-like macros1229if (arguments.size() < macro.parameters.size())1230return warning(_token.location, "not enough arguments for function-like macro invocation '" + name + "'");1231if (arguments.size() > macro.parameters.size() && !macro.is_variadic)1232return warning(_token.location, "too many arguments for function-like macro invocation '" + name + "'");12331234std::string input;1235input.reserve(macro.replacement_list.size());12361237for (size_t offset = 0; offset < macro.replacement_list.size(); ++offset)1238{1239if (macro.replacement_list[offset] != macro_replacement_start)1240{1241input += macro.replacement_list[offset];1242continue;1243}12441245// This is a special replacement sequence1246const char type = macro.replacement_list[++offset];1247const char index = macro.replacement_list[++offset];1248if (static_cast<size_t>(index) >= arguments.size())1249{1250if (macro.is_variadic)1251{1252// The concatenation operator has a special meaning when placed between a comma and a variable argument, deleting the preceding comma1253if (type == macro_replacement_concat && input.back() == ',')1254input.pop_back();1255if (type == macro_replacement_stringize)1256input += "\"\"";1257}1258continue;1259}12601261switch (type)1262{1263case macro_replacement_argument:1264// Argument prescan1265push(arguments[index] + static_cast<char>(macro_replacement_argument));1266while (true)1267{1268// Consume all tokens of the argument (until the end marker is reached)1269consume();12701271if (_token == tokenid::unknown) // 'macro_replacement_argument' is 'tokenid::unknown'1272break;1273if (_token == tokenid::identifier && evaluate_identifier_as_macro())1274continue;12751276input += _current_token_raw_data;1277}1278assert(_current_token_raw_data[0] == macro_replacement_argument);1279break;1280case macro_replacement_concat:1281input += arguments[index];1282break;1283case macro_replacement_stringize:1284// Adds backslashes to escape quotes1285input += escape_string<'\"'>(arguments[index]);1286break;1287}1288}12891290push(std::move(input));12911292// Avoid expanding macros again that are referencing themselves1293_input_stack[_current_input_index].hidden_macros.insert(name);1294}12951296void reshadefx::preprocessor::create_macro_replacement_list(macro ¯o)1297{1298// Since the number of parameters is encoded in the string, it may not exceed the available size of a char1299if (macro.parameters.size() >= std::numeric_limits<unsigned char>::max())1300return error(_token.location, "too many macro parameters");13011302// Ignore whitespace preceding the replacement list1303accept(tokenid::space);13041305bool next_concat = false;13061307while (!peek(tokenid::end_of_line) && !peek(tokenid::end_of_file))1308{1309consume();13101311switch (_token)1312{1313case tokenid::hash:1314if (accept(tokenid::hash, false))1315{1316if (macro.replacement_list.empty())1317return error(_token.location, "## cannot appear at start of macro expansion");1318if (peek(tokenid::end_of_line))1319return error(_token.location, "## cannot appear at end of macro expansion");13201321// Remove any whitespace preceding or following the concatenation operator (so "a ## b" becomes "ab")1322if (macro.replacement_list.back() == ' ')1323macro.replacement_list.pop_back();1324accept(tokenid::space);13251326// Disable macro expansion for any argument preceding or following the ## token concatenation operator1327if (macro.replacement_list.size() > 2 && macro.replacement_list[macro.replacement_list.size() - 2] == macro_replacement_argument)1328macro.replacement_list[macro.replacement_list.size() - 2] = macro_replacement_concat;1329next_concat = true;1330continue;1331}1332if (macro.is_function_like)1333{1334if (!expect(tokenid::identifier))1335return;13361337const auto it = std::find(macro.parameters.begin(), macro.parameters.end(), _token.literal_as_string);1338if (it == macro.parameters.end() && !(macro.is_variadic && _token.literal_as_string == "__VA_ARGS__"))1339return error(_token.location, "# must be followed by parameter name");13401341// Start a # stringize operator1342macro.replacement_list += macro_replacement_start;1343macro.replacement_list += macro_replacement_stringize;1344macro.replacement_list += static_cast<char>(std::distance(macro.parameters.begin(), it));1345next_concat = false;1346continue;1347}1348break;1349case tokenid::space:1350// Collapse all whitespace down to a single space1351macro.replacement_list += ' ';1352continue;1353case tokenid::minus:1354// Special case to handle things like "#define NUM -1\n -NUM", which would otherwise result in "--1", making parsing fail1355if (macro.replacement_list.empty())1356macro.replacement_list += ' ';1357break;1358case tokenid::identifier:1359if (const auto it = std::find(macro.parameters.begin(), macro.parameters.end(), _token.literal_as_string);1360it != macro.parameters.end() || (macro.is_variadic && _token.literal_as_string == "__VA_ARGS__"))1361{1362macro.replacement_list += macro_replacement_start;1363macro.replacement_list += static_cast<char>(next_concat ? macro_replacement_concat : macro_replacement_argument);1364macro.replacement_list += static_cast<char>(std::distance(macro.parameters.begin(), it));1365next_concat = false;1366continue;1367}1368break;1369default:1370// Token needs no special handling, raw data is added to macro below1371break;1372}13731374macro.replacement_list += _current_token_raw_data;1375next_concat = false;1376}13771378// Trim whitespace following the replacement list1379if (macro.replacement_list.size() && macro.replacement_list.back() == ' ')1380macro.replacement_list.pop_back();1381}138213831384