/* * Copyright (C) 2014 Patrick Mours * SPDX-License-Identifier: BSD-3-Clause */ #include "effect_lexer.hpp" #include "effect_preprocessor.hpp" #include #include #include // std::find_if #ifndef _WIN32 // On Linux systems the native path encoding is UTF-8 already, so no conversion necessary #define u8path(p) path(p) #define u8string() string() #endif enum op_type { op_none = -1, op_or, op_and, op_bitor, op_bitxor, op_bitand, op_not_equal, op_equal, op_less, op_greater, op_less_equal, op_greater_equal, op_leftshift, op_rightshift, op_add, op_subtract, op_modulo, op_divide, op_multiply, op_plus, op_negate, op_not, op_bitnot, op_parentheses }; enum macro_replacement { macro_replacement_start = '\x00', macro_replacement_argument = '\xFD', macro_replacement_concat = '\xFF', macro_replacement_stringize = '\xFE', }; static const int precedence_lookup[] = { 0, 1, 2, 3, 4, // bitwise operators 5, 6, 7, 7, 7, 7, // logical operators 8, 8, // left shift, right shift 9, 9, // add, subtract 10, 10, 10, // modulo, divide, multiply 11, 11, 11, 11 // unary operators }; static bool read_file(const std::string &path, std::string &data, reshadefx::preprocessor::include_read_file_callback &cb) { std::string file_data; if (!cb(path, file_data)) return false; // Append a new line feed to the end of the input string to avoid issues with parsing file_data.push_back('\n'); // Remove BOM (0xefbbbf means 0xfeff) if (file_data.size() >= 3 && static_cast(file_data[0]) == 0xef && static_cast(file_data[1]) == 0xbb && static_cast(file_data[2]) == 0xbf) file_data.erase(0, 3); data = std::move(file_data); return true; } bool reshadefx::preprocessor::stdfs_read_file_callback(const std::string &path, std::string &data) { std::ifstream file(std::filesystem::path(path), std::ios::binary); if (!file) return false; // Read file contents into memory std::error_code ec; const uintmax_t file_size = std::filesystem::file_size(path, ec); if (ec) return false; data.reserve(file_size + 1); data.resize(static_cast(file_size), '\0'); if (!file.read(data.data(), file_size)) return false; // No longer need to have a handle open to the file, since all data was read, so can safely close it file.close(); return true; } bool reshadefx::preprocessor::stdfs_file_exists_callback(const std::string &path) { return std::filesystem::exists(std::filesystem::path(path)); } template static std::string escape_string(std::string s) { for (size_t offset = 0; (offset = s.find(ESCAPE_CHAR, offset)) != std::string::npos; offset += 2) s.insert(offset, "\\", 1); return '\"' + s + '\"'; } reshadefx::preprocessor::preprocessor() : _file_exists_cb(stdfs_file_exists_callback) , _read_file_cb(stdfs_read_file_callback) { } reshadefx::preprocessor::~preprocessor() { } void reshadefx::preprocessor::set_include_callbacks(include_file_exists_callback file_exists, include_read_file_callback read_file) { _file_exists_cb = file_exists; _read_file_cb = read_file; } void reshadefx::preprocessor::add_include_path(const std::string &path) { assert(!path.empty()); _include_paths.push_back(std::filesystem::path(path)); } bool reshadefx::preprocessor::add_macro_definition(const std::string &name, const macro ¯o) { assert(!name.empty()); return _macros.emplace(name, macro).second; } bool reshadefx::preprocessor::append_file(const std::string &path) { std::string source_code; if (!read_file(path, source_code, _read_file_cb)) return false; return append_string(std::move(source_code), path); } bool reshadefx::preprocessor::append_string(std::string source_code, const std::string &path /* = std::string() */) { // Enforce all input strings to end with a line feed assert(!source_code.empty() && source_code.back() == '\n'); _success = true; // Clear success flag before parsing a new string // Give this push a name, so that lexer location starts at a new line // 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 line // 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 line push(std::move(source_code), path.empty() ? "unknown" : path); parse(); return _success; } std::vector reshadefx::preprocessor::included_files() const { std::vector files; files.reserve(_file_cache.size()); for (const auto &it : _file_cache) files.push_back(std::filesystem::u8path(it.first)); return files; } std::vector> reshadefx::preprocessor::used_macro_definitions() const { std::vector> defines; defines.reserve(_used_macros.size()); for (const std::string &name : _used_macros) if (const auto it = _macros.find(name); // Do not include function-like macros, since they are more likely to contain a complex replacement list it != _macros.end() && !it->second.is_function_like) defines.emplace_back(name, it->second.replacement_list); return defines; } void reshadefx::preprocessor::error(const location &location, const std::string &message) { _errors += location.source + '(' + std::to_string(location.line) + ", " + std::to_string(location.column) + ')' + ": preprocessor error: " + message + '\n'; _success = false; // Unset success flag } void reshadefx::preprocessor::warning(const location &location, const std::string &message) { _errors += location.source + '(' + std::to_string(location.line) + ", " + std::to_string(location.column) + ')' + ": preprocessor warning: " + message + '\n'; } void reshadefx::preprocessor::push(std::string input, const std::string &name) { location start_location = !name.empty() ? // Start at the beginning of the file when pushing a new file location(name, 1) : // Start with last known token location when pushing an unnamed string _token.location; input_level level = { name }; level.lexer.reset(new lexer( std::move(input), true /* ignore_comments */, false /* ignore_whitespace */, false /* ignore_pp_directives */, false /* ignore_line_directives */, true /* ignore_keywords */, false /* escape_string_literals */, start_location)); level.next_token.id = tokenid::unknown; level.next_token.location = start_location; // This is used in 'consume' to initialize the output location // Inherit hidden macros from parent if (!_input_stack.empty()) level.hidden_macros = _input_stack.back().hidden_macros; _input_stack.push_back(std::move(level)); _next_input_index = _input_stack.size() - 1; // Advance into the input stack to update next token consume(); } bool reshadefx::preprocessor::peek(tokenid tokid) const { if (_input_stack.empty()) return tokid == tokenid::end_of_file; return _input_stack[_next_input_index].next_token == tokid; } void reshadefx::preprocessor::consume() { _current_input_index = _next_input_index; if (_input_stack.empty()) { // End of input has been reached already (this can happen when the input text is not terminated with a new line) assert(_current_input_index == 0); return; } // Clear out input stack, now that the current token is overwritten while (_input_stack.size() > (_current_input_index + 1)) _input_stack.pop_back(); // Update location information after switching input levels input_level &input = _input_stack[_current_input_index]; if (!input.name.empty() && input.name != _output_location.source) { _output += "#line " + std::to_string(input.next_token.location.line) + " \"" + input.name + "\"\n"; // Line number is increased before checking against next token in 'tokenid::end_of_line' handling in 'parse' function below, so compensate for that here _output_location.line = input.next_token.location.line - 1; _output_location.source = input.name; } // Set current token _token = std::move(input.next_token); _current_token_raw_data = input.lexer->input_string().substr(_token.offset, _token.length); // Get the next token input.next_token = input.lexer->lex(); // Verify string literals (since the lexer cannot throw errors itself) if (_token == tokenid::string_literal && _current_token_raw_data.back() != '\"') error(_token.location, "unterminated string literal"); // Pop input level if lexical analysis has reached the end of it // This ensures the EOF token is not consumed until the very last file while (peek(tokenid::end_of_file)) { // Remove any unterminated blocks from the stack for (; !_if_stack.empty() && _if_stack.back().input_index >= _next_input_index; _if_stack.pop_back()) error(_if_stack.back().pp_token.location, "unterminated #if"); if (_next_input_index == 0) { // End of input has been reached, so cannot pop further and this is the last token _input_stack.pop_back(); return; } else { _next_input_index -= 1; } } } void reshadefx::preprocessor::consume_until(tokenid tokid) { while (!accept(tokid) && !peek(tokenid::end_of_file)) { consume(); } } bool reshadefx::preprocessor::accept(tokenid tokid, bool ignore_whitespace) { if (ignore_whitespace) { while (peek(tokenid::space)) { consume(); } } if (peek(tokid)) { consume(); return true; } return false; } bool reshadefx::preprocessor::expect(tokenid tokid) { if (!accept(tokid)) { if (_input_stack.empty()) return tokid == tokenid::end_of_line || tokid == tokenid::end_of_file; token actual_token = _input_stack[_next_input_index].next_token; actual_token.location.source = _output_location.source; if (actual_token == tokenid::end_of_line) error(actual_token.location, "syntax error: unexpected new line"); else error(actual_token.location, "syntax error: unexpected token '" + _input_stack[_next_input_index].lexer->input_string().substr(actual_token.offset, actual_token.length) + '\''); return false; } return true; } void reshadefx::preprocessor::parse() { std::string line; // Consume all tokens in the input while (!peek(tokenid::end_of_file)) { consume(); _recursion_count = 0; const bool skip = !_if_stack.empty() && _if_stack.back().skipping; switch (_token) { case tokenid::hash_if: parse_if(); if (!expect(tokenid::end_of_line)) consume_until(tokenid::end_of_line); continue; case tokenid::hash_ifdef: parse_ifdef(); if (!expect(tokenid::end_of_line)) consume_until(tokenid::end_of_line); continue; case tokenid::hash_ifndef: parse_ifndef(); if (!expect(tokenid::end_of_line)) consume_until(tokenid::end_of_line); continue; case tokenid::hash_else: parse_else(); if (!expect(tokenid::end_of_line)) consume_until(tokenid::end_of_line); continue; case tokenid::hash_elif: parse_elif(); if (!expect(tokenid::end_of_line)) consume_until(tokenid::end_of_line); continue; case tokenid::hash_endif: parse_endif(); if (!expect(tokenid::end_of_line)) consume_until(tokenid::end_of_line); continue; default: // All other tokens are handled below break; } if (skip) // Ignore token since the current section is disabled continue; switch (_token) { case tokenid::hash_def: parse_def(); if (!expect(tokenid::end_of_line)) consume_until(tokenid::end_of_line); continue; case tokenid::hash_undef: parse_undef(); if (!expect(tokenid::end_of_line)) consume_until(tokenid::end_of_line); continue; case tokenid::hash_error: parse_error(); if (!expect(tokenid::end_of_line)) consume_until(tokenid::end_of_line); continue; case tokenid::hash_warning: parse_warning(); if (!expect(tokenid::end_of_line)) consume_until(tokenid::end_of_line); continue; case tokenid::hash_pragma: parse_pragma(); if (!expect(tokenid::end_of_line)) consume_until(tokenid::end_of_line); continue; case tokenid::hash_include: parse_include(); continue; case tokenid::hash_unknown: // Standalone "#" is valid and should be ignored if (_token.length != 0) error(_token.location, "unrecognized preprocessing directive '" + _token.literal_as_string + '\''); if (!expect(tokenid::end_of_line)) consume_until(tokenid::end_of_line); continue; case tokenid::end_of_line: if (line.empty()) continue; // Do not append empty lines to output, instead emit "#line" statements _output_location.line++; if (_token.location.line != _output_location.line) { _output += "#line " + std::to_string(_token.location.line) + '\n'; _output_location.line = _token.location.line; } _output += line; _output += '\n'; line.clear(); continue; case tokenid::identifier: if (evaluate_identifier_as_macro()) continue; [[fallthrough]]; default: line += _current_token_raw_data; break; } } // Append the last line after the EOF token was reached to the output _output += line; _output += '\n'; } void reshadefx::preprocessor::parse_def() { if (!expect(tokenid::identifier)) return; if (_token.literal_as_string == "defined") return warning(_token.location, "macro name 'defined' is reserved"); macro m; const location location = std::move(_token.location); const std::string macro_name = std::move(_token.literal_as_string); // Only create function-like macro if the parenthesis follows the macro name without any whitespace between if (accept(tokenid::parenthesis_open, false)) { m.is_function_like = true; while (accept(tokenid::identifier)) { m.parameters.push_back(_token.literal_as_string); if (!accept(tokenid::comma)) break; } if (accept(tokenid::ellipsis)) m.is_variadic = true; if (!expect(tokenid::parenthesis_close)) return; } create_macro_replacement_list(m); if (!add_macro_definition(macro_name, m)) return error(location, "redefinition of '" + macro_name + "'"); } void reshadefx::preprocessor::parse_undef() { if (!expect(tokenid::identifier)) return; if (_token.literal_as_string == "defined") return warning(_token.location, "macro name 'defined' is reserved"); _macros.erase(_token.literal_as_string); } void reshadefx::preprocessor::parse_if() { if_level level; level.pp_token = _token; level.input_index = _current_input_index; // Evaluate expression after updating 'pp_token', so that it points at the beginning # token level.value = evaluate_expression(); const bool parent_skipping = !_if_stack.empty() && _if_stack.back().skipping; level.skipping = parent_skipping || !level.value; _if_stack.push_back(std::move(level)); } void reshadefx::preprocessor::parse_ifdef() { if_level level; level.pp_token = _token; level.input_index = _current_input_index; if (!expect(tokenid::identifier)) return; level.value = is_defined(_token.literal_as_string); const bool parent_skipping = !_if_stack.empty() && _if_stack.back().skipping; level.skipping = parent_skipping || !level.value; _if_stack.push_back(std::move(level)); // Only add to used macro list if this #ifdef is active and the macro was not defined before if (!parent_skipping) if (const auto it = _macros.find(_token.literal_as_string); it == _macros.end() || it->second.is_predefined) _used_macros.emplace(_token.literal_as_string); } void reshadefx::preprocessor::parse_ifndef() { if_level level; level.pp_token = _token; level.input_index = _current_input_index; if (!expect(tokenid::identifier)) return; level.value = !is_defined(_token.literal_as_string); const bool parent_skipping = !_if_stack.empty() && _if_stack.back().skipping; level.skipping = parent_skipping || !level.value; _if_stack.push_back(std::move(level)); // Only add to used macro list if this #ifndef is active and the macro was not defined before if (!parent_skipping) if (const auto it = _macros.find(_token.literal_as_string); it == _macros.end() || it->second.is_predefined) _used_macros.emplace(_token.literal_as_string); } void reshadefx::preprocessor::parse_elif() { if (_if_stack.empty()) return error(_token.location, "missing #if for #elif"); if_level &level = _if_stack.back(); if (level.pp_token == tokenid::hash_else) return error(_token.location, "#elif is not allowed after #else"); // Update 'pp_token' before evaluating expression, so that it points at the beginning # token level.pp_token = _token; level.input_index = _current_input_index; const bool parent_skipping = _if_stack.size() > 1 && _if_stack[_if_stack.size() - 2].skipping; const bool condition_result = evaluate_expression(); level.skipping = parent_skipping || level.value || !condition_result; if (!level.value) level.value = condition_result; } void reshadefx::preprocessor::parse_else() { if (_if_stack.empty()) return error(_token.location, "missing #if for #else"); if_level &level = _if_stack.back(); if (level.pp_token == tokenid::hash_else) return error(_token.location, "#else is not allowed after #else"); level.pp_token = _token; level.input_index = _current_input_index; const bool parent_skipping = _if_stack.size() > 1 && _if_stack[_if_stack.size() - 2].skipping; level.skipping = parent_skipping || level.value; if (!level.value) level.value = true; } void reshadefx::preprocessor::parse_endif() { if (_if_stack.empty()) error(_token.location, "missing #if for #endif"); else _if_stack.pop_back(); } void reshadefx::preprocessor::parse_error() { const location keyword_location = std::move(_token.location); if (!expect(tokenid::string_literal)) return; error(keyword_location, _token.literal_as_string); } void reshadefx::preprocessor::parse_warning() { const location keyword_location = std::move(_token.location); if (!expect(tokenid::string_literal)) return; warning(keyword_location, _token.literal_as_string); } void reshadefx::preprocessor::parse_pragma() { const location keyword_location = std::move(_token.location); if (!expect(tokenid::identifier)) return; std::string pragma = std::move(_token.literal_as_string); std::string pragma_args; // Ignore whitespace preceding the argument list accept(tokenid::space); while (!peek(tokenid::end_of_line) && !peek(tokenid::end_of_file)) { consume(); if (_token == tokenid::identifier && evaluate_identifier_as_macro()) continue; // Collapse all whitespace down to a single space if (_token == tokenid::space) pragma_args += ' '; else pragma_args += _current_token_raw_data; } if (pragma == "once") { // Clear file contents, so that future include statements simply push an empty string instead of these file contents again if (const auto it = _file_cache.find(_output_location.source); it != _file_cache.end()) it->second.clear(); return; } if (pragma == "warning" || pragma == "reshade") { _used_pragmas.emplace_back(std::move(pragma), std::move(pragma_args)); return; } warning(keyword_location, "unknown pragma ignored"); } void reshadefx::preprocessor::parse_include() { const location keyword_location = std::move(_token.location); while (accept(tokenid::identifier)) { if (!evaluate_identifier_as_macro()) { error(_token.location, "syntax error: unexpected identifier in #include"); consume_until(tokenid::end_of_line); return; } } if (!expect(tokenid::string_literal)) { consume_until(tokenid::end_of_line); return; } std::filesystem::path file_name = std::filesystem::u8path(_token.literal_as_string); std::filesystem::path file_path = std::filesystem::u8path(_output_location.source); file_path.replace_filename(file_name); if (!_file_exists_cb(file_path.u8string())) for (const std::filesystem::path &include_path : _include_paths) if (_file_exists_cb((file_path = include_path / file_name).u8string())) break; const std::string file_path_string = file_path.u8string(); // Detect recursive include and abort to avoid infinite loop if (std::find_if(_input_stack.begin(), _input_stack.end(), [&file_path_string](const input_level &level) { return level.name == file_path_string; }) != _input_stack.end()) return error(_token.location, "recursive #include"); std::string input; if (const auto it = _file_cache.find(file_path_string); it != _file_cache.end()) { input = it->second; } else { if (!read_file(file_path_string, input, _read_file_cb)) return error(keyword_location, "could not open included file '" + file_name.u8string() + '\''); _file_cache.emplace(file_path_string, input); } // 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 again if (!expect(tokenid::end_of_line)) consume_until(tokenid::end_of_line); // Clear out input stack before pushing include, so that hidden macros do not bleed into the include while (_input_stack.size() > (_next_input_index + 1)) _input_stack.pop_back(); push(std::move(input), file_path_string); } bool reshadefx::preprocessor::evaluate_expression() { struct rpn_token { int value; bool is_op; }; size_t rpn_index = 0; size_t stack_index = 0; const size_t STACK_SIZE = 128; rpn_token rpn[STACK_SIZE]; int stack[STACK_SIZE]; // Keep track of previous token to figure out data type of expression tokenid previous_token = _token; // Run shunting-yard algorithm while (!peek(tokenid::end_of_line) && !peek(tokenid::end_of_file)) { if (stack_index >= STACK_SIZE || rpn_index >= STACK_SIZE) return error(_token.location, "expression evaluator ran out of stack space"), false; consume(); auto op = op_none; bool left_associative = true; bool parenthesis_matched = false; switch (_token) { case tokenid::space: continue; case tokenid::exclaim: op = op_not; left_associative = false; break; case tokenid::percent: op = op_modulo; break; case tokenid::ampersand: op = op_bitand; break; case tokenid::star: op = op_multiply; break; case tokenid::plus: left_associative = previous_token == tokenid::int_literal || previous_token == tokenid::uint_literal || previous_token == tokenid::identifier || previous_token == tokenid::parenthesis_close; op = left_associative ? op_add : op_plus; break; case tokenid::minus: left_associative = previous_token == tokenid::int_literal || previous_token == tokenid::uint_literal || previous_token == tokenid::identifier || previous_token == tokenid::parenthesis_close; op = left_associative ? op_subtract : op_negate; break; case tokenid::slash: op = op_divide; break; case tokenid::less: op = op_less; break; case tokenid::greater: op = op_greater; break; case tokenid::caret: op = op_bitxor; break; case tokenid::pipe: op = op_bitor; break; case tokenid::tilde: op = op_bitnot; left_associative = false; break; case tokenid::exclaim_equal: op = op_not_equal; break; case tokenid::ampersand_ampersand: op = op_and; break; case tokenid::less_less: op = op_leftshift; break; case tokenid::less_equal: op = op_less_equal; break; case tokenid::equal_equal: op = op_equal; break; case tokenid::greater_greater: op = op_rightshift; break; case tokenid::greater_equal: op = op_greater_equal; break; case tokenid::pipe_pipe: op = op_or; break; default: // This is not an operator token break; } switch (_token) { case tokenid::parenthesis_open: stack[stack_index++] = op_parentheses; break; case tokenid::parenthesis_close: parenthesis_matched = false; while (stack_index > 0) { const int op2 = stack[--stack_index]; if (op2 == op_parentheses) { parenthesis_matched = true; break; } rpn[rpn_index++] = { op2, true }; } if (!parenthesis_matched) return error(_token.location, "unmatched ')'"), false; break; case tokenid::identifier: if (evaluate_identifier_as_macro()) continue; if (_token.literal_as_string == "exists") { const bool has_parentheses = accept(tokenid::parenthesis_open); while (accept(tokenid::identifier)) { if (!evaluate_identifier_as_macro()) { error(_token.location, "syntax error: unexpected identifier after 'exists'"); return false; } } if (!expect(tokenid::string_literal)) return false; std::filesystem::path file_name = std::filesystem::u8path(_token.literal_as_string); std::filesystem::path file_path = std::filesystem::u8path(_output_location.source); file_path.replace_filename(file_name); if (has_parentheses && !expect(tokenid::parenthesis_close)) return false; if (!_file_exists_cb(file_path.u8string())) for (const std::filesystem::path &include_path : _include_paths) if (_file_exists_cb((file_path = include_path / file_name).u8string())) break; rpn[rpn_index++] = { _file_exists_cb(file_path.u8string()) ? 1 : 0, false }; continue; } if (_token.literal_as_string == "defined") { const bool has_parentheses = accept(tokenid::parenthesis_open); if (!expect(tokenid::identifier)) return false; const std::string macro_name = std::move(_token.literal_as_string); if (has_parentheses && !expect(tokenid::parenthesis_close)) return false; rpn[rpn_index++] = { is_defined(macro_name) ? 1 : 0, false }; continue; } // An identifier that cannot be replaced with a number becomes zero rpn[rpn_index++] = { 0, false }; break; case tokenid::int_literal: case tokenid::uint_literal: rpn[rpn_index++] = { _token.literal_as_int, false }; break; default: if (op == op_none) return error(_token.location, "invalid expression"), false; while (stack_index > 0) { const int prev_op = stack[stack_index - 1]; if (prev_op == op_parentheses) break; if (left_associative ? (precedence_lookup[op] > precedence_lookup[prev_op]) : (precedence_lookup[op] >= precedence_lookup[prev_op])) break; stack_index--; rpn[rpn_index++] = { prev_op, true }; } stack[stack_index++] = op; break; } previous_token = _token; } while (stack_index > 0) { const int op = stack[--stack_index]; if (op == op_parentheses) return error(_token.location, "unmatched ')'"), false; rpn[rpn_index++] = { op, true }; } #define UNARY_OPERATION(op) { \ if (stack_index < 1) \ return error(_token.location, "invalid expression"), 0; \ stack[stack_index - 1] = op stack[stack_index - 1]; \ } #define BINARY_OPERATION(op) { \ if (stack_index < 2) \ return error(_token.location, "invalid expression"), 0; \ stack[stack_index - 2] = stack[stack_index - 2] op stack[stack_index - 1]; \ stack_index--; \ } // Evaluate reverse polish notation output for (rpn_token *token = rpn; rpn_index--; token++) { if (token->is_op) { switch (token->value) { case op_or: BINARY_OPERATION(||); break; case op_and: BINARY_OPERATION(&&); break; case op_bitor: BINARY_OPERATION(|); break; case op_bitxor: BINARY_OPERATION(^); break; case op_bitand: BINARY_OPERATION(&); break; case op_not_equal: BINARY_OPERATION(!=); break; case op_equal: BINARY_OPERATION(==); break; case op_less: BINARY_OPERATION(<); break; case op_greater: BINARY_OPERATION(>); break; case op_less_equal: BINARY_OPERATION(<=); break; case op_greater_equal: BINARY_OPERATION(>=); break; case op_leftshift: BINARY_OPERATION(<<); break; case op_rightshift: BINARY_OPERATION(>>); break; case op_add: BINARY_OPERATION(+); break; case op_subtract: BINARY_OPERATION(-); break; case op_modulo: BINARY_OPERATION(%); break; case op_divide: BINARY_OPERATION(/); break; case op_multiply: BINARY_OPERATION(*); break; case op_plus: UNARY_OPERATION(+); break; case op_negate: UNARY_OPERATION(-); break; case op_not: UNARY_OPERATION(!); break; case op_bitnot: UNARY_OPERATION(~); break; } } else { stack[stack_index++] = token->value; } } if (stack_index != 1) return error(_token.location, "invalid expression"), false; return stack[0] != 0; } bool reshadefx::preprocessor::evaluate_identifier_as_macro() { if (_token.literal_as_string == "__LINE__") { push(std::to_string(_token.location.line)); return true; } if (_token.literal_as_string == "__FILE__") { push(escape_string(_token.location.source)); return true; } if (_token.literal_as_string == "__FILE_STEM__") { const std::filesystem::path file_stem = std::filesystem::u8path(_token.location.source).stem(); push(escape_string(file_stem.u8string())); return true; } if (_token.literal_as_string == "__FILE_NAME__") { const std::filesystem::path file_name = std::filesystem::u8path(_token.location.source).filename(); push(escape_string(file_name.u8string())); return true; } const auto it = _macros.find(_token.literal_as_string); if (it == _macros.end()) return false; if (!_input_stack.empty()) { const std::unordered_set &hidden_macros = _input_stack[_current_input_index].hidden_macros; if (hidden_macros.find(_token.literal_as_string) != hidden_macros.end()) return false; } const location macro_location = _token.location; if (_recursion_count++ >= 256) return error(macro_location, "macro recursion too high"), false; std::vector arguments; if (it->second.is_function_like) { if (!accept(tokenid::parenthesis_open)) return false; // Function like macro used without arguments, handle that like a normal identifier instead while (true) { int parentheses_level = 0; std::string argument; // Ignore whitespace preceding the argument accept(tokenid::space); if (accept(tokenid::parenthesis_close)) break; // Special case for when there are no arguments while (true) { if (peek(tokenid::end_of_file)) return error(macro_location, "unexpected end of file in macro expansion"), false; // Consume all tokens of the argument consume(); if (_token == tokenid::comma && parentheses_level == 0 && !(it->second.is_variadic && arguments.size() == it->second.parameters.size())) break; // Comma marks end of an argument (unless this is the last argument in a variadic macro invocation) if (_token == tokenid::parenthesis_open) parentheses_level++; if (_token == tokenid::parenthesis_close && --parentheses_level < 0) break; // Collapse all whitespace down to a single space if (_token == tokenid::space) argument += ' '; else argument += _current_token_raw_data; } // Trim whitespace following the argument if (argument.size() && argument.back() == ' ') argument.pop_back(); arguments.push_back(std::move(argument)); if (parentheses_level < 0) break; } } expand_macro(it->first, it->second, arguments); return true; } bool reshadefx::preprocessor::is_defined(const std::string &name) const { return _macros.find(name) != _macros.end() || // Check built-in macros as well name == "__LINE__" || name == "__FILE__" || name == "__FILE_NAME__" || name == "__FILE_STEM__"; } void reshadefx::preprocessor::expand_macro(const std::string &name, const macro ¯o, const std::vector &arguments) { if (macro.replacement_list.empty()) return; // Verify argument count for function-like macros if (arguments.size() < macro.parameters.size()) return warning(_token.location, "not enough arguments for function-like macro invocation '" + name + "'"); if (arguments.size() > macro.parameters.size() && !macro.is_variadic) return warning(_token.location, "too many arguments for function-like macro invocation '" + name + "'"); std::string input; input.reserve(macro.replacement_list.size()); for (size_t offset = 0; offset < macro.replacement_list.size(); ++offset) { if (macro.replacement_list[offset] != macro_replacement_start) { input += macro.replacement_list[offset]; continue; } // This is a special replacement sequence const char type = macro.replacement_list[++offset]; const char index = macro.replacement_list[++offset]; if (static_cast(index) >= arguments.size()) { if (macro.is_variadic) { // The concatenation operator has a special meaning when placed between a comma and a variable argument, deleting the preceding comma if (type == macro_replacement_concat && input.back() == ',') input.pop_back(); if (type == macro_replacement_stringize) input += "\"\""; } continue; } switch (type) { case macro_replacement_argument: // Argument prescan push(arguments[index] + static_cast(macro_replacement_argument)); while (true) { // Consume all tokens of the argument (until the end marker is reached) consume(); if (_token == tokenid::unknown) // 'macro_replacement_argument' is 'tokenid::unknown' break; if (_token == tokenid::identifier && evaluate_identifier_as_macro()) continue; input += _current_token_raw_data; } assert(_current_token_raw_data[0] == macro_replacement_argument); break; case macro_replacement_concat: input += arguments[index]; break; case macro_replacement_stringize: // Adds backslashes to escape quotes input += escape_string<'\"'>(arguments[index]); break; } } push(std::move(input)); // Avoid expanding macros again that are referencing themselves _input_stack[_current_input_index].hidden_macros.insert(name); } void reshadefx::preprocessor::create_macro_replacement_list(macro ¯o) { // Since the number of parameters is encoded in the string, it may not exceed the available size of a char if (macro.parameters.size() >= std::numeric_limits::max()) return error(_token.location, "too many macro parameters"); // Ignore whitespace preceding the replacement list accept(tokenid::space); bool next_concat = false; while (!peek(tokenid::end_of_line) && !peek(tokenid::end_of_file)) { consume(); switch (_token) { case tokenid::hash: if (accept(tokenid::hash, false)) { if (macro.replacement_list.empty()) return error(_token.location, "## cannot appear at start of macro expansion"); if (peek(tokenid::end_of_line)) return error(_token.location, "## cannot appear at end of macro expansion"); // Remove any whitespace preceding or following the concatenation operator (so "a ## b" becomes "ab") if (macro.replacement_list.back() == ' ') macro.replacement_list.pop_back(); accept(tokenid::space); // Disable macro expansion for any argument preceding or following the ## token concatenation operator if (macro.replacement_list.size() > 2 && macro.replacement_list[macro.replacement_list.size() - 2] == macro_replacement_argument) macro.replacement_list[macro.replacement_list.size() - 2] = macro_replacement_concat; next_concat = true; continue; } if (macro.is_function_like) { if (!expect(tokenid::identifier)) return; const auto it = std::find(macro.parameters.begin(), macro.parameters.end(), _token.literal_as_string); if (it == macro.parameters.end() && !(macro.is_variadic && _token.literal_as_string == "__VA_ARGS__")) return error(_token.location, "# must be followed by parameter name"); // Start a # stringize operator macro.replacement_list += macro_replacement_start; macro.replacement_list += macro_replacement_stringize; macro.replacement_list += static_cast(std::distance(macro.parameters.begin(), it)); next_concat = false; continue; } break; case tokenid::space: // Collapse all whitespace down to a single space macro.replacement_list += ' '; continue; case tokenid::minus: // Special case to handle things like "#define NUM -1\n -NUM", which would otherwise result in "--1", making parsing fail if (macro.replacement_list.empty()) macro.replacement_list += ' '; break; case tokenid::identifier: if (const auto it = std::find(macro.parameters.begin(), macro.parameters.end(), _token.literal_as_string); it != macro.parameters.end() || (macro.is_variadic && _token.literal_as_string == "__VA_ARGS__")) { macro.replacement_list += macro_replacement_start; macro.replacement_list += static_cast(next_concat ? macro_replacement_concat : macro_replacement_argument); macro.replacement_list += static_cast(std::distance(macro.parameters.begin(), it)); next_concat = false; continue; } break; default: // Token needs no special handling, raw data is added to macro below break; } macro.replacement_list += _current_token_raw_data; next_concat = false; } // Trim whitespace following the replacement list if (macro.replacement_list.size() && macro.replacement_list.back() == ' ') macro.replacement_list.pop_back(); }