Duckstation/dep/reshadefx/src/effect_parser_stmt.cpp
2023-08-16 01:12:59 +10:00

2005 lines
73 KiB
C++

/*
* Copyright (C) 2014 Patrick Mours
* SPDX-License-Identifier: BSD-3-Clause
*/
#include "effect_lexer.hpp"
#include "effect_parser.hpp"
#include "effect_codegen.hpp"
#include <cctype> // std::toupper
#include <cassert>
#include <limits>
#include <functional>
#include <string_view>
struct on_scope_exit
{
template <typename F>
explicit on_scope_exit(F lambda) : leave(lambda) { }
~on_scope_exit() { leave(); }
std::function<void()> leave;
};
bool reshadefx::parser::parse(std::string input, codegen *backend)
{
_lexer.reset(new lexer(std::move(input)));
// Set backend for subsequent code-generation
_codegen = backend;
assert(backend != nullptr);
consume();
bool parse_success = true;
bool current_success = true;
while (!peek(tokenid::end_of_file))
{
parse_top(current_success);
if (!current_success)
parse_success = false;
}
return parse_success;
}
void reshadefx::parser::parse_top(bool &parse_success)
{
if (accept(tokenid::namespace_))
{
// Anonymous namespaces are not supported right now, so an identifier is a must
if (!expect(tokenid::identifier))
{
parse_success = false;
return;
}
const auto name = std::move(_token.literal_as_string);
if (!expect('{'))
{
parse_success = false;
return;
}
enter_namespace(name);
bool current_success = true;
bool parse_success_namespace = true;
// Recursively parse top level statements until the namespace is closed again
while (!peek('}')) // Empty namespaces are valid
{
parse_top(current_success);
if (!current_success)
parse_success_namespace = false;
}
leave_namespace();
parse_success = expect('}') && parse_success_namespace;
}
else if (accept(tokenid::struct_)) // Structure keyword found, parse the structure definition
{
// Structure definitions are terminated with a semicolon
parse_success = parse_struct() && expect(';');
}
else if (accept(tokenid::technique)) // Technique keyword found, parse the technique definition
{
parse_success = parse_technique();
}
else
{
if (type type; parse_type(type)) // Type found, this can be either a variable or a function declaration
{
parse_success = expect(tokenid::identifier);
if (!parse_success)
return;
if (peek('('))
{
const auto name = std::move(_token.literal_as_string);
// This is definitely a function declaration, so parse it
if (!parse_function(type, name))
{
// Insert dummy function into symbol table, so later references can be resolved despite the error
insert_symbol(name, { symbol_type::function, ~0u, { type::t_function } }, true);
parse_success = false;
return;
}
}
else
{
// There may be multiple variable names after the type, handle them all
unsigned int count = 0;
do {
if (count++ > 0 && !(expect(',') && expect(tokenid::identifier)))
{
parse_success = false;
return;
}
const auto name = std::move(_token.literal_as_string);
if (!parse_variable(type, name, true))
{
// Insert dummy variable into symbol table, so later references can be resolved despite the error
insert_symbol(name, { symbol_type::variable, ~0u, type }, true);
// Skip the rest of the statement
consume_until(';');
parse_success = false;
return;
}
} while (!peek(';'));
// Variable declarations are terminated with a semicolon
parse_success = expect(';');
}
}
else if (accept(';')) // Ignore single semicolons in the source
{
parse_success = true;
}
else
{
// Unexpected token in source stream, consume and report an error about it
consume();
// Only add another error message if succeeded parsing previously
// This is done to avoid walls of error messages because of consequential errors following a top-level syntax mistake
if (parse_success)
error(_token.location, 3000, "syntax error: unexpected '" + token::id_to_name(_token.id) + '\'');
parse_success = false;
}
}
}
bool reshadefx::parser::parse_statement(bool scoped)
{
if (!_codegen->is_in_block())
return error(_token_next.location, 0, "unreachable code"), false;
unsigned int loop_control = 0;
unsigned int selection_control = 0;
// Read any loop and branch control attributes first
while (accept('['))
{
enum control_mask
{
unroll = 0x1,
dont_unroll = 0x2,
flatten = (0x1 << 4),
dont_flatten = (0x2 << 4),
switch_force_case = (0x4 << 4),
switch_call = (0x8 << 4)
};
const auto attribute = std::move(_token_next.literal_as_string);
if (!expect(tokenid::identifier) || !expect(']'))
return false;
if (attribute == "unroll")
loop_control |= unroll;
else if (attribute == "loop" || attribute == "fastopt")
loop_control |= dont_unroll;
else if (attribute == "flatten")
selection_control |= flatten;
else if (attribute == "branch")
selection_control |= dont_flatten;
else if (attribute == "forcecase")
selection_control |= switch_force_case;
else if (attribute == "call")
selection_control |= switch_call;
else
warning(_token.location, 0, "unknown attribute");
if ((loop_control & (unroll | dont_unroll)) == (unroll | dont_unroll))
return error(_token.location, 3524, "can't use loop and unroll attributes together"), false;
if ((selection_control & (flatten | dont_flatten)) == (flatten | dont_flatten))
return error(_token.location, 3524, "can't use branch and flatten attributes together"), false;
}
// Shift by two so that the possible values are 0x01 for 'flatten' and 0x02 for 'dont_flatten', equivalent to 'unroll' and 'dont_unroll'
selection_control >>= 4;
if (peek('{')) // Parse statement block
return parse_statement_block(scoped);
else if (accept(';')) // Ignore empty statements
return true;
// Most statements with the exception of declarations are only valid inside functions
if (_codegen->is_in_function())
{
assert(_current_function != nullptr);
const auto location = _token_next.location;
if (accept(tokenid::if_))
{
codegen::id true_block = _codegen->create_block(); // Block which contains the statements executed when the condition is true
codegen::id false_block = _codegen->create_block(); // Block which contains the statements executed when the condition is false
const codegen::id merge_block = _codegen->create_block(); // Block that is executed after the branch re-merged with the current control flow
expression condition;
if (!expect('(') || !parse_expression(condition) || !expect(')'))
return false;
else if (!condition.type.is_scalar())
return error(condition.location, 3019, "if statement conditional expressions must evaluate to a scalar"), false;
// Load condition and convert to boolean value as required by 'OpBranchConditional'
condition.add_cast_operation({ type::t_bool, 1, 1 });
const codegen::id condition_value = _codegen->emit_load(condition);
const codegen::id condition_block = _codegen->leave_block_and_branch_conditional(condition_value, true_block, false_block);
{ // Then block of the if statement
_codegen->enter_block(true_block);
if (!parse_statement(true))
return false;
true_block = _codegen->leave_block_and_branch(merge_block);
}
{ // Else block of the if statement
_codegen->enter_block(false_block);
if (accept(tokenid::else_) && !parse_statement(true))
return false;
false_block = _codegen->leave_block_and_branch(merge_block);
}
_codegen->enter_block(merge_block);
// Emit structured control flow for an if statement and connect all basic blocks
_codegen->emit_if(location, condition_value, condition_block, true_block, false_block, selection_control);
return true;
}
if (accept(tokenid::switch_))
{
const codegen::id merge_block = _codegen->create_block(); // Block that is executed after the switch re-merged with the current control flow
expression selector_exp;
if (!expect('(') || !parse_expression(selector_exp) || !expect(')'))
return false;
else if (!selector_exp.type.is_scalar())
return error(selector_exp.location, 3019, "switch statement expression must evaluate to a scalar"), false;
// Load selector and convert to integral value as required by switch instruction
selector_exp.add_cast_operation({ type::t_int, 1, 1 });
const auto selector_value = _codegen->emit_load(selector_exp);
const auto selector_block = _codegen->leave_block_and_switch(selector_value, merge_block);
if (!expect('{'))
return false;
_loop_break_target_stack.push_back(merge_block);
on_scope_exit _([this]() { _loop_break_target_stack.pop_back(); });
bool parse_success = true;
// The default case jumps to the end of the switch statement if not overwritten
codegen::id default_label = merge_block, default_block = merge_block;
codegen::id current_label = _codegen->create_block();
std::vector<codegen::id> case_literal_and_labels, case_blocks;
size_t last_case_label_index = 0;
// Enter first switch statement body block
_codegen->enter_block(current_label);
while (!peek(tokenid::end_of_file))
{
while (accept(tokenid::case_) || accept(tokenid::default_))
{
if (_token.id == tokenid::case_)
{
expression case_label;
if (!parse_expression(case_label))
return consume_until('}'), false;
else if (!case_label.type.is_scalar() || !case_label.type.is_integral() || !case_label.is_constant)
return error(case_label.location, 3020, "invalid type for case expression - value must be an integer scalar"), consume_until('}'), false;
// Check for duplicate case values
for (size_t i = 0; i < case_literal_and_labels.size(); i += 2)
{
if (case_literal_and_labels[i] == case_label.constant.as_uint[0])
{
parse_success = false;
error(case_label.location, 3532, "duplicate case " + std::to_string(case_label.constant.as_uint[0]));
break;
}
}
case_blocks.emplace_back(); // This is set to the actual block below
case_literal_and_labels.push_back(case_label.constant.as_uint[0]);
case_literal_and_labels.push_back(current_label);
}
else
{
// Check if the default label was already changed by a previous 'default' statement
if (default_label != merge_block)
{
parse_success = false;
error(_token.location, 3532, "duplicate default in switch statement");
}
default_label = current_label;
default_block = 0; // This is set to the actual block below
}
if (!expect(':'))
return consume_until('}'), false;
}
// It is valid for no statement to follow if this is the last label in the switch body
const bool end_of_switch = peek('}');
if (!end_of_switch && !parse_statement(true))
return consume_until('}'), false;
// Handle fall-through case and end of switch statement
if (peek(tokenid::case_) || peek(tokenid::default_) || end_of_switch)
{
if (_codegen->is_in_block()) // Disallow fall-through for now
{
parse_success = false;
error(_token_next.location, 3533, "non-empty case statements must have break or return");
}
const codegen::id next_label = end_of_switch ? merge_block : _codegen->create_block();
// This is different from 'current_label', since there may have been branching logic inside the case, which would have changed the active block
const codegen::id current_block = _codegen->leave_block_and_branch(next_label);
if (0 == default_block)
default_block = current_block;
for (size_t i = last_case_label_index; i < case_blocks.size(); ++i)
// Need to use the initial label for the switch table, but the current block to merge all the block data
case_blocks[i] = current_block;
current_label = next_label;
_codegen->enter_block(current_label);
if (end_of_switch) // We reached the end, nothing more to do
break;
last_case_label_index = case_blocks.size();
}
}
if (case_literal_and_labels.empty() && default_label == merge_block)
warning(location, 5002, "switch statement contains no 'case' or 'default' labels");
// Emit structured control flow for a switch statement and connect all basic blocks
_codegen->emit_switch(location, selector_value, selector_block, default_label, default_block, case_literal_and_labels, case_blocks, selection_control);
return expect('}') && parse_success;
}
if (accept(tokenid::for_))
{
if (!expect('('))
return false;
enter_scope();
on_scope_exit _([this]() { leave_scope(); });
// Parse initializer first
if (type type; parse_type(type))
{
unsigned int count = 0;
do { // There may be multiple declarations behind a type, so loop through them
if (count++ > 0 && !expect(','))
return false;
if (!expect(tokenid::identifier) || !parse_variable(type, std::move(_token.literal_as_string)))
return false;
} while (!peek(';'));
}
else
{
// Initializer can also contain an expression if not a variable declaration list and not empty
if (!peek(';'))
{
expression expression;
if (!parse_expression(expression))
return false;
}
}
if (!expect(';'))
return false;
const codegen::id merge_block = _codegen->create_block(); // Block that is executed after the loop
const codegen::id header_label = _codegen->create_block(); // Pointer to the loop merge instruction
const codegen::id continue_label = _codegen->create_block(); // Pointer to the continue block
codegen::id loop_block = _codegen->create_block(); // Pointer to the main loop body block
codegen::id condition_block = _codegen->create_block(); // Pointer to the condition check
codegen::id condition_value = 0;
// End current block by branching to the next label
const codegen::id prev_block = _codegen->leave_block_and_branch(header_label);
{ // Begin loop block (this header is used for explicit structured control flow)
_codegen->enter_block(header_label);
_codegen->leave_block_and_branch(condition_block);
}
{ // Parse condition block
_codegen->enter_block(condition_block);
if (!peek(';'))
{
expression condition;
if (!parse_expression(condition))
return false;
if (!condition.type.is_scalar())
return error(condition.location, 3019, "scalar value expected"), false;
// Evaluate condition and branch to the right target
condition.add_cast_operation({ type::t_bool, 1, 1 });
condition_value = _codegen->emit_load(condition);
condition_block = _codegen->leave_block_and_branch_conditional(condition_value, loop_block, merge_block);
}
else // It is valid for there to be no condition expression
{
condition_block = _codegen->leave_block_and_branch(loop_block);
}
if (!expect(';'))
return false;
}
{ // Parse loop continue block into separate block so it can be appended to the end down the line
_codegen->enter_block(continue_label);
if (!peek(')'))
{
expression continue_exp;
if (!parse_expression(continue_exp))
return false;
}
if (!expect(')'))
return false;
// Branch back to the loop header at the end of the continue block
_codegen->leave_block_and_branch(header_label);
}
{ // Parse loop body block
_codegen->enter_block(loop_block);
_loop_break_target_stack.push_back(merge_block);
_loop_continue_target_stack.push_back(continue_label);
const bool parse_success = parse_statement(false);
_loop_break_target_stack.pop_back();
_loop_continue_target_stack.pop_back();
if (!parse_success)
return false;
loop_block = _codegen->leave_block_and_branch(continue_label);
}
// Add merge block label to the end of the loop
_codegen->enter_block(merge_block);
// Emit structured control flow for a loop statement and connect all basic blocks
_codegen->emit_loop(location, condition_value, prev_block, header_label, condition_block, loop_block, continue_label, loop_control);
return true;
}
if (accept(tokenid::while_))
{
enter_scope();
on_scope_exit _([this]() { leave_scope(); });
const codegen::id merge_block = _codegen->create_block();
const codegen::id header_label = _codegen->create_block();
const codegen::id continue_label = _codegen->create_block();
codegen::id loop_block = _codegen->create_block();
codegen::id condition_block = _codegen->create_block();
codegen::id condition_value = 0;
// End current block by branching to the next label
const codegen::id prev_block = _codegen->leave_block_and_branch(header_label);
{ // Begin loop block
_codegen->enter_block(header_label);
_codegen->leave_block_and_branch(condition_block);
}
{ // Parse condition block
_codegen->enter_block(condition_block);
expression condition;
if (!expect('(') || !parse_expression(condition) || !expect(')'))
return false;
else if (!condition.type.is_scalar())
return error(condition.location, 3019, "scalar value expected"), false;
// Evaluate condition and branch to the right target
condition.add_cast_operation({ type::t_bool, 1, 1 });
condition_value = _codegen->emit_load(condition);
condition_block = _codegen->leave_block_and_branch_conditional(condition_value, loop_block, merge_block);
}
{ // Parse loop body block
_codegen->enter_block(loop_block);
_loop_break_target_stack.push_back(merge_block);
_loop_continue_target_stack.push_back(continue_label);
const bool parse_success = parse_statement(false);
_loop_break_target_stack.pop_back();
_loop_continue_target_stack.pop_back();
if (!parse_success)
return false;
loop_block = _codegen->leave_block_and_branch(continue_label);
}
{ // Branch back to the loop header in empty continue block
_codegen->enter_block(continue_label);
_codegen->leave_block_and_branch(header_label);
}
// Add merge block label to the end of the loop
_codegen->enter_block(merge_block);
// Emit structured control flow for a loop statement and connect all basic blocks
_codegen->emit_loop(location, condition_value, prev_block, header_label, condition_block, loop_block, continue_label, loop_control);
return true;
}
if (accept(tokenid::do_))
{
const codegen::id merge_block = _codegen->create_block();
const codegen::id header_label = _codegen->create_block();
const codegen::id continue_label = _codegen->create_block();
codegen::id loop_block = _codegen->create_block();
codegen::id condition_value = 0;
// End current block by branching to the next label
const codegen::id prev_block = _codegen->leave_block_and_branch(header_label);
{ // Begin loop block
_codegen->enter_block(header_label);
_codegen->leave_block_and_branch(loop_block);
}
{ // Parse loop body block
_codegen->enter_block(loop_block);
_loop_break_target_stack.push_back(merge_block);
_loop_continue_target_stack.push_back(continue_label);
const bool parse_success = parse_statement(true);
_loop_break_target_stack.pop_back();
_loop_continue_target_stack.pop_back();
if (!parse_success)
return false;
loop_block = _codegen->leave_block_and_branch(continue_label);
}
{ // Continue block does the condition evaluation
_codegen->enter_block(continue_label);
expression condition;
if (!expect(tokenid::while_) || !expect('(') || !parse_expression(condition) || !expect(')') || !expect(';'))
return false;
else if (!condition.type.is_scalar())
return error(condition.location, 3019, "scalar value expected"), false;
// Evaluate condition and branch to the right target
condition.add_cast_operation({ type::t_bool, 1, 1 });
condition_value = _codegen->emit_load(condition);
_codegen->leave_block_and_branch_conditional(condition_value, header_label, merge_block);
}
// Add merge block label to the end of the loop
_codegen->enter_block(merge_block);
// Emit structured control flow for a loop statement and connect all basic blocks
_codegen->emit_loop(location, condition_value, prev_block, header_label, 0, loop_block, continue_label, loop_control);
return true;
}
if (accept(tokenid::break_))
{
if (_loop_break_target_stack.empty())
return error(location, 3518, "break must be inside loop"), false;
// Branch to the break target of the inner most loop on the stack
_codegen->leave_block_and_branch(_loop_break_target_stack.back(), 1);
return expect(';');
}
if (accept(tokenid::continue_))
{
if (_loop_continue_target_stack.empty())
return error(location, 3519, "continue must be inside loop"), false;
// Branch to the continue target of the inner most loop on the stack
_codegen->leave_block_and_branch(_loop_continue_target_stack.back(), 2);
return expect(';');
}
if (accept(tokenid::return_))
{
const type &ret_type = _current_function->return_type;
if (!peek(';'))
{
expression expression;
if (!parse_expression(expression))
return consume_until(';'), false;
// Cannot return to void
if (ret_type.is_void())
// Consume the semicolon that follows the return expression so that parsing may continue
return error(location, 3079, "void functions cannot return a value"), accept(';'), false;
// Cannot return arrays from a function
if (expression.type.is_array() || !type::rank(expression.type, ret_type))
return error(location, 3017, "expression (" + expression.type.description() + ") does not match function return type (" + ret_type.description() + ')'), accept(';'), false;
// Load return value and perform implicit cast to function return type
if (expression.type.components() > ret_type.components())
warning(expression.location, 3206, "implicit truncation of vector type");
expression.add_cast_operation(ret_type);
const auto return_value = _codegen->emit_load(expression);
_codegen->leave_block_and_return(return_value);
}
else if (!ret_type.is_void())
{
// No return value was found, but the function expects one
error(location, 3080, "function must return a value");
// Consume the semicolon that follows the return expression so that parsing may continue
accept(';');
return false;
}
else
{
_codegen->leave_block_and_return();
}
return expect(';');
}
if (accept(tokenid::discard_))
{
// Leave the current function block
_codegen->leave_block_and_kill();
return expect(';');
}
}
// Handle variable declarations
if (type type; parse_type(type))
{
unsigned int count = 0;
do { // There may be multiple declarations behind a type, so loop through them
if (count++ > 0 && !expect(','))
// Try to consume the rest of the declaration so that parsing may continue despite the error
return consume_until(';'), false;
if (!expect(tokenid::identifier) || !parse_variable(type, std::move(_token.literal_as_string)))
return consume_until(';'), false;
} while (!peek(';'));
return expect(';');
}
// Handle expression statements
if (expression expression; parse_expression(expression))
return expect(';'); // A statement has to be terminated with a semicolon
// Gracefully consume any remaining characters until the statement would usually end, so that parsing may continue despite the error
consume_until(';');
return false;
}
bool reshadefx::parser::parse_statement_block(bool scoped)
{
if (!expect('{'))
return false;
if (scoped)
enter_scope();
// Parse statements until the end of the block is reached
while (!peek('}') && !peek(tokenid::end_of_file))
{
if (!parse_statement(true))
{
if (scoped)
leave_scope();
// Ignore the rest of this block
unsigned int level = 0;
while (!peek(tokenid::end_of_file))
{
if (accept('{'))
{
++level;
}
else if (accept('}'))
{
if (level-- == 0)
break;
} // These braces are necessary to match the 'else' to the correct 'if' statement
else
{
consume();
}
}
return false;
}
}
if (scoped)
leave_scope();
return expect('}');
}
bool reshadefx::parser::parse_type(type &type)
{
type.qualifiers = 0;
accept_type_qualifiers(type);
if (!accept_type_class(type))
return false;
if (type.is_integral() && (type.has(type::q_centroid) || type.has(type::q_noperspective)))
return error(_token.location, 4576, "signature specifies invalid interpolation mode for integer component type"), false;
else if (type.has(type::q_centroid) && !type.has(type::q_noperspective))
type.qualifiers |= type::q_linear;
return true;
}
bool reshadefx::parser::parse_array_size(type &type)
{
// Reset array length to zero before checking if one exists
type.array_length = 0;
if (accept('['))
{
if (accept(']'))
{
// No length expression, so this is an unsized array
type.array_length = -1;
}
else if (expression expression; parse_expression(expression) && expect(']'))
{
if (!expression.is_constant || !(expression.type.is_scalar() && expression.type.is_integral()))
return error(expression.location, 3058, "array dimensions must be literal scalar expressions"), false;
type.array_length = expression.constant.as_uint[0];
if (type.array_length < 1 || type.array_length > 65536)
return error(expression.location, 3059, "array dimension must be between 1 and 65536"), false;
}
else
{
return false;
}
}
// Multi-dimensional arrays are not supported
if (peek('['))
return error(_token_next.location, 3119, "arrays cannot be multi-dimensional"), false;
return true;
}
bool reshadefx::parser::parse_annotations(std::vector<annotation> &annotations)
{
// Check if annotations exist and return early if none do
if (!accept('<'))
return true;
bool parse_success = true;
while (!peek('>'))
{
if (type type; accept_type_class(type))
warning(_token.location, 4717, "type prefixes for annotations are deprecated and ignored");
if (!expect(tokenid::identifier))
return consume_until('>'), false;
auto name = std::move(_token.literal_as_string);
if (expression expression; !expect('=') || !parse_expression_multary(expression) || !expect(';'))
return consume_until('>'), false;
else if (expression.is_constant)
annotations.push_back({ expression.type, std::move(name), std::move(expression.constant) });
else // Continue parsing annotations despite this not being a constant, since the syntax is still correct
parse_success = false,
error(expression.location, 3011, "value must be a literal expression");
}
return expect('>') && parse_success;
}
bool reshadefx::parser::parse_struct()
{
const auto location = std::move(_token.location);
struct_info info;
// The structure name is optional
if (accept(tokenid::identifier))
info.name = std::move(_token.literal_as_string);
else
info.name = "_anonymous_struct_" + std::to_string(location.line) + '_' + std::to_string(location.column);
info.unique_name = 'S' + current_scope().name + info.name;
std::replace(info.unique_name.begin(), info.unique_name.end(), ':', '_');
if (!expect('{'))
return false;
bool parse_success = true;
while (!peek('}')) // Empty structures are possible
{
struct_member_info member;
if (!parse_type(member.type))
return error(_token_next.location, 3000, "syntax error: unexpected '" + token::id_to_name(_token_next.id) + "', expected struct member type"), consume_until('}'), accept(';'), false;
unsigned int count = 0;
do {
if (count++ > 0 && !expect(','))
return consume_until('}'), accept(';'), false;
if (!expect(tokenid::identifier))
return consume_until('}'), accept(';'), false;
member.name = std::move(_token.literal_as_string);
member.location = std::move(_token.location);
if (member.type.is_void())
parse_success = false,
error(member.location, 3038, '\'' + member.name + "': struct members cannot be void");
if (member.type.is_struct()) // Nesting structures would make input/output argument flattening more complicated, so prevent it for now
parse_success = false,
error(member.location, 3090, '\'' + member.name + "': nested struct members are not supported");
if (member.type.has(type::q_in) || member.type.has(type::q_out))
parse_success = false,
error(member.location, 3055, '\'' + member.name + "': struct members cannot be declared 'in' or 'out'");
if (member.type.has(type::q_const))
parse_success = false,
error(member.location, 3035, '\'' + member.name + "': struct members cannot be declared 'const'");
if (member.type.has(type::q_extern))
parse_success = false,
error(member.location, 3006, '\'' + member.name + "': struct members cannot be declared 'extern'");
if (member.type.has(type::q_static))
parse_success = false,
error(member.location, 3007, '\'' + member.name + "': struct members cannot be declared 'static'");
if (member.type.has(type::q_uniform))
parse_success = false,
error(member.location, 3047, '\'' + member.name + "': struct members cannot be declared 'uniform'");
if (member.type.has(type::q_groupshared))
parse_success = false,
error(member.location, 3010, '\'' + member.name + "': struct members cannot be declared 'groupshared'");
// Modify member specific type, so that following members in the declaration list are not affected by this
if (!parse_array_size(member.type))
return consume_until('}'), accept(';'), false;
else if (member.type.array_length < 0)
parse_success = false,
error(member.location, 3072, '\'' + member.name + "': array dimensions of struct members must be explicit");
// Structure members may have semantics to use them as input/output types
if (accept(':'))
{
if (!expect(tokenid::identifier))
return consume_until('}'), accept(';'), false;
member.semantic = std::move(_token.literal_as_string);
// Make semantic upper case to simplify comparison later on
std::transform(member.semantic.begin(), member.semantic.end(), member.semantic.begin(),
[](std::string::value_type c) {
return static_cast<std::string::value_type>(std::toupper(c));
});
if (member.semantic.compare(0, 3, "SV_") != 0)
{
// Always numerate semantics, so that e.g. TEXCOORD and TEXCOORD0 point to the same location
if (const char c = member.semantic.back(); c < '0' || c > '9')
member.semantic += '0';
if (member.type.is_integral() && !member.type.has(type::q_nointerpolation))
{
member.type.qualifiers |= type::q_nointerpolation; // Integer fields do not interpolate, so make this explicit (to avoid issues with GLSL)
warning(member.location, 4568, '\'' + member.name + "': integer fields have the 'nointerpolation' qualifier by default");
}
}
else
{
// Remove optional trailing zero from system value semantics, so that e.g. SV_POSITION and SV_POSITION0 mean the same thing
if (member.semantic.back() == '0' && (member.semantic[member.semantic.size() - 2] < '0' || member.semantic[member.semantic.size() - 2] > '9'))
member.semantic.pop_back();
}
}
// Save member name and type for book keeping
info.member_list.push_back(member);
} while (!peek(';'));
if (!expect(';'))
return consume_until('}'), accept(';'), false;
}
// Empty structures are valid, but not usually intended, so emit a warning
if (info.member_list.empty())
warning(location, 5001, "struct has no members");
// Define the structure now that information about all the member types was gathered
const auto id = _codegen->define_struct(location, info);
// Insert the symbol into the symbol table
const symbol symbol = { symbol_type::structure, id };
if (!insert_symbol(info.name, symbol, true))
return error(location, 3003, "redefinition of '" + info.name + '\''), false;
return expect('}') && parse_success;
}
bool reshadefx::parser::parse_function(type type, std::string name)
{
const auto location = std::move(_token.location);
if (!expect('(')) // Functions always have a parameter list
return false;
if (type.qualifiers != 0)
return error(location, 3047, '\'' + name + "': function return type cannot have any qualifiers"), false;
function_info info;
info.name = name;
info.unique_name = 'F' + current_scope().name + name;
std::replace(info.unique_name.begin(), info.unique_name.end(), ':', '_');
info.return_type = type;
_current_function = &info;
bool parse_success = true;
bool expect_parenthesis = true;
// Enter function scope (and leave it again when finished parsing this function)
enter_scope();
on_scope_exit _([this]() {
leave_scope();
_codegen->leave_function();
_current_function = nullptr;
});
while (!peek(')'))
{
if (!info.parameter_list.empty() && !expect(','))
{
parse_success = false;
expect_parenthesis = false;
consume_until(')');
break;
}
struct_member_info param;
if (!parse_type(param.type))
{
error(_token_next.location, 3000, "syntax error: unexpected '" + token::id_to_name(_token_next.id) + "', expected parameter type");
parse_success = false;
expect_parenthesis = false;
consume_until(')');
break;
}
if (!expect(tokenid::identifier))
{
parse_success = false;
expect_parenthesis = false;
consume_until(')');
break;
}
param.name = std::move(_token.literal_as_string);
param.location = std::move(_token.location);
if (param.type.is_void())
parse_success = false,
error(param.location, 3038, '\'' + param.name + "': function parameters cannot be void");
if (param.type.has(type::q_extern))
parse_success = false,
error(param.location, 3006, '\'' + param.name + "': function parameters cannot be declared 'extern'");
if (param.type.has(type::q_static))
parse_success = false,
error(param.location, 3007, '\'' + param.name + "': function parameters cannot be declared 'static'");
if (param.type.has(type::q_uniform))
parse_success = false,
error(param.location, 3047, '\'' + param.name + "': function parameters cannot be declared 'uniform', consider placing in global scope instead");
if (param.type.has(type::q_groupshared))
parse_success = false,
error(param.location, 3010, '\'' + param.name + "': function parameters cannot be declared 'groupshared'");
if (param.type.has(type::q_out) && param.type.has(type::q_const))
parse_success = false,
error(param.location, 3046, '\'' + param.name + "': output parameters cannot be declared 'const'");
else if (!param.type.has(type::q_out))
param.type.qualifiers |= type::q_in; // Function parameters are implicitly 'in' if not explicitly defined as 'out'
if (!parse_array_size(param.type))
{
parse_success = false;
expect_parenthesis = false;
consume_until(')');
break;
}
else if (param.type.array_length < 0)
{
parse_success = false;
error(param.location, 3072, '\'' + param.name + "': array dimensions of function parameters must be explicit");
}
// Handle parameter type semantic
if (accept(':'))
{
if (!expect(tokenid::identifier))
{
parse_success = false;
expect_parenthesis = false;
consume_until(')');
break;
}
param.semantic = std::move(_token.literal_as_string);
// Make semantic upper case to simplify comparison later on
std::transform(param.semantic.begin(), param.semantic.end(), param.semantic.begin(),
[](std::string::value_type c) {
return static_cast<std::string::value_type>(std::toupper(c));
});
if (param.semantic.compare(0, 3, "SV_") != 0)
{
// Always numerate semantics, so that e.g. TEXCOORD and TEXCOORD0 point to the same location
if (const char c = param.semantic.back(); c < '0' || c > '9')
param.semantic += '0';
if (param.type.is_integral() && !param.type.has(type::q_nointerpolation))
{
param.type.qualifiers |= type::q_nointerpolation; // Integer parameters do not interpolate, so make this explicit (to avoid issues with GLSL)
warning(param.location, 4568, '\'' + param.name + "': integer parameters have the 'nointerpolation' qualifier by default");
}
}
else
{
// Remove optional trailing zero from system value semantics, so that e.g. SV_POSITION and SV_POSITION0 mean the same thing
if (param.semantic.back() == '0' && (param.semantic[param.semantic.size() - 2] < '0' || param.semantic[param.semantic.size() - 2] > '9'))
param.semantic.pop_back();
}
}
info.parameter_list.push_back(std::move(param));
}
if (expect_parenthesis && !expect(')'))
return false;
// Handle return type semantic
if (accept(':'))
{
if (!expect(tokenid::identifier))
return false;
if (type.is_void())
return error(_token.location, 3076, '\'' + name + "': void function cannot have a semantic"), false;
info.return_semantic = std::move(_token.literal_as_string);
// Make semantic upper case to simplify comparison later on
std::transform(info.return_semantic.begin(), info.return_semantic.end(), info.return_semantic.begin(),
[](std::string::value_type c) {
return static_cast<std::string::value_type>(std::toupper(c));
});
}
// Check if this is a function declaration without a body
if (accept(';'))
return error(location, 3510, '\'' + name + "': function is missing an implementation"), false;
// Define the function now that information about the declaration was gathered
const auto id = _codegen->define_function(location, info);
// Insert the function and parameter symbols into the symbol table and update current function pointer to the permanent one
symbol symbol = { symbol_type::function, id, { type::t_function } };
symbol.function = _current_function = &_codegen->get_function(id);
if (!insert_symbol(name, symbol, true))
return error(location, 3003, "redefinition of '" + name + '\''), false;
for (const struct_member_info &param : info.parameter_list)
if (!insert_symbol(param.name, { symbol_type::variable, param.definition, param.type }))
return error(param.location, 3003, "redefinition of '" + param.name + '\''), false;
// A function has to start with a new block
_codegen->enter_block(_codegen->create_block());
if (!parse_statement_block(false))
parse_success = false;
// Add implicit return statement to the end of functions
if (_codegen->is_in_block())
_codegen->leave_block_and_return();
return parse_success;
}
bool reshadefx::parser::parse_variable(type type, std::string name, bool global)
{
const auto location = std::move(_token.location);
if (type.is_void())
return error(location, 3038, '\'' + name + "': variables cannot be void"), false;
if (type.has(type::q_in) || type.has(type::q_out))
return error(location, 3055, '\'' + name + "': variables cannot be declared 'in' or 'out'"), false;
// Local and global variables have different requirements
if (global)
{
// Check that type qualifier combinations are valid
if (type.has(type::q_static))
{
// Global variables that are 'static' cannot be of another storage class
if (type.has(type::q_uniform))
return error(location, 3007, '\'' + name + "': uniform global variables cannot be declared 'static'"), false;
// The 'volatile' qualifier is only valid memory object declarations that are storage images or uniform blocks
if (type.has(type::q_volatile))
return error(location, 3008, '\'' + name + "': global variables cannot be declared 'volatile'"), false;
}
else if (!type.has(type::q_groupshared))
{
// Make all global variables 'uniform' by default, since they should be externally visible without the 'static' keyword
if (!type.has(type::q_uniform) && !type.is_object())
warning(location, 5000, '\'' + name + "': global variables are considered 'uniform' by default");
// Global variables that are not 'static' are always 'extern' and 'uniform'
type.qualifiers |= type::q_extern | type::q_uniform;
// It is invalid to make 'uniform' variables constant, since they can be modified externally
if (type.has(type::q_const))
return error(location, 3035, '\'' + name + "': variables which are 'uniform' cannot be declared 'const'"), false;
}
}
else
{
// Static does not really have meaning on local variables
if (type.has(type::q_static))
type.qualifiers &= ~type::q_static;
if (type.has(type::q_extern))
return error(location, 3006, '\'' + name + "': local variables cannot be declared 'extern'"), false;
if (type.has(type::q_uniform))
return error(location, 3047, '\'' + name + "': local variables cannot be declared 'uniform'"), false;
if (type.has(type::q_groupshared))
return error(location, 3010, '\'' + name + "': local variables cannot be declared 'groupshared'"), false;
if (type.is_object())
return error(location, 3038, '\'' + name + "': local variables cannot be texture, sampler or storage objects"), false;
}
// The variable name may be followed by an optional array size expression
if (!parse_array_size(type))
return false;
bool parse_success = true;
expression initializer;
texture_info texture_info;
sampler_info sampler_info;
storage_info storage_info;
if (accept(':'))
{
if (!expect(tokenid::identifier))
return false;
else if (!global) // Only global variables can have a semantic
return error(_token.location, 3043, '\'' + name + "': local variables cannot have semantics"), false;
std::string &semantic = texture_info.semantic;
semantic = std::move(_token.literal_as_string);
// Make semantic upper case to simplify comparison later on
std::transform(semantic.begin(), semantic.end(), semantic.begin(),
[](std::string::value_type c) {
return static_cast<std::string::value_type>(std::toupper(c));
});
}
else
{
// Global variables can have optional annotations
if (global && !parse_annotations(sampler_info.annotations))
parse_success = false;
// Variables without a semantic may have an optional initializer
if (accept('='))
{
if (!parse_expression_assignment(initializer))
return false;
if (type.has(type::q_groupshared))
return error(initializer.location, 3009, '\'' + name + "': variables declared 'groupshared' cannot have an initializer"), false;
// TODO: This could be resolved by initializing these at the beginning of the entry point
if (global && !initializer.is_constant)
return error(initializer.location, 3011, '\'' + name + "': initial value must be a literal expression"), false;
// Check type compatibility
if ((type.array_length >= 0 && initializer.type.array_length != type.array_length) || !type::rank(initializer.type, type))
return error(initializer.location, 3017, '\'' + name + "': initial value (" + initializer.type.description() + ") does not match variable type (" + type.description() + ')'), false;
if ((initializer.type.rows < type.rows || initializer.type.cols < type.cols) && !initializer.type.is_scalar())
return error(initializer.location, 3017, '\'' + name + "': cannot implicitly convert these vector types (from " + initializer.type.description() + " to " + type.description() + ')'), false;
// Deduce array size from the initializer expression
if (initializer.type.is_array())
type.array_length = initializer.type.array_length;
// Perform implicit cast from initializer expression to variable type
if (initializer.type.components() > type.components())
warning(initializer.location, 3206, "implicit truncation of vector type");
initializer.add_cast_operation(type);
if (type.has(type::q_static))
initializer.type.qualifiers |= type::q_static;
}
else if (type.is_numeric() || type.is_struct()) // Numeric variables without an initializer need special handling
{
if (type.has(type::q_const)) // Constants have to have an initial value
return error(location, 3012, '\'' + name + "': missing initial value"), false;
else if (!type.has(type::q_uniform)) // Zero initialize all global variables
initializer.reset_to_rvalue_constant(location, {}, type);
}
else if (global && accept('{')) // Textures and samplers can have a property block attached to their declaration
{
// Non-numeric variables cannot be constants
if (type.has(type::q_const))
return error(location, 3035, '\'' + name + "': this variable type cannot be declared 'const'"), false;
while (!peek('}'))
{
if (!expect(tokenid::identifier))
return consume_until('}'), false;
const auto property_name = std::move(_token.literal_as_string);
const auto property_location = std::move(_token.location);
if (!expect('='))
return consume_until('}'), false;
backup();
expression expression;
if (accept(tokenid::identifier)) // Handle special enumeration names for property values
{
// Transform identifier to uppercase to do case-insensitive comparison
std::transform(_token.literal_as_string.begin(), _token.literal_as_string.end(), _token.literal_as_string.begin(),
[](std::string::value_type c) {
return static_cast<std::string::value_type>(std::toupper(c));
});
static const std::unordered_map<std::string_view, uint32_t> s_enum_values = {
{ "NONE", 0 }, { "POINT", 0 },
{ "LINEAR", 1 },
{ "WRAP", uint32_t(texture_address_mode::wrap) }, { "REPEAT", uint32_t(texture_address_mode::wrap) },
{ "MIRROR", uint32_t(texture_address_mode::mirror) },
{ "CLAMP", uint32_t(texture_address_mode::clamp) },
{ "BORDER", uint32_t(texture_address_mode::border) },
{ "R8", uint32_t(texture_format::r8) },
{ "R16", uint32_t(texture_format::r16) },
{ "R16F", uint32_t(texture_format::r16f) },
{ "R32I", uint32_t(texture_format::r32i) },
{ "R32U", uint32_t(texture_format::r32u) },
{ "R32F", uint32_t(texture_format::r32f) },
{ "RG8", uint32_t(texture_format::rg8) }, { "R8G8", uint32_t(texture_format::rg8) },
{ "RG16", uint32_t(texture_format::rg16) }, { "R16G16", uint32_t(texture_format::rg16) },
{ "RG16F", uint32_t(texture_format::rg16f) }, { "R16G16F", uint32_t(texture_format::rg16f) },
{ "RG32F", uint32_t(texture_format::rg32f) }, { "R32G32F", uint32_t(texture_format::rg32f) },
{ "RGBA8", uint32_t(texture_format::rgba8) }, { "R8G8B8A8", uint32_t(texture_format::rgba8) },
{ "RGBA16", uint32_t(texture_format::rgba16) }, { "R16G16B16A16", uint32_t(texture_format::rgba16) },
{ "RGBA16F", uint32_t(texture_format::rgba16f) }, { "R16G16B16A16F", uint32_t(texture_format::rgba16f) },
{ "RGBA32F", uint32_t(texture_format::rgba32f) }, { "R32G32B32A32F", uint32_t(texture_format::rgba32f) },
{ "RGB10A2", uint32_t(texture_format::rgb10a2) }, { "R10G10B10A2", uint32_t(texture_format::rgb10a2) },
};
// Look up identifier in list of possible enumeration names
if (const auto it = s_enum_values.find(_token.literal_as_string);
it != s_enum_values.end())
expression.reset_to_rvalue_constant(_token.location, it->second);
else // No match found, so rewind to parser state before the identifier was consumed and try parsing it as a normal expression
restore();
}
// Parse right hand side as normal expression if no special enumeration name was matched already
if (!expression.is_constant && !parse_expression_multary(expression))
return consume_until('}'), false;
if (property_name == "Texture")
{
// Ignore invalid symbols that were added during error recovery
if (expression.base == 0xFFFFFFFF)
return consume_until('}'), false;
if (!expression.type.is_texture())
return error(expression.location, 3020, "type mismatch, expected texture name"), consume_until('}'), false;
if (type.is_sampler() || type.is_storage())
{
reshadefx::texture_info &target_info = _codegen->get_texture(expression.base);
if (type.is_storage())
// Texture is used as storage
target_info.storage_access = true;
texture_info = target_info;
sampler_info.texture_name = target_info.unique_name;
storage_info.texture_name = target_info.unique_name;
}
}
else
{
if (!expression.is_constant || !expression.type.is_scalar())
return error(expression.location, 3538, "value must be a literal scalar expression"), consume_until('}'), false;
// All states below expect the value to be of an integer type
expression.add_cast_operation({ type::t_int, 1, 1 });
const int value = expression.constant.as_int[0];
if (value < 0) // There is little use for negative values, so warn in those cases
warning(expression.location, 3571, "negative value specified for property '" + property_name + '\'');
if (type.is_texture())
{
if (property_name == "Width")
texture_info.width = value > 0 ? value : 1;
else if (type.texture_dimension() >= 2 && property_name == "Height")
texture_info.height = value > 0 ? value : 1;
else if (type.texture_dimension() >= 3 && property_name == "Depth")
texture_info.depth = value > 0 && value <= std::numeric_limits<uint16_t>::max() ? static_cast<uint16_t>(value) : 1;
else if (property_name == "MipLevels")
// Also ensures negative values do not cause problems
texture_info.levels = value > 0 && value <= std::numeric_limits<uint16_t>::max() ? static_cast<uint16_t>(value) : 1;
else if (property_name == "Format")
texture_info.format = static_cast<texture_format>(value);
else
return error(property_location, 3004, "unrecognized property '" + property_name + '\''), consume_until('}'), false;
}
else if (type.is_sampler())
{
if (property_name == "SRGBTexture" || property_name == "SRGBReadEnable")
sampler_info.srgb = value != 0;
else if (property_name == "AddressU")
sampler_info.address_u = static_cast<texture_address_mode>(value);
else if (property_name == "AddressV")
sampler_info.address_v = static_cast<texture_address_mode>(value);
else if (property_name == "AddressW")
sampler_info.address_w = static_cast<texture_address_mode>(value);
else if (property_name == "MinFilter")
sampler_info.filter = static_cast<filter_mode>((uint32_t(sampler_info.filter) & 0x0F) | ((value << 4) & 0x30)); // Combine sampler filter components into a single filter enumeration value
else if (property_name == "MagFilter")
sampler_info.filter = static_cast<filter_mode>((uint32_t(sampler_info.filter) & 0x33) | ((value << 2) & 0x0C));
else if (property_name == "MipFilter")
sampler_info.filter = static_cast<filter_mode>((uint32_t(sampler_info.filter) & 0x3C) | (value & 0x03));
else if (property_name == "MinLOD" || property_name == "MaxMipLevel")
sampler_info.min_lod = static_cast<float>(value);
else if (property_name == "MaxLOD")
sampler_info.max_lod = static_cast<float>(value);
else if (property_name == "MipLODBias" || property_name == "MipMapLodBias")
sampler_info.lod_bias = static_cast<float>(value);
else
return error(property_location, 3004, "unrecognized property '" + property_name + '\''), consume_until('}'), false;
}
else if (type.is_storage())
{
if (property_name == "MipLOD" || property_name == "MipLevel")
storage_info.level = value > 0 && value < std::numeric_limits<uint16_t>::max() ? static_cast<uint16_t>(value) : 0;
else
return error(property_location, 3004, "unrecognized property '" + property_name + '\''), consume_until('}'), false;
}
}
if (!expect(';'))
return consume_until('}'), false;
}
if (!expect('}'))
return false;
}
}
// At this point the array size should be known (either from the declaration or the initializer)
if (type.array_length < 0)
return error(location, 3074, '\'' + name + "': implicit array missing initial value"), false;
symbol symbol;
// Variables with a constant initializer and constant type are named constants
// Skip this for very large arrays though, to avoid large amounts of duplicated values when that array constant is accessed with a dynamic index
if (type.is_numeric() && type.has(type::q_const) && initializer.is_constant && type.array_length < 100)
{
// Named constants are special symbols
symbol = { symbol_type::constant, 0, type, initializer.constant };
}
else if (type.is_texture())
{
assert(global);
texture_info.name = name;
texture_info.type = static_cast<texture_type>(type.texture_dimension());
// Add namespace scope to avoid name clashes
texture_info.unique_name = 'V' + current_scope().name + name;
std::replace(texture_info.unique_name.begin(), texture_info.unique_name.end(), ':', '_');
texture_info.annotations = std::move(sampler_info.annotations);
symbol = { symbol_type::variable, 0, type };
symbol.id = _codegen->define_texture(location, texture_info);
}
// Samplers are actually combined image samplers
else if (type.is_sampler())
{
assert(global);
if (sampler_info.texture_name.empty())
return error(location, 3012, '\'' + name + "': missing 'Texture' property"), false;
if (type.texture_dimension() != static_cast<unsigned int>(texture_info.type))
return error(location, 3521, '\'' + name + "': type mismatch between texture and sampler type"), false;
if (sampler_info.srgb && texture_info.format != texture_format::rgba8)
return error(location, 4582, '\'' + name + "': texture does not support sRGB sampling (only textures with RGBA8 format do)"), false;
if (texture_info.format == texture_format::r32i ?
!type.is_integral() || !type.is_signed() :
texture_info.format == texture_format::r32u ?
!type.is_integral() || !type.is_unsigned() :
!type.is_floating_point())
return error(location, 4582, '\'' + name + "': type mismatch between texture format and sampler element type"), false;
sampler_info.name = name;
sampler_info.type = type;
// Add namespace scope to avoid name clashes
sampler_info.unique_name = 'V' + current_scope().name + name;
std::replace(sampler_info.unique_name.begin(), sampler_info.unique_name.end(), ':', '_');
symbol = { symbol_type::variable, 0, type };
symbol.id = _codegen->define_sampler(location, texture_info, sampler_info);
}
else if (type.is_storage())
{
assert(global);
if (storage_info.texture_name.empty())
return error(location, 3012, '\'' + name + "': missing 'Texture' property"), false;
if (type.texture_dimension() != static_cast<unsigned int>(texture_info.type))
return error(location, 3521, '\'' + name + "': type mismatch between texture and storage type"), false;
if (texture_info.format == texture_format::r32i ?
!type.is_integral() || !type.is_signed() :
texture_info.format == texture_format::r32u ?
!type.is_integral() || !type.is_unsigned() :
!type.is_floating_point())
return error(location, 4582, '\'' + name + "': type mismatch between texture format and storage element type"), false;
storage_info.name = name;
storage_info.type = type;
// Add namespace scope to avoid name clashes
storage_info.unique_name = 'V' + current_scope().name + name;
std::replace(storage_info.unique_name.begin(), storage_info.unique_name.end(), ':', '_');
if (storage_info.level > texture_info.levels - 1)
storage_info.level = texture_info.levels - 1;
symbol = { symbol_type::variable, 0, type };
symbol.id = _codegen->define_storage(location, texture_info, storage_info);
}
// Uniform variables are put into a global uniform buffer structure
else if (type.has(type::q_uniform))
{
assert(global);
uniform_info uniform_info;
uniform_info.name = name;
uniform_info.type = type;
uniform_info.annotations = std::move(sampler_info.annotations);
uniform_info.initializer_value = std::move(initializer.constant);
uniform_info.has_initializer_value = initializer.is_constant;
symbol = { symbol_type::variable, 0, type };
symbol.id = _codegen->define_uniform(location, uniform_info);
}
// All other variables are separate entities
else
{
// Update global variable names to contain the namespace scope to avoid name clashes
std::string unique_name = global ? 'V' + current_scope().name + name : name;
std::replace(unique_name.begin(), unique_name.end(), ':', '_');
symbol = { symbol_type::variable, 0, type };
symbol.id = _codegen->define_variable(location, type, std::move(unique_name), global,
// Shared variables cannot have an initializer
type.has(type::q_groupshared) ? 0 : _codegen->emit_load(initializer));
}
// Insert the symbol into the symbol table
if (!insert_symbol(name, symbol, global))
return error(location, 3003, "redefinition of '" + name + '\''), false;
return parse_success;
}
bool reshadefx::parser::parse_technique()
{
if (!expect(tokenid::identifier))
return false;
technique_info info;
info.name = std::move(_token.literal_as_string);
bool parse_success = parse_annotations(info.annotations);
if (!expect('{'))
return false;
while (!peek('}'))
{
if (pass_info pass; parse_technique_pass(pass))
info.passes.push_back(std::move(pass));
else {
parse_success = false;
if (!peek(tokenid::pass) && !peek('}')) // If there is another pass definition following, try to parse that despite the error
return consume_until('}'), false;
}
}
_codegen->define_technique(std::move(info));
return expect('}') && parse_success;
}
bool reshadefx::parser::parse_technique_pass(pass_info &info)
{
if (!expect(tokenid::pass))
return false;
const auto pass_location = std::move(_token.location);
// Passes can have an optional name
if (accept(tokenid::identifier))
info.name = std::move(_token.literal_as_string);
bool parse_success = true;
bool targets_support_srgb = true;
function_info vs_info, ps_info, cs_info;
if (!expect('{'))
return false;
while (!peek('}'))
{
// Parse pass states
if (!expect(tokenid::identifier))
return consume_until('}'), false;
auto location = std::move(_token.location);
const auto state = std::move(_token.literal_as_string);
if (!expect('='))
return consume_until('}'), false;
const bool is_shader_state = state == "VertexShader" || state == "PixelShader" || state == "ComputeShader";
const bool is_texture_state = state.compare(0, 12, "RenderTarget") == 0 && (state.size() == 12 || (state[12] >= '0' && state[12] < '8'));
// Shader and render target assignment looks up values in the symbol table, so handle those separately from the other states
if (is_shader_state || is_texture_state)
{
std::string identifier;
scoped_symbol symbol;
if (!accept_symbol(identifier, symbol))
return consume_until('}'), false;
location = std::move(_token.location);
int num_threads[3] = { 1, 1, 1 };
if (accept('<'))
{
expression x, y, z;
if (!parse_expression_multary(x, 8) || !expect(',') || !parse_expression_multary(y, 8))
return consume_until('}'), false;
// Parse optional third dimension (defaults to 1)
z.reset_to_rvalue_constant({}, 1);
if (accept(',') && !parse_expression_multary(z, 8))
return consume_until('}'), false;
if (!x.is_constant)
return error(x.location, 3011, "value must be a literal expression"), consume_until('}'), false;
if (!y.is_constant)
return error(y.location, 3011, "value must be a literal expression"), consume_until('}'), false;
if (!z.is_constant)
return error(z.location, 3011, "value must be a literal expression"), consume_until('}'), false;
x.add_cast_operation({ type::t_int, 1, 1 });
y.add_cast_operation({ type::t_int, 1, 1 });
z.add_cast_operation({ type::t_int, 1, 1 });
num_threads[0] = x.constant.as_int[0];
num_threads[1] = y.constant.as_int[0];
num_threads[2] = z.constant.as_int[0];
if (!expect('>'))
return consume_until('}'), false;
}
// Ignore invalid symbols that were added during error recovery
if (symbol.id != 0xFFFFFFFF)
{
if (is_shader_state)
{
if (!symbol.id)
parse_success = false,
error(location, 3501, "undeclared identifier '" + identifier + "', expected function name");
else if (!symbol.type.is_function())
parse_success = false,
error(location, 3020, "type mismatch, expected function name");
else {
// Look up the matching function info for this function definition
function_info &function_info = _codegen->get_function(symbol.id);
// We potentially need to generate a special entry point function which translates between function parameters and input/output variables
switch (state[0])
{
case 'V':
vs_info = function_info;
_codegen->define_entry_point(vs_info, shader_type::vs);
info.vs_entry_point = vs_info.unique_name;
break;
case 'P':
ps_info = function_info;
_codegen->define_entry_point(ps_info, shader_type::ps);
info.ps_entry_point = ps_info.unique_name;
break;
case 'C':
cs_info = function_info;
_codegen->define_entry_point(cs_info, shader_type::cs, num_threads);
info.cs_entry_point = cs_info.unique_name;
break;
}
}
}
else
{
assert(is_texture_state);
if (!symbol.id)
parse_success = false,
error(location, 3004, "undeclared identifier '" + identifier + "', expected texture name");
else if (!symbol.type.is_texture())
parse_success = false,
error(location, 3020, "type mismatch, expected texture name");
else if (symbol.type.texture_dimension() != 2)
parse_success = false,
error(location, 3020, "cannot use texture" + std::to_string(symbol.type.texture_dimension()) + "D as render target");
else {
reshadefx::texture_info &target_info = _codegen->get_texture(symbol.id);
// Texture is used as a render target
target_info.render_target = true;
// Verify that all render targets in this pass have the same dimensions
if (info.viewport_width != 0 && info.viewport_height != 0 && (target_info.width != info.viewport_width || target_info.height != info.viewport_height))
parse_success = false,
error(location, 4545, "cannot use multiple render targets with different texture dimensions (is " + std::to_string(target_info.width) + 'x' + std::to_string(target_info.height) + ", but expected " + std::to_string(info.viewport_width) + 'x' + std::to_string(info.viewport_height) + ')');
info.viewport_width = target_info.width;
info.viewport_height = target_info.height;
const auto target_index = state.size() > 12 ? (state[12] - '0') : 0;
info.render_target_names[target_index] = target_info.unique_name;
// Only RGBA8 format supports sRGB writes across all APIs
if (target_info.format != texture_format::rgba8)
targets_support_srgb = false;
}
}
}
else
{
parse_success = false;
}
}
else // Handle the rest of the pass states
{
backup();
expression expression;
if (accept(tokenid::identifier)) // Handle special enumeration names for pass states
{
// Transform identifier to uppercase to do case-insensitive comparison
std::transform(_token.literal_as_string.begin(), _token.literal_as_string.end(), _token.literal_as_string.begin(),
[](std::string::value_type c) {
return static_cast<std::string::value_type>(std::toupper(c));
});
static const std::unordered_map<std::string_view, uint32_t> s_enum_values = {
{ "NONE", 0 }, { "ZERO", 0 }, { "ONE", 1 },
{ "ADD", uint32_t(pass_blend_op::add) },
{ "SUBTRACT", uint32_t(pass_blend_op::subtract) },
{ "REVSUBTRACT", uint32_t(pass_blend_op::reverse_subtract) },
{ "MIN", uint32_t(pass_blend_op::min) },
{ "MAX", uint32_t(pass_blend_op::max) },
{ "SRCCOLOR", uint32_t(pass_blend_factor::source_color) },
{ "INVSRCCOLOR", uint32_t(pass_blend_factor::one_minus_source_color) },
{ "DESTCOLOR", uint32_t(pass_blend_factor::dest_color) },
{ "INVDESTCOLOR", uint32_t(pass_blend_factor::one_minus_dest_color) },
{ "SRCALPHA", uint32_t(pass_blend_factor::source_alpha) },
{ "INVSRCALPHA", uint32_t(pass_blend_factor::one_minus_source_alpha) },
{ "DESTALPHA", uint32_t(pass_blend_factor::dest_alpha) },
{ "INVDESTALPHA", uint32_t(pass_blend_factor::one_minus_dest_alpha) },
{ "KEEP", uint32_t(pass_stencil_op::keep) },
{ "REPLACE", uint32_t(pass_stencil_op::replace) },
{ "INVERT", uint32_t(pass_stencil_op::invert) },
{ "INCR", uint32_t(pass_stencil_op::increment) },
{ "INCRSAT", uint32_t(pass_stencil_op::increment_saturate) },
{ "DECR", uint32_t(pass_stencil_op::decrement) },
{ "DECRSAT", uint32_t(pass_stencil_op::decrement_saturate) },
{ "NEVER", uint32_t(pass_stencil_func::never) },
{ "EQUAL", uint32_t(pass_stencil_func::equal) },
{ "NEQUAL", uint32_t(pass_stencil_func::not_equal) }, { "NOTEQUAL", uint32_t(pass_stencil_func::not_equal) },
{ "LESS", uint32_t(pass_stencil_func::less) },
{ "GREATER", uint32_t(pass_stencil_func::greater) },
{ "LEQUAL", uint32_t(pass_stencil_func::less_equal) }, { "LESSEQUAL", uint32_t(pass_stencil_func::less_equal) },
{ "GEQUAL", uint32_t(pass_stencil_func::greater_equal) }, { "GREATEREQUAL", uint32_t(pass_stencil_func::greater_equal) },
{ "ALWAYS", uint32_t(pass_stencil_func::always) },
{ "POINTS", uint32_t(primitive_topology::point_list) },
{ "POINTLIST", uint32_t(primitive_topology::point_list) },
{ "LINES", uint32_t(primitive_topology::line_list) },
{ "LINELIST", uint32_t(primitive_topology::line_list) },
{ "LINESTRIP", uint32_t(primitive_topology::line_strip) },
{ "TRIANGLES", uint32_t(primitive_topology::triangle_list) },
{ "TRIANGLELIST", uint32_t(primitive_topology::triangle_list) },
{ "TRIANGLESTRIP", uint32_t(primitive_topology::triangle_strip) },
};
// Look up identifier in list of possible enumeration names
if (const auto it = s_enum_values.find(_token.literal_as_string);
it != s_enum_values.end())
expression.reset_to_rvalue_constant(_token.location, it->second);
else // No match found, so rewind to parser state before the identifier was consumed and try parsing it as a normal expression
restore();
}
// Parse right hand side as normal expression if no special enumeration name was matched already
if (!expression.is_constant && !parse_expression_multary(expression))
return consume_until('}'), false;
else if (!expression.is_constant || !expression.type.is_scalar())
parse_success = false,
error(expression.location, 3011, "pass state value must be a literal scalar expression");
// All states below expect the value to be of an unsigned integer type
expression.add_cast_operation({ type::t_uint, 1, 1 });
const unsigned int value = expression.constant.as_uint[0];
#define SET_STATE_VALUE_INDEXED(name, info_name, value) \
else if (constexpr size_t name##_len = sizeof(#name) - 1; state.compare(0, name##_len, #name) == 0 && (state.size() == name##_len || (state[name##_len] >= '0' && state[name##_len] < ('0' + static_cast<char>(std::size(info.info_name)))))) \
{ \
if (state.size() != name##_len) \
info.info_name[state[name##_len] - '0'] = (value); \
else \
for (int i = 0; i < static_cast<int>(std::size(info.info_name)); ++i) \
info.info_name[i] = (value); \
}
if (state == "SRGBWriteEnable")
info.srgb_write_enable = (value != 0);
SET_STATE_VALUE_INDEXED(BlendEnable, blend_enable, value != 0)
else if (state == "StencilEnable")
info.stencil_enable = (value != 0);
else if (state == "ClearRenderTargets")
info.clear_render_targets = (value != 0);
SET_STATE_VALUE_INDEXED(ColorWriteMask, color_write_mask, value & 0xFF)
SET_STATE_VALUE_INDEXED(RenderTargetWriteMask, color_write_mask, value & 0xFF)
else if (state == "StencilReadMask" || state == "StencilMask")
info.stencil_read_mask = value & 0xFF;
else if (state == "StencilWriteMask")
info.stencil_write_mask = value & 0xFF;
SET_STATE_VALUE_INDEXED(BlendOp, blend_op, static_cast<pass_blend_op>(value))
SET_STATE_VALUE_INDEXED(BlendOpAlpha, blend_op_alpha, static_cast<pass_blend_op>(value))
SET_STATE_VALUE_INDEXED(SrcBlend, src_blend, static_cast<pass_blend_factor>(value))
SET_STATE_VALUE_INDEXED(SrcBlendAlpha, src_blend_alpha, static_cast<pass_blend_factor>(value))
SET_STATE_VALUE_INDEXED(DestBlend, dest_blend, static_cast<pass_blend_factor>(value))
SET_STATE_VALUE_INDEXED(DestBlendAlpha, dest_blend_alpha, static_cast<pass_blend_factor>(value))
else if (state == "StencilFunc")
info.stencil_comparison_func = static_cast<pass_stencil_func>(value);
else if (state == "StencilRef")
info.stencil_reference_value = value;
else if (state == "StencilPass" || state == "StencilPassOp")
info.stencil_op_pass = static_cast<pass_stencil_op>(value);
else if (state == "StencilFail" || state == "StencilFailOp")
info.stencil_op_fail = static_cast<pass_stencil_op>(value);
else if (state == "StencilZFail" || state == "StencilDepthFail" || state == "StencilDepthFailOp")
info.stencil_op_depth_fail = static_cast<pass_stencil_op>(value);
else if (state == "VertexCount")
info.num_vertices = value;
else if (state == "PrimitiveType" || state == "PrimitiveTopology")
info.topology = static_cast<primitive_topology>(value);
else if (state == "DispatchSizeX")
info.viewport_width = value;
else if (state == "DispatchSizeY")
info.viewport_height = value;
else if (state == "DispatchSizeZ")
info.viewport_dispatch_z = value;
else if (state == "GenerateMipmaps" || state == "GenerateMipMaps")
info.generate_mipmaps = (value != 0);
else
parse_success = false,
error(location, 3004, "unrecognized pass state '" + state + '\'');
#undef SET_STATE_VALUE_INDEXED
}
if (!expect(';'))
return consume_until('}'), false;
}
if (parse_success)
{
if (!info.cs_entry_point.empty())
{
if (info.viewport_width == 0 || info.viewport_height == 0)
{
parse_success = false;
error(pass_location, 3012, "pass is missing 'DispatchSizeX' or 'DispatchSizeY' property");
}
if (!info.vs_entry_point.empty())
warning(pass_location, 3089, "pass is specifying both 'VertexShader' and 'ComputeShader' which cannot be used together");
if (!info.ps_entry_point.empty())
warning(pass_location, 3089, "pass is specifying both 'PixelShader' and 'ComputeShader' which cannot be used together");
for (codegen::id id : cs_info.referenced_samplers)
info.samplers.push_back(_codegen->get_sampler(id));
for (codegen::id id : cs_info.referenced_storages)
info.storages.push_back(_codegen->get_storage(id));
}
else if (info.vs_entry_point.empty() || info.ps_entry_point.empty())
{
parse_success = false;
if (info.vs_entry_point.empty())
error(pass_location, 3012, "pass is missing 'VertexShader' property");
if (info.ps_entry_point.empty())
error(pass_location, 3012, "pass is missing 'PixelShader' property");
}
else
{
// Verify that shader signatures between VS and PS match (both semantics and interpolation qualifiers)
std::unordered_map<std::string_view, type> vs_semantic_mapping;
if (vs_info.return_semantic.empty())
{
if (!vs_info.return_type.is_void() && !vs_info.return_type.is_struct())
{
parse_success = false;
error(pass_location, 3503, '\'' + vs_info.name + "': function return value is missing semantics");
}
}
else
{
vs_semantic_mapping[vs_info.return_semantic] = vs_info.return_type;
}
for (const struct_member_info &param : vs_info.parameter_list)
{
if (param.semantic.empty())
{
if (!param.type.is_struct())
{
parse_success = false;
if (param.type.has(type::q_in))
error(pass_location, 3502, '\'' + vs_info.name + "': input parameter '" + param.name + "' is missing semantics");
else
error(pass_location, 3503, '\'' + vs_info.name + "': output parameter '" + param.name + "' is missing semantics");
}
}
else if (param.type.has(type::q_out))
{
vs_semantic_mapping[param.semantic] = param.type;
}
}
if (ps_info.return_semantic.empty())
{
if (!ps_info.return_type.is_void() && !ps_info.return_type.is_struct())
{
parse_success = false;
error(pass_location, 3503, '\'' + ps_info.name + "': function return value is missing semantics");
}
}
for (const struct_member_info &param : ps_info.parameter_list)
{
if (param.semantic.empty())
{
if (!param.type.is_struct())
{
parse_success = false;
if (param.type.has(type::q_in))
error(pass_location, 3502, '\'' + ps_info.name + "': input parameter '" + param.name + "' is missing semantics");
else
error(pass_location, 3503, '\'' + ps_info.name + "': output parameter '" + param.name + "' is missing semantics");
}
}
else if (param.type.has(type::q_in))
{
if (const auto it = vs_semantic_mapping.find(param.semantic);
it == vs_semantic_mapping.end() || it->second != param.type)
warning(pass_location, 4576, '\'' + ps_info.name + "': input parameter '" + param.name + "' semantic does not match vertex shader one");
else if (((it->second.qualifiers ^ param.type.qualifiers) & (type::q_linear | type::q_noperspective | type::q_centroid | type::q_nointerpolation)) != 0)
parse_success = false,
error( pass_location, 4568, '\'' + ps_info.name + "': input parameter '" + param.name + "' interpolation qualifiers do not match vertex shader ones");
}
}
for (codegen::id id : vs_info.referenced_samplers)
info.samplers.push_back(_codegen->get_sampler(id));
for (codegen::id id : ps_info.referenced_samplers)
info.samplers.push_back(_codegen->get_sampler(id));
if (!vs_info.referenced_storages.empty() || !ps_info.referenced_storages.empty())
{
parse_success = false;
error(pass_location, 3667, "storage writes are only valid in compute shaders");
}
}
// Verify render target format supports sRGB writes if enabled
if (info.srgb_write_enable && !targets_support_srgb)
parse_success = false,
error(pass_location, 4582, "one or more render target(s) do not support sRGB writes (only textures with RGBA8 format do)");
}
return expect('}') && parse_success;
}