Add Chromium-only Blender WebEngine parity work

This commit is contained in:
mes123456
2026-08-12 04:47:48 -04:00
commit 9fd26010f6
18225 changed files with 11622124 additions and 0 deletions

View File

@@ -0,0 +1,59 @@
# SPDX-FileCopyrightText: 2023 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
# -----------------------------------------------------------------------------
# Build `shader_tool` executable.
set(SRC
attribute.cc
enum.cc
flow_control.cc
function.cc
grammar.cc
intermediate.cc
metadata.cc
namespace.cc
processor.cc
resource_table.cc
shader_tool.cc
string.cc
struct.cc
template.cc
union.cc
enums.hh
expression.hh
intermediate.hh
metadata.hh
processor.hh
scope.hh
time_it.hh
token.hh
token_stream.hh
utils.hh
lexit/lexit.cc
lexit/identifier.hh
lexit/lexit.hh
lexit/simd.hh
lexit/tables.hh
lexit/types.hh
lexit/vector.hh
)
if(WITH_GPU_SHADER_ASSERT)
add_definitions(-DWITH_GPU_SHADER_ASSERT)
endif()
# `SRC_DNA_INC` is defined in the parent directory.
add_executable(shader_tool ${SRC})
if(EMSCRIPTEN)
target_link_options(shader_tool PRIVATE
"-sMODULARIZE=0" "-sEXPORT_ES6=0" "-sENVIRONMENT=node" "-sEXIT_RUNTIME=1"
"-sNODERAWFS=1")
add_custom_command(TARGET shader_tool POST_BUILD
COMMAND chmod +x "$<TARGET_FILE:shader_tool>")
endif()
optimize_debug_target(shader_tool)

View File

@@ -0,0 +1,182 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup shader_tool
*/
#include "intermediate.hh"
#include "metadata.hh"
#include "processor.hh"
namespace blender::gpu::shader {
using namespace std;
using namespace shader::parser;
using namespace metadata;
void SourceProcessor::lower_maybe_unused(Parser &parser)
{
using namespace metadata;
parser().foreach_token(SquareOpen, [&](Token par_open) {
if (par_open.next() != '[') {
return;
}
Scope attributes = par_open.next().scope();
attributes.foreach_attribute([&](Token attr, Scope) {
if (attr.str() == "maybe_unused") {
if (attr.next() == ',') {
parser.erase(attr, attr.next());
}
else if (attr.prev() == ',') {
parser.erase(attr.prev(), attr);
}
else if (attr.next() == ']') {
parser.erase(attributes.scope());
}
}
});
});
parser.apply_mutations();
}
void SourceProcessor::lint_attributes(Parser &parser)
{
parser().foreach_token(SquareOpen, [&](Token par_open) {
if (par_open.next() != '[') {
return;
}
Scope attributes = par_open.next().scope();
bool invalid = false;
attributes.foreach_attribute([&](Token attr, Scope attr_scope) {
string attr_str = string(attr.str());
if (attr_str == "base_instance" || attr_str == "clip_distance" ||
attr_str == "compilation_constant" || attr_str == "compute" || attr_str == "shared" ||
attr_str == "early_fragment_tests" || attr_str == "flat" || attr_str == "frag_coord" ||
attr_str == "frag_stencil_ref" || attr_str == "fragment" || attr_str == "front_facing" ||
attr_str == "global_invocation_id" || attr_str == "in" || attr_str == "instance_id" ||
attr_str == "instance_index" || attr_str == "layer" ||
attr_str == "local_invocation_id" || attr_str == "local_invocation_index" ||
attr_str == "no_perspective" || attr_str == "num_work_groups" || attr_str == "out" ||
attr_str == "subpass_in" || attr_str == "point_coord" || attr_str == "point_size" ||
attr_str == "position" || attr_str == "push_constant" || attr_str == "resource_table" ||
attr_str == "smooth" || attr_str == "vertex_id" || attr_str == "legacy_info" ||
attr_str == "vertex" || attr_str == "viewport_index" || attr_str == "work_group_id" ||
attr_str == "maybe_unused" || attr_str == "fallthrough" || attr_str == "nodiscard" ||
attr_str == "node" || attr_str == "clip_control" || attr_str == "texture_atomic")
{
if (attr_scope.is_valid()) {
report_error(attr, "This attribute requires no argument");
invalid = true;
}
}
else if (attr_str == "attribute" || attr_str == "index" || attr_str == "frag_color" ||
attr_str == "frag_depth" || attr_str == "uniform" || attr_str == "condition" ||
attr_str == "raster_order_group" || attr_str == "frequency" ||
attr_str == "sampler" || attr_str == "specialization_constant")
{
if (attr_scope.is_invalid()) {
report_error(attr, "This attribute requires 1 argument");
invalid = true;
}
}
else if (attr_str == "storage" || attr_str == "subpass_input") {
if (attr_scope.is_invalid()) {
report_error(attr, "This attribute requires 2 arguments");
invalid = true;
}
}
else if (attr_str == "image") {
if (attr_scope.is_invalid()) {
report_error(attr, "This attribute requires 3 arguments");
invalid = true;
}
}
else if (attr_str == "local_size") {
if (attr_scope.is_invalid()) {
report_error(attr, "This attribute requires at least 1 argument");
invalid = true;
}
}
else if (attr_str == "metal_max_total_threads_per_threadgroup") {
if (attr_scope.is_invalid()) {
report_error(attr, "This attribute requires at least 1 argument");
invalid = true;
}
}
else if (attr_str == "host_shared") {
if (attributes.front().prev().prev() != Struct && attributes.front().prev().prev() != Enum)
{
report_error(
attr, "host_shared attributes must be placed after a struct or an enum definition");
invalid = true;
}
/* Placement already checked. */
return;
}
else if (attr_str == "unroll" || attr_str == "unroll_n") {
if (attributes.front().prev().prev().scope().front().prev() != For) {
report_error(attr, "[[unroll]] attribute must be declared after a 'for' statement");
invalid = true;
}
/* Placement already checked. */
return;
}
else if (attr_str == "static_branch") {
if (attributes.front().prev().prev().scope().front().prev() != If) {
report_error(attr,
"[[static_branch]] attribute must be declared after a 'if' condition");
invalid = true;
}
/* Placement already checked. */
return;
}
else {
report_error(attr, "Unrecognized attribute");
invalid = true;
/* Attribute already invalid, don't check placement. */
return;
}
if (attr_str == "fallthrough") {
/* Placement is too complicated to check. C++ compilation should already have checked. */
return;
}
Token prev_tok = attributes.front().prev().prev();
if (prev_tok == '(' || prev_tok == '{' || prev_tok == ';' || prev_tok == ',' ||
prev_tok == '}' || prev_tok == ')' || prev_tok == '\n' || prev_tok == ' ' ||
prev_tok == '>' || prev_tok.is_invalid() ||
prev_tok.scope().type() == ScopeType::Preprocessor)
{
/* Placement is maybe correct. Could refine a bit more. */
}
else {
report_error(attr, "attribute must be declared at a start of a declaration");
invalid = true;
}
});
if (invalid) {
/* Erase invalid attributes to avoid spawning more errors. */
parser.erase(attributes.scope());
}
});
parser.apply_mutations();
}
/* Merge attribute scopes. They are equivalent in the C++ standard.
* This allow to simplify parsing later on.
* `[[a]] [[b]]` > `[[a, b]]` */
void SourceProcessor::lower_attribute_sequences(Parser &parser)
{
do {
parser().foreach_match("[[..]][[..]]", [&](vector<Token> toks) {
parser.insert_before(toks[4], ",");
parser.erase(toks[4], toks[7]);
});
} while (parser.apply_mutations());
}
} // namespace blender::gpu::shader

View File

@@ -0,0 +1,142 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup shader_tool
*/
#include "intermediate.hh"
#include "metadata.hh"
#include "processor.hh"
namespace blender::gpu::shader {
using namespace std;
using namespace shader::parser;
using namespace metadata;
void SourceProcessor::lower_enums(Parser &parser)
{
/**
* Transform C,C++ enum declaration into GLSL compatible defines and constants:
*
* \code{.cpp}
* enum MyEnum : uint {
* ENUM_1 = 0u,
* ENUM_2 = 1u,
* ENUM_3 = 2u,
* };
* \endcode
*
* becomes
*
* \code{.glsl}
* #define MyEnum uint
* constant static constexpr uint ENUM_1 = 0u;
* constant static constexpr uint ENUM_2 = 1u;
* constant static constexpr uint ENUM_3 = 2u;
*
* \endcode
*
* It is made like so to avoid messing with error lines, allowing to point at the exact
* location inside the source file.
*
* IMPORTANT: This has some requirements:
* - Enums needs to have underlying types set to uint32_t to make them usable in UBO and SSBO.
*/
auto missing_underlying_type = [&](vector<Token> tokens) {
report_error(tokens[0], "enum declaration must explicitly use an underlying type");
};
parser().foreach_match("MA{", missing_underlying_type);
parser().foreach_match("MSA{", missing_underlying_type);
const string placeholder_value = "=__auto__";
auto placeholder = [&](Scope enum_scope) {
const string &value = placeholder_value;
const string start = " = 0" + string(enum_scope.front().prev().str()[0] == 'u' ? "u" : "");
auto insert = [&](Token name, const string &replacement) {
if (name.next() == ',' || name.next() == '}') {
parser.insert_after(name, replacement);
}
};
enum_scope.foreach_match("{A", [&](const Tokens &t) { insert(t[1], start); });
enum_scope.foreach_match(",A", [&](const Tokens &t) { insert(t[1], value); });
};
parser().foreach_match("MSA:A{", [&](const Tokens &t) { placeholder(t[5].scope()); });
parser().foreach_match("MA:A{", [&](const Tokens &t) { placeholder(t[4].scope()); });
parser().foreach_match("MS[[A]]A:A{", [&](const Tokens &t) { placeholder(t[10].scope()); });
parser().foreach_match("M[[A]]A:A{", [&](const Tokens &t) { placeholder(t[9].scope()); });
parser.apply_mutations();
auto process_enum = [&](Token enum_tok,
Token class_tok,
Token enum_name,
Token enum_type,
Scope enum_scope,
const bool is_host_shared) {
const string type_str(enum_type.str());
const string enum_name_str(enum_name.str());
string previous_value = "error_invalid_first_value";
enum_scope.foreach_scope(ScopeType::Assignment, [&](Scope scope) {
Token name_tok = scope.front().prev();
string name(name_tok.str());
string value(scope.str());
if (value == placeholder_value) {
value = "= " + previous_value + " + 1" + (enum_type.str()[0] == 'u' ? "u" : "");
}
if (class_tok.is_valid()) {
name = enum_name_str + "::" + name;
}
string decl = "constant static constexpr " + type_str + " " + name + " " + value + ";\n";
parser.insert_line_number(enum_tok.prev(), name_tok.line_number());
parser.insert_after(enum_tok.prev(), decl);
previous_value = name;
});
parser.insert_directive(enum_tok.prev(),
"#define " + enum_name_str + " " + string(enum_type.str()) + "\n");
if (is_host_shared) {
if (type_str != "uint32_t" && type_str != "int32_t") {
report_error(enum_type,
"Host shared enum declaration must use uint32_t or int32_t underlying type");
return;
}
string define = "#define ";
define += enum_name_str + linted_struct_suffix + " " + enum_name_str + "\n";
parser.insert_directive(enum_tok.prev(), define);
}
const string ctor_decl = enum_name_str + " " + enum_name_str + "_ctor_() { return " +
enum_name_str + "(0); }";
parser.insert_directive(enum_tok.prev(), ctor_decl);
parser.erase(enum_tok, enum_scope.back().next());
};
parser().foreach_match("MSA:A{", [&](vector<Token> tokens) {
process_enum(tokens[0], tokens[1], tokens[2], tokens[4], tokens[5].scope(), false);
});
parser().foreach_match("MA:A{", [&](vector<Token> tokens) {
process_enum(tokens[0], Token(parser), tokens[1], tokens[3], tokens[4].scope(), false);
});
parser().foreach_match("MS[[A]]A:A{", [&](vector<Token> tokens) {
process_enum(tokens[0], tokens[1], tokens[7], tokens[9], tokens[10].scope(), true);
});
parser().foreach_match("M[[A]]A:A{", [&](vector<Token> tokens) {
process_enum(tokens[0], Token(parser), tokens[6], tokens[8], tokens[9].scope(), true);
});
parser.apply_mutations();
parser().foreach_token(Enum, [&](Token tok) {
report_error(tok, "invalid enum declaration, likely missing underlying type");
});
}
} // namespace blender::gpu::shader

View File

@@ -0,0 +1,56 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup shader_tool
*
*/
#pragma once
#include "lexit/lexit.hh"
namespace blender::gpu::shader::parser {
using namespace lexit;
enum class ScopeType : char {
Invalid = 0,
/* Use ascii chars to store them in string, and for easy debugging / testing. */
Global = 'G',
Namespace = 'N',
Struct = 'S',
Function = 'F',
LoopArgs = 'l',
LoopBody = 'p',
SwitchArg = 'w',
SwitchBody = 'W',
FunctionArgs = 'f',
FunctionCall = 'c',
Template = 'T',
TemplateArg = 't',
Subscript = 'A',
Preprocessor = 'P',
Assignment = 'a',
Attributes = 'B',
Attribute = 'b',
/* Added scope inside function body. */
Local = 'L',
/* Added scope inside FunctionArgs. */
FunctionArg = 'g',
/* Added scope inside FunctionCall. */
FunctionParam = 'm',
/* Added scope inside LoopArgs. */
LoopArg = 'r',
Statement = 's',
Separator = 'o',
/* Undetermined. */
Angle = Template,
Bracket = Local,
Parenthesis = FunctionArgs,
Square = Subscript,
};
} // namespace blender::gpu::shader::parser

View File

@@ -0,0 +1,254 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup shader_tool
*
* Simple integer logic expression using a Pratt-parser.
*/
#pragma once
#include "token.hh"
#include "token_stream.hh"
#include <stdexcept>
namespace blender::gpu::shader::parser {
/**
* Same as FullLexer but considers angle brackets as multitokens, allowing identification of
* operators using them.
*/
struct ExpressionLexer {
static void lexical_analysis(LexerBase &lex, std::string_view input)
{
lex.process(input, LexerBase::default_char_class_table.data());
lex.merge_complex_literals();
lex.identify_keywords();
}
};
/**
* Simple expression parsing and evaluation.
* Will evaluate starting the given token until the end of the token stream.
* As this is supposed to be use for preprocessor directives, unknown identifiers (words) will
* evaluate to 0.
*/
class ExpressionParser : public Parser<ExpressionLexer, NullParser> {
public:
int64_t eval() const
{
assert(size_ > 0);
EvalContext ctx((*this)[0]);
int64_t v = ctx.expr(0);
if (ctx.peek() != Invalid) {
throw std::runtime_error("Trailing input");
}
return v;
}
private:
struct EvalContext {
private:
Token tok;
public:
EvalContext(Token tok) : tok(tok) {}
int64_t expr(int right_binding_power)
{
/* Parse unary operator, evaluate parenthesis, evaluate constant. */
int64_t left = nud(consume());
/* While left binding power is greater than the right, continue consuming binary operations.
*/
while (left_binding_power(peek().type()) > right_binding_power) {
left = led(left, consume());
}
return left;
}
/* How a token evaluates without left context (e.g. unary operator).
* Also known as Null-Denotation or NUD. */
int64_t nud(const Token &t)
{
/* Unary operators must have the highest precedence. */
static constexpr int unary_binding_power = 1000;
/* Let parenthesis evaluate everything until a closing parenthesis. */
static constexpr int parenthesis_binding_power = 0;
switch (t.type()) {
case Word:
/* Undefined identifier (not macro substituted). Evaluate to 0. */
return 0;
case Number:
return std::stol(std::string(t.str()));
case Plus:
return +expr(unary_binding_power);
case Minus:
return -expr(unary_binding_power);
case Not: {
int v = expr(unary_binding_power);
/* Note that '!' token is of MultiTok class and can contain many unary '!'. */
return (t.str().size() & 1) ? !v : !!v;
}
case BitwiseNot:
return ~expr(unary_binding_power);
case ParOpen: {
/* Parse the whole parenthesis expression. */
int64_t v = expr(parenthesis_binding_power);
/* Consume the closing parenthesis. */
if (consume() != ParClose) {
throw std::runtime_error("Expected ')'");
}
return v;
}
default:
throw std::runtime_error("Invalid expression");
}
}
/* How a token evaluates from left-to-right, on two operands.
* Also known as Left-Denotation or LED. */
int64_t led(int64_t left, const Token &t)
{
switch (t.type()) {
case Multiply:
return left * expr(left_binding_power(Multiply));
case Divide: {
int64_t right = expr(left_binding_power(Divide));
if (right == 0) {
throw std::runtime_error("Division by zero");
}
return left / right;
}
case Modulo: {
int64_t right = expr(left_binding_power(Modulo));
if (right == 0) {
throw std::runtime_error("Modulo by zero");
}
return left % right;
}
case Plus:
return left + expr(left_binding_power(Plus));
case Minus:
return left - expr(left_binding_power(Minus));
#if 0 /* Not implemented yet. */
case LShift:
return left << expression(binding_power(LShift));
case RShift:
return left >> expression(binding_power(RShift));
#endif
case LThan:
return left < expr(left_binding_power(LThan));
case LEqual:
return left <= expr(left_binding_power(LEqual));
case GThan:
return left > expr(left_binding_power(GThan));
case GEqual:
return left >= expr(left_binding_power(GEqual));
case Equal:
return left == expr(left_binding_power(Equal));
case NotEqual:
return left != expr(left_binding_power(NotEqual));
case And:
return left & expr(left_binding_power(And));
case Xor:
return left ^ expr(left_binding_power(Xor));
case Or:
return left | expr(left_binding_power(Or));
case LogicalAnd: {
/* Avoid short circuit. */
int right = expr(left_binding_power(LogicalAnd));
return left && right;
}
case LogicalOr: {
/* Avoid short circuit. */
int right = expr(left_binding_power(LogicalOr));
return left || right;
}
case Question: {
/* The middle expression can be almost anything.
* We use 0 so it only stops at the ':' (since Colon has a precedence of 0). */
int64_t tval = expr(0);
if (consume().type() != Colon) {
throw std::runtime_error("Expected ':'");
}
/* Use (Precedence - 1) to handle right-associativity. */
int64_t fval = expr(left_binding_power(Question) - 1);
return left ? tval : fval;
}
default:
throw std::runtime_error("Invalid operator");
}
}
int left_binding_power(TokenType type)
{
switch (type) {
case Multiply:
case Divide:
case Modulo:
return 110;
case Plus:
case Minus:
return 100;
#if 0 /* Not implemented yet. */
case LShift:
case RShift:
return 90;
#endif
case LThan:
case LEqual:
case GThan:
case GEqual:
return 80;
case Equal:
case NotEqual:
return 70;
case And:
return 60;
case Xor:
return 50;
case Or:
return 40;
case LogicalAnd:
return 30;
case LogicalOr:
return 20;
case Question:
return 10;
case Colon:
case ParOpen:
case ParClose:
return 0;
case Not:
case BitwiseNot:
/* Prefix operators don't bind to the left! */
return 0;
case Invalid: /* EndOfFile */
return -1;
default:
break;
}
throw std::runtime_error("Invalid token");
return 0;
}
Token peek() const
{
return tok;
}
Token consume()
{
Token t = tok;
tok = tok.next();
return t;
}
};
};
} // namespace blender::gpu::shader::parser

View File

@@ -0,0 +1,355 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup shader_tool
*/
#include "intermediate.hh"
#include "metadata.hh"
#include "processor.hh"
namespace blender::gpu::shader {
using namespace std;
using namespace shader::parser;
using namespace metadata;
void SourceProcessor::lower_loop_unroll(Parser &parser)
{
struct ArgParseResult {
Scope init, condition, iter;
};
auto parse_for_args = [&](const Scope loop_args) -> ArgParseResult {
ArgParseResult result{Scope(parser), Scope(parser), Scope(parser)};
loop_args.foreach_scope(ScopeType::LoopArg, [&](const Scope arg) {
if (arg.front().prev() == '(' && arg.back().next() == ';') {
result.init = arg;
}
else if (arg.front().prev() == ';' && arg.back().next() == ';') {
result.condition = arg;
}
else if (arg.front().prev() == ';' && arg.back().next() == ')') {
result.iter = arg;
}
else {
report_error(arg.front(), "Invalid loop declaration.");
}
});
return result;
};
auto process_loop = [&](const Token loop_start,
const int iter_count,
const int iter_init,
const int iter_incr,
const bool condition_is_trivial,
const bool iteration_is_trivial,
const Scope init,
const Scope cond,
const Scope iter,
const Scope body,
const string body_prefix = "",
const string body_suffix = "") {
/* Check that there is no unsupported keywords in the loop body. */
bool error = false;
/* Checks if `continue` exists, even in switch statement inside the unrolled loop. */
body.foreach_token(Continue, [&](const Token token) {
if (token.scope().first_scope_of_type(ScopeType::LoopBody) == body) {
report_error(token, "Unrolled loop cannot contain \"continue\" statement.");
error = true;
}
});
/* Checks if `break` exists directly the unrolled loop scope. Switch statements are ok. */
body.foreach_token(Break, [&](const Token token) {
if (token.scope().first_scope_of_type(ScopeType::LoopBody) == body) {
const Scope switch_scope = token.scope().first_scope_of_type(ScopeType::SwitchBody);
if (switch_scope.is_invalid() || !body.contains(switch_scope)) {
report_error(token, "Unrolled loop cannot contain \"break\" statement.");
error = true;
}
}
});
if (error) {
return;
}
if (!parser.replace_try(loop_start, body.back(), "", true)) {
/* This is the case of nested loops. This loop will be processed in another parser pass. */
return;
}
string indent_init, indent_cond, indent_iter;
if (init.is_valid()) {
indent_init = string(init.front().char_number() - 1, ' ');
}
if (cond.is_valid()) {
indent_cond = string(cond.front().char_number() - 3, ' ');
}
if (iter.is_valid()) {
indent_iter = string(iter.front().char_number(), ' ');
}
string indent_body = string(body.front().char_number(), ' ');
string indent_end = string(body.back().char_number(), ' ');
/* If possible, replaces the index of the loop iteration inside the given string. */
auto replace_index = [&](const string &str, int loop_index) {
if (iter.is_invalid() || !iteration_is_trivial || str.empty()) {
return str;
}
Parser str_parser(str, error_handler);
str_parser().foreach_token(Word, [&](const Token tok) {
if (tok.str() == iter[0].str()) {
str_parser.replace(tok, to_string(loop_index), true);
}
});
return str_parser.result_get();
};
parser.insert_after(body.back(), "\n");
if (init.is_valid() && !iteration_is_trivial) {
parser.insert_line_number(body.back(), init.front().line_number());
parser.insert_after(body.back(),
indent_init + "{" + string(init.str_with_whitespace()) + ";\n");
}
else {
parser.insert_after(body.back(), "{\n");
}
for (int64_t i = 0, value = iter_init; i < iter_count; i++, value += iter_incr) {
if (cond.is_valid() && !condition_is_trivial) {
parser.insert_line_number(body.back(), cond.front().line_number());
parser.insert_after(body.back(),
indent_cond + "if(" + string(cond.str_with_whitespace()) + ")\n");
}
parser.insert_after(body.back(), replace_index(body_prefix, value));
parser.insert_line_number(body.back(), body.front().line_number());
parser.insert_after(body.back(),
indent_body + replace_index(string(body.str_with_whitespace()), value) +
"\n");
parser.insert_after(body.back(), body_suffix);
if (iter.is_valid() && !iteration_is_trivial) {
parser.insert_line_number(body.back(), iter.front().line_number());
parser.insert_after(body.back(), indent_iter + string(iter.str_with_whitespace()) + ";\n");
}
}
parser.insert_line_number(body.back(), body.back().line_number());
parser.insert_after(body.back(), indent_end + string(body.back().str_with_whitespace()));
};
do {
/* WORKAROUND: We need to differentiate for and switch statements apart for proper break and
* continue statement usage linting. For this, we modify the body scope types to be able to
* detect which loop or switch body the break and continue statements are part of. */
parser().foreach_match("f(..)[[..]]{..}", [&](const vector<Token> tokens) {
tokens[11].scope().set_type(ScopeType::LoopBody);
});
parser().foreach_match("f(..){..}", [&](const vector<Token> tokens) {
tokens[5].scope().set_type(ScopeType::LoopBody);
});
parser().foreach_match("h(..){..}", [&](const vector<Token> tokens) {
tokens[5].scope().set_type(ScopeType::SwitchBody);
});
/* [[unroll]]. */
parser().foreach_match("f(..)[[A]]{..}", [&](const vector<Token> tokens) {
if (tokens[6].scope().str_with_whitespace() != "[unroll]") {
return;
}
const Token for_tok = tokens[0];
const Scope loop_args = tokens[1].scope();
const Scope loop_body = tokens[10].scope();
auto [init, cond, iter] = parse_for_args(loop_args);
/* Init statement. */
const Token var_type = init[0];
const Token var_name = init[1];
const Token var_init = init[2];
if (var_type.str() != "int" && var_type.str() != "uint") {
report_error(var_init, "Can only unroll integer based loop.");
return;
}
if (var_init != '=') {
report_error(var_init, "Expecting assignment here.");
return;
}
if (init[3] != Number && init[3] != '-') {
report_error(init[3], "Expecting integer literal here.");
return;
}
/* Conditional statement. */
int t = 0;
const Token cond_var = cond[t++];
const Token cond_type = cond[t++];
if (cond_type.next() == '=') {
t++; /* Skip equal sign. */
}
const Token cond_sign = (cond[t] == '+' || cond[t] == '-') ? cond[t++] : Token(parser);
const Token cond_end = cond[t];
if (cond_var.str() != var_name.str()) {
report_error(cond_var, "Non matching loop counter variable.");
return;
}
if (cond_end != Number) {
report_error(cond_end, "Expecting integer literal here.");
return;
}
/* Iteration statement. */
const Token iter_var = iter[0];
const Token iter_type = iter[1];
const Token iter_end = iter[1];
int iter_incr = 0;
if (iter_var.str() != var_name.str()) {
report_error(iter_var, "Non matching loop counter variable.");
return;
}
if (iter_type == Increment) {
iter_incr = +1;
if (cond_type == '>') {
report_error(for_tok, "Unsupported condition in unrolled loop.");
return;
}
}
else if (iter_type == Decrement) {
iter_incr = -1;
if (cond_type == '<') {
report_error(for_tok, "Unsupported condition in unrolled loop.");
return;
}
}
else {
report_error(iter_type, "Unsupported loop expression. Expecting ++ or --.");
return;
}
int64_t init_value = stol(
parser.substr_range_inclusive(var_init.next(), var_init.scope().back()));
int64_t end_value = stol(
parser.substr_range_inclusive(cond_sign.is_valid() ? cond_sign : cond_end, cond_end));
/* TODO(fclem): Support arbitrary strides (aka, arbitrary iter statement). */
int iter_count = abs(end_value - init_value);
if (cond_type.next() == '=') {
iter_count += 1;
}
bool condition_is_trivial = (cond_end == cond.back());
bool iteration_is_trivial = (iter_end == iter.back());
process_loop(tokens[0],
iter_count,
init_value,
iter_incr,
condition_is_trivial,
iteration_is_trivial,
init,
cond,
iter,
loop_body);
});
/* [[unroll_n(n)]]. */
parser().foreach_match("f(..)[[A(1)]]{..}", [&](const vector<Token> tokens) {
if (tokens[7].str() != "unroll_n") {
return;
}
const Scope loop_args = tokens[1].scope();
const Scope loop_body = tokens[13].scope();
auto [init, cond, iter] = parse_for_args(loop_args);
int iter_count = stol(string(tokens[9].str()));
process_loop(tokens[0], iter_count, 0, 0, false, false, init, cond, iter, loop_body);
});
} while (parser.apply_mutations());
/* Check for remaining keywords. */
parser().foreach_match("[[A", [&](const vector<Token> tokens) {
if (tokens[2].str().find("unroll") != string::npos) {
report_error(tokens[0], "Incompatible loop format for [[unroll]].");
}
});
}
void SourceProcessor::lower_static_branch(Parser &parser)
{
do {
/* Transform `if constexpr (...)` into `if (...) [[static_branch]]`. */
parser().foreach_match("iC(..)", [&](const vector<Token> &tokens) {
parser.erase(tokens[1]);
parser.insert_after(tokens[5], "[[static_branch]]");
});
} while (parser.apply_mutations());
do {
parser().foreach_match("i(..)[[A]]{..}", [&](const vector<Token> &tokens) {
Token if_tok = tokens[0];
Scope condition = tokens[1].scope();
Token attribute = tokens[7];
Scope body = tokens[10].scope();
if (attribute.str() != "static_branch") {
return;
}
if (condition.str().find("&&") != string::npos || condition.str().find("||") != string::npos)
{
report_error(condition[0], "Expecting single condition.");
return;
}
const int i = condition[1].str() == "!" ? 2 : 1;
/* TODO(fclem): Make full check of all params. */
const bool is_constexpr = condition[i].str() == "true" || condition[i].str() == "false";
const bool constexpr_val = is_constexpr ? (condition[i].str() == "true") ^ (i == 2) : false;
if (condition[i].str() != "srt_access" && !is_constexpr) {
report_error(if_tok,
"Expecting compilation or specialization constant. Make sure SRT arguments "
"have the [[resource_table]] attribute.");
return;
}
Token before_body = body.front().prev();
string test = is_constexpr ? string(constexpr_val ? "1" : "0") :
string(i == 2 ? "!" : "") + "SRT_CONSTANT_" +
string(condition[4 + i].str()) + " ";
if (condition[is_constexpr ? 2 : 7] != condition.back().prev()) {
test += parser.substr_range_inclusive(condition[is_constexpr ? 2 : 7],
condition.back().prev());
}
string directive = (if_tok.prev() == Else ? "#elif " : "#if ");
parser.insert_directive(before_body, directive + test);
parser.erase(if_tok, before_body);
if (body.back().next() == Else) {
Token else_tok = body.back().next();
parser.erase(else_tok);
if (else_tok.next() == If) {
/* Will be processed later. */
Token next_if = else_tok.next();
/* Ensure the rest of the if clauses also have the attribute. */
Scope attributes = next_if.next().scope().back().next().scope();
if (attributes.type() != ScopeType::Subscript ||
attributes.front().next().scope().str_exclusive() != "static_branch")
{
report_error(next_if, "Expecting next if statement to also be a static branch.");
return;
}
return;
}
body = else_tok.next().scope();
parser.insert_directive(else_tok, "#else");
}
parser.insert_directive(body.back(), "#endif");
});
} while (parser.apply_mutations());
}
} // namespace blender::gpu::shader

View File

@@ -0,0 +1,541 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup shader_tool
*/
#include "intermediate.hh"
#include "metadata.hh"
#include "processor.hh"
namespace blender::gpu::shader {
using namespace std;
using namespace shader::parser;
using namespace metadata;
/* Parse entry point definitions and mutating all parameter usage to global resources. */
void SourceProcessor::lower_entry_points(Parser &parser)
{
using namespace metadata;
auto to_uppercase = [](string str) {
for (char &c : str) {
c = toupper(c);
}
return str;
};
parser().foreach_function([&](bool, Token type, Token fn_name, Scope args, bool, Scope fn_body) {
bool is_entry_point = false;
bool is_compute_func = false;
bool is_vertex_func = false;
bool is_fragment_func = false;
bool use_early_frag_test = false;
bool use_clip_control = false;
bool use_texture_atomic = false;
string metal_max_total_threads_per_threadgroup;
string local_size;
if (type.prev() == ']' && type.prev().scope().type() == ScopeType::Subscript) {
Scope attributes = type.prev().prev().scope();
attributes.foreach_attribute([&](Token attr, Scope attr_scope) {
const string attr_str(attr.str());
if (attr_str == "vertex") {
is_vertex_func = true;
is_entry_point = true;
}
else if (attr_str == "fragment") {
is_fragment_func = true;
is_entry_point = true;
}
else if (attr_str == "compute") {
is_compute_func = true;
is_entry_point = true;
}
else if (attr_str == "early_fragment_tests") {
use_early_frag_test = true;
}
else if (attr_str == "local_size") {
local_size = attr_scope.str();
}
else if (attr_str == "metal_max_total_threads_per_threadgroup") {
metal_max_total_threads_per_threadgroup = attr_scope.str();
}
else if (attr_str == "clip_control") {
use_clip_control = true;
}
else if (attr_str == "texture_atomic") {
use_texture_atomic = true;
}
});
}
if (is_entry_point && type.str() != "void") {
report_error(type, "Entry point function must return void.");
return;
}
auto parse_condition = [&](const Scope &attributes) {
string cond;
attributes.foreach_attribute([&](Token attribute_name, Scope attribute_parameters) {
if (attribute_name.str() == "condition") {
if (!cond.empty()) {
report_error(attribute_name, "Only one condition attribute is allowed.");
return;
}
attribute_parameters[1].scope().foreach_token(Word, [&](const Token tok) {
cond += "int " + string(tok.str()) + " = ";
cond += "ShaderCreateInfo::find_constant(constants, \"" + string(tok.str()) + "\"); ";
});
cond += "return " + string(attribute_parameters[1].scope().str()) + ";";
}
});
if (!cond.empty()) {
cond = ", [](blender::Span<CompilationConstant> constants) { " + cond + "}";
}
return cond;
};
auto replace_word = [&](const string &replaced, const string &replacement) {
fn_body.foreach_token(Word, [&](const Token tok) {
if (tok.str() == replaced) {
parser.replace(tok, replacement, true);
}
});
};
auto replace_word_and_accessor = [&](const string &replaced, const string &replacement) {
fn_body.foreach_token(Word, [&](const Token tok) {
if (tok.next().type() == Dot && tok.str() == replaced) {
parser.replace(tok, tok.next(), replacement);
}
});
};
/* For now, just emit good old create info macros. */
string create_info_decl;
if (!local_size.empty()) {
if (!is_compute_func) {
report_error(type, "Only compute entry point function can use [[local_size(x,y,z)]].");
}
else {
create_info_decl += "LOCAL_GROUP_SIZE" + local_size + "\n";
}
}
if (use_early_frag_test) {
if (!is_fragment_func) {
report_error(type, "Only fragment entry point function can use [[use_early_frag_test]].");
}
else {
create_info_decl += "EARLY_FRAGMENT_TEST(true)\n";
}
}
if (use_clip_control) {
if (!is_vertex_func) {
report_error(type, "Only vertex entry point function can use [[clip_control]].");
}
else {
create_info_decl += "BUILTINS(BuiltinBits::CLIP_CONTROL)\n";
}
}
if (use_texture_atomic) {
create_info_decl += "BUILTINS(BuiltinBits::TEXTURE_ATOMIC)\n";
}
if (!metal_max_total_threads_per_threadgroup.empty()) {
if (!is_compute_func) {
report_error(type,
"Only compute entry point function can use "
"[[metal_max_total_threads_per_threadgroup(x)]].");
}
else {
create_info_decl += "MTL_MAX_TOTAL_THREADS_PER_THREADGROUP" +
metal_max_total_threads_per_threadgroup + "\n";
}
}
auto process_argument = [&](Token type, Token var, Scope attributes) {
const bool is_const = type.prev() == Const;
string srt_type(type.str());
string srt_var(var.str());
string srt_attr(attributes[1].str());
if (srt_attr == "vertex_id" && is_entry_point) {
if (!is_vertex_func) {
report_error(attributes[1], "[[vertex_id]] is only supported in vertex functions.");
}
else if (!is_const || srt_type != "int") {
report_error(type, "[[vertex_id]] must be declared as `const int`.");
}
replace_word(srt_var, "gl_VertexID");
metadata_.builtins.emplace_back(Builtin(hash("gl_VertexID")));
create_info_decl += "BUILTINS(BuiltinBits::VERTEX_ID)\n";
}
else if (srt_attr == "instance_id" && is_entry_point) {
if (!is_vertex_func) {
report_error(attributes[1], "[[instance_id]] is only supported in vertex functions.");
}
else if (!is_const || srt_type != "int") {
report_error(type, "[[instance_id]] must be declared as `const int`.");
}
replace_word(srt_var, "gl_InstanceID");
metadata_.builtins.emplace_back(Builtin(hash("gl_InstanceID")));
create_info_decl += "BUILTINS(BuiltinBits::INSTANCE_ID)\n";
}
else if (srt_attr == "instance_index" && is_entry_point) {
if (!is_vertex_func) {
report_error(attributes[1], "[[instance_index]] is only supported in vertex functions.");
}
else if (!is_const || srt_type != "int") {
report_error(type, "[[instance_index]] must be declared as `const int`.");
}
replace_word(srt_var, "gpu_InstanceIndex");
metadata_.builtins.emplace_back(Builtin(hash("gpu_InstanceIndex")));
create_info_decl += "BUILTINS(BuiltinBits::INSTANCE_ID)\n";
}
else if (srt_attr == "base_instance" && is_entry_point) {
if (!is_vertex_func) {
report_error(attributes[1], "[[base_instance]] is only supported in vertex functions.");
}
else if (!is_const || srt_type != "int") {
report_error(type,
"[[base_instance]] must be declared as "
"`const int`.");
}
replace_word(srt_var, "gpu_BaseInstance");
metadata_.builtins.emplace_back(Builtin(hash("gpu_BaseInstance")));
create_info_decl += "BUILTINS(BuiltinBits::INSTANCE_ID)\n";
}
else if (srt_attr == "point_size" && is_entry_point) {
if (!is_vertex_func) {
report_error(attributes[1], "[[point_size]] is only supported in vertex functions.");
}
else if (is_const || srt_type != "float") {
report_error(type,
"[[point_size]] must be declared as non-const reference (aka `float &`).");
}
replace_word(srt_var, "gl_PointSize");
create_info_decl += "BUILTINS(BuiltinBits::POINT_SIZE)\n";
}
else if (srt_attr == "clip_distance" && is_entry_point) {
if (!is_vertex_func) {
report_error(attributes[1], "[[clip_distance]] is only supported in vertex functions.");
}
else if (is_const || srt_type != "float") {
report_error(type,
"[[clip_distance]] must be declared as non-const reference "
"(aka `float (&)[]`).");
}
replace_word(srt_var, "gl_ClipDistance");
create_info_decl += "BUILTINS(BuiltinBits::CLIP_DISTANCES)\n";
}
else if (srt_attr == "layer" && is_entry_point) {
if (is_compute_func) {
report_error(attributes[1],
"[[layer]] is only supported in vertex and fragment functions.");
}
else if (is_vertex_func && (is_const || srt_type != "int")) {
report_error(type,
"[[layer]] must be declared as non-const reference "
"(aka `int &`).");
}
else if (is_fragment_func && (!is_const || srt_type != "int")) {
report_error(type,
"[[layer]] must be declared as const reference "
"(aka `const int &`).");
}
replace_word(srt_var, "gl_Layer");
create_info_decl += "BUILTINS(BuiltinBits::LAYER)\n";
}
else if (srt_attr == "viewport_index" && is_entry_point) {
if (is_compute_func) {
report_error(attributes[1],
"[[viewport_index]] is only supported in vertex and "
"fragment functions.");
}
else if (is_vertex_func && (is_const || srt_type != "int")) {
report_error(type,
"[[viewport_index]] must be declared as non-const reference "
"(aka `int &`).");
}
else if (is_fragment_func && (!is_const || srt_type != "int")) {
report_error(type,
"[[viewport_index]] must be declared as const reference "
"(aka `const int &`).");
}
replace_word(srt_var, "gpu_ViewportIndex");
create_info_decl += "BUILTINS(BuiltinBits::VIEWPORT_INDEX)\n";
}
else if (srt_attr == "position" && is_entry_point) {
if (!is_vertex_func) {
report_error(attributes[1], "[[position]] is only supported in vertex functions.");
}
else if (is_const || srt_type != "float4") {
report_error(type,
"[[position]] must be declared as non-const reference (aka `float4 &`).");
}
else {
replace_word(srt_var, "gl_Position");
}
}
else if (srt_attr == "frag_coord" && is_entry_point) {
if (!is_fragment_func) {
report_error(attributes[1], "[[frag_coord]] is only supported in fragment functions.");
}
else if (!is_const || srt_type != "float4") {
report_error(type, "[[frag_coord]] must be declared as `const float4`.");
}
else {
create_info_decl += "BUILTINS(BuiltinBits::FRAG_COORD)\n";
replace_word(srt_var, "gl_FragCoord");
}
}
else if (srt_attr == "point_coord" && is_entry_point) {
if (!is_fragment_func) {
report_error(attributes[1], "[[point_coord]] is only supported in fragment functions.");
}
else if (!is_const || srt_type != "float2") {
report_error(type, "[[point_coord]] must be declared as `const float2`.");
}
else {
create_info_decl += "BUILTINS(BuiltinBits::POINT_COORD)\n";
replace_word(srt_var, "gl_PointCoord");
}
}
else if (srt_attr == "front_facing" && is_entry_point) {
if (!is_fragment_func) {
report_error(attributes[1], "[[front_facing]] is only supported in fragment functions.");
}
else if (!is_const || srt_type != "bool") {
report_error(type, "[[front_facing]] must be declared as `const bool`.");
}
else {
create_info_decl += "BUILTINS(BuiltinBits::FRONT_FACING)\n";
replace_word(srt_var, "gl_FrontFacing");
}
}
else if (srt_attr == "global_invocation_id" && is_entry_point) {
if (!is_compute_func) {
report_error(attributes[1],
"[[global_invocation_id]] is only supported in compute functions.");
}
else if (!is_const || srt_type != "uint3") {
report_error(type, "[[global_invocation_id]] must be declared as `const uint3`.");
}
else {
create_info_decl += "BUILTINS(BuiltinBits::GLOBAL_INVOCATION_ID)\n";
replace_word(srt_var, "gl_GlobalInvocationID");
}
}
else if (srt_attr == "local_invocation_id" && is_entry_point) {
if (!is_compute_func) {
report_error(attributes[1],
"[[local_invocation_id]] is only supported in compute functions.");
}
else if (!is_const || srt_type != "uint3") {
report_error(type, "[[local_invocation_id]] must be declared as `const uint3`.");
}
else {
create_info_decl += "BUILTINS(BuiltinBits::LOCAL_INVOCATION_ID)\n";
replace_word(srt_var, "gl_LocalInvocationID");
}
}
else if (srt_attr == "local_invocation_index" && is_entry_point) {
if (!is_compute_func) {
report_error(attributes[1],
"[[local_invocation_index]] is only supported in compute functions.");
}
else if (!is_const || srt_type != "uint") {
report_error(type, "[[local_invocation_index]] must be declared as `const uint`.");
}
else {
create_info_decl += "BUILTINS(BuiltinBits::LOCAL_INVOCATION_INDEX)\n";
replace_word(srt_var, "gl_LocalInvocationIndex");
}
}
else if (srt_attr == "work_group_id" && is_entry_point) {
if (!is_compute_func) {
report_error(attributes[1], "[[work_group_id]] is only supported in compute functions.");
}
else if (!is_const || srt_type != "uint3") {
report_error(type,
"[[work_group_id]] must be declared as "
"`const uint3`.");
}
else {
create_info_decl += "BUILTINS(BuiltinBits::WORK_GROUP_ID)\n";
replace_word(srt_var, "gl_WorkGroupID");
}
}
else if (srt_attr == "num_work_groups" && is_entry_point) {
if (!is_compute_func) {
report_error(attributes[1],
"[[num_work_groups]] is only supported in compute functions.");
}
else if (!is_const || srt_type != "uint3") {
report_error(type,
"[[num_work_groups]] must be declared as "
"`const uint3`.");
}
else {
create_info_decl += "BUILTINS(BuiltinBits::NUM_WORK_GROUP)\n";
replace_word(srt_var, "gl_NumWorkGroups");
}
}
else if (srt_attr == "in") {
if (is_compute_func) {
report_error(attributes[1],
"[[in]] is only supported in vertex and fragment functions.");
}
else if (!is_const) {
report_error(type, "[[in]] must be declared as const reference.");
}
else if (is_vertex_func) {
replace_word_and_accessor(srt_var, "");
create_info_decl += "ADDITIONAL_INFO(" + srt_type + ")\n";
}
else if (is_fragment_func) {
replace_word_and_accessor(srt_var, srt_type + "_");
// create_info_decl += "VERTEX_OUT(" + srt_type + ")\n";
}
}
else if (srt_attr == "subpass_in") {
if (is_compute_func) {
report_error(attributes[1], "[[subpass_in]] is only supported in fragment functions.");
}
else if (!is_const) {
report_error(type, "[[subpass_in]] must be declared as const reference.");
}
else if (is_fragment_func) {
replace_word_and_accessor(srt_var, srt_type + "_");
create_info_decl += "ADDITIONAL_INFO(" + srt_type + ")\n";
}
}
else if (srt_attr == "out") {
if (is_compute_func) {
report_error(attributes[1],
"[[out]] is only supported in vertex and fragment functions.");
}
else if (is_const) {
report_error(type, "[[out]] must be declared as non-const reference.");
}
else if (is_vertex_func) {
replace_word_and_accessor(srt_var, srt_type + "_");
create_info_decl += "VERTEX_OUT(" + srt_type + "_t)\n";
}
else if (is_fragment_func) {
replace_word_and_accessor(srt_var, srt_type + "_");
create_info_decl += "ADDITIONAL_INFO(" + srt_type + ")\n";
}
}
else if (srt_attr == "resource_table") {
if (is_entry_point) {
/* Add dummy var at start of function body. */
parser.insert_after(fn_body.front().str_index_start(),
" " + srt_type + " " + srt_var + "{};");
string res_condition_lambda = parse_condition(attributes);
if (res_condition_lambda.empty()) {
create_info_decl += "ADDITIONAL_INFO(" + srt_type + ")\n";
}
else {
create_info_decl += ".additional_info_with_condition(\"" + srt_type + "\"" +
res_condition_lambda + ")\n";
}
}
}
else if (srt_attr == "frag_depth") {
if (srt_type != "float") {
report_error(type, "[[frag_depth]] needs to be declared as float");
}
const string mode(attributes[3].str());
if (mode != "any" && mode != "greater" && mode != "less") {
report_error(attributes[3], "unrecognized mode, expecting 'any', 'greater' or 'less'");
}
else {
create_info_decl += "DEPTH_WRITE(DepthWrite::" + to_uppercase(mode) + ")\n";
replace_word(srt_var, "gl_FragDepth");
}
}
else if (srt_attr == "frag_stencil_ref") {
if (srt_type != "int") {
report_error(type, "[[frag_stencil_ref]] needs to be declared as int");
}
else {
create_info_decl += "BUILTINS(BuiltinBits::STENCIL_REF)\n";
replace_word(srt_var, "gl_FragStencilRefARB");
}
}
else {
report_error(attributes[1], "Invalid attribute.");
}
};
args.foreach_match("[[..]]c?AA", [&](const vector<Token> toks) {
process_argument(toks[8], toks[9], toks[1].scope());
});
args.foreach_match("[[..]]c?A&A", [&](const vector<Token> toks) {
process_argument(toks[8], toks[10], toks[1].scope());
});
args.foreach_match("[[..]]c?A(&A)", [&](const vector<Token> toks) {
process_argument(toks[8], toks[11], toks[1].scope());
});
if (is_entry_point) {
if (create_info_decl.empty()) {
/* Add unused define to avoid warning about unused expression. */
create_info_decl += "DEFINE(\"EMPTY_CREATE_INFO\")\n";
}
create_info_decl = "GPU_SHADER_CREATE_INFO(" + string(fn_name.str()) + "_infos_)\n" +
create_info_decl + "GPU_SHADER_CREATE_END()\n";
metadata_.create_infos_declarations.emplace_back(create_info_decl);
}
});
parser.apply_mutations();
}
/* Removes entry point arguments to make it compatible with the legacy code.
* Has to run after mutation related to function arguments. */
void SourceProcessor::lower_entry_points_signature(Parser &parser)
{
using namespace metadata;
parser().foreach_function([&](bool, Token type, Token name, Scope args, bool, Scope fn_body) {
bool is_entry_point = false;
if (type.prev() == ']' && type.prev().scope().type() == ScopeType::Subscript) {
Scope attributes = type.prev().prev().scope();
attributes.foreach_attribute([&](Token attr, Scope) {
const string attr_str(attr.str());
if (attr_str == "vertex" || attr_str == "fragment" || attr_str == "compute") {
is_entry_point = true;
}
});
}
if (is_entry_point && args.str() != "()") {
parser.erase(args.front().next(), args.back().prev());
}
/* Mute entry points when not enabled.
* Could be lifted at some point, but for now required because of stage_in/out parameters. */
if (is_entry_point) {
/* Take attributes into account. */
parser.insert_directive(type.prev().scope().front().prev(),
"#if defined(ENTRY_POINT_" + string(name.str()) + ")");
parser.insert_directive(fn_body.back(), "#endif");
}
});
parser.apply_mutations();
}
} // namespace blender::gpu::shader

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,467 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup shader_tool
*
*/
#include "intermediate.hh"
#include "scope.hh"
#include "token.hh"
#include "token_stream.hh"
#include "lexit/lexit.hh"
#include "lexit/tables.hh"
#if defined(_MSC_VER)
# include <malloc.h>
#endif
#include <algorithm>
#include <array>
#include <cstdlib>
#include <cstring>
#if defined(_MSC_VER)
# define always_inline __forceinline
#else
# define always_inline inline __attribute__((always_inline))
#endif
namespace blender::gpu::shader::parser {
size_t line_number(const std::string_view &str, size_t pos)
{
std::string_view directive = "#line ";
/* String to count the number of line. */
std::string_view sub_str = str.substr(0, pos);
size_t nearest_line_directive = sub_str.rfind(directive);
size_t line_count = 1;
if (nearest_line_directive != std::string::npos) {
sub_str = sub_str.substr(nearest_line_directive + directive.size());
line_count = std::stoll(std::string(sub_str)) - 1;
}
return line_count + std::count(sub_str.begin(), sub_str.end(), '\n');
}
size_t char_number(const std::string_view &str, size_t pos)
{
std::string_view sub_str = str.substr(0, pos);
size_t nearest_line_directive = sub_str.rfind('\n');
return (nearest_line_directive == std::string::npos) ?
(sub_str.size()) :
(sub_str.size() - nearest_line_directive - 1);
}
std::string filename(const std::string_view &str, size_t pos)
{
std::string_view directive = "#line ";
std::string_view sub_str = str.substr(0, pos);
while (!sub_str.empty()) {
size_t nearest_line_directive = sub_str.rfind(directive);
/* If no directive is found, break and return an empty string. */
if (nearest_line_directive == std::string_view::npos) {
break;
}
/* Extract just the line containing the directive. */
size_t line_end = sub_str.find('\n', nearest_line_directive);
std::string_view directive_line = sub_str.substr(nearest_line_directive,
line_end - nearest_line_directive);
/* Look for the quotes containing the filepath. */
size_t first_quote = directive_line.find('"');
if (first_quote != std::string_view::npos) {
size_t second_quote = directive_line.find('"', first_quote + 1);
if (second_quote != std::string_view::npos) {
return std::string(directive_line.substr(first_quote + 1, second_quote - first_quote - 1));
}
}
/* If this directive didn't have a filename, shrink the search space to look further up. */
if (nearest_line_directive == 0) {
break;
}
sub_str = sub_str.substr(0, nearest_line_directive);
}
return "";
}
std::string line_str(const std::string_view &str, size_t pos)
{
size_t start = str.rfind('\n', pos);
size_t end = str.find('\n', pos);
start = (start != std::string::npos) ? start + 1 : 0;
return std::string(str.substr(start, end - start));
}
Scope Token::scope() const
{
const ParserBase &parser = static_cast<const ParserBase &>(*buf_);
if (this->is_invalid()) {
return Scope(parser, -1);
}
return Scope(parser, parser.token_scope[index_]);
}
Scope Token::attribute_before() const
{
const ParserBase &parser = static_cast<const ParserBase &>(*buf_);
if (is_invalid()) {
return Scope(parser, -1);
}
Token prev = this->prev();
if (prev == ']' && prev.prev().scope().type() == ScopeType::Attributes) {
return prev.prev().scope();
}
return Scope(parser, -1);
}
Scope Token::attribute_after() const
{
const ParserBase &parser = static_cast<const ParserBase &>(*buf_);
if (is_invalid()) {
return Scope(parser, -1);
}
Token next = this->next();
if (next == '[' && next.next().scope().type() == ScopeType::Attributes) {
return next.next().scope();
}
return Scope(parser, -1);
}
void ErrorHandler::report(Token tok, std::string_view message)
{
/* Only log the first error. */
if (err) {
return;
}
std::string token_filename = tok.filename();
std::string full_report = token_filename.empty() ? default_filename : token_filename;
full_report += ":" + std::to_string(tok.line_number());
full_report += ":" + std::to_string(tok.char_number() + 1);
full_report += ": " + std::string(message);
if (tok.is_valid()) {
full_report += "\n";
full_report += tok.line_str() + "\n";
if (tok.str().size() > 0) {
full_report += std::string(tok.char_number(), ' ') + "^" +
std::string(tok.str().size() - 1, '~');
}
}
err = {std::string(message), full_report};
}
void ErrorHandler::report(int row, int column, std::string line, std::string_view message)
{
/* Only log the first error. */
if (err) {
return;
}
std::string full_report = default_filename;
full_report += ":" + std::to_string(row);
full_report += ":" + std::to_string(column + 1);
full_report += ": " + std::string(message) + "\n";
full_report += line;
err = {std::string(message), full_report};
}
alignas(128) const std::array<CharClass, 128> LexerBase::default_char_class_table = [] {
std::array<CharClass, 128> table;
memcpy(table.data(), lexit::char_class_table, sizeof(lexit::char_class_table));
return table;
}();
/* Same thing as default table but consider numbers as words to avoid second merging pass. */
alignas(128) const std::array<CharClass, 128> LexerBase::bsl_char_class_table = [] {
std::array<CharClass, 128> table;
memcpy(table.data(), lexit::char_class_table, sizeof(lexit::char_class_table));
table['\n'] = CharClass::WhiteSpace;
/* Make < and > separators in order to support template.
* That means >= and <= need to be manually handled. */
table['<'] = CharClass::Separator;
table['>'] = CharClass::Separator;
return table;
}();
static always_inline TokenType multi_tok_lookup(TokenType input, std::string_view s)
{
switch (s.size()) {
case 2:
switch (s[0]) {
case '=':
return (s[1] == '=') ? Equal : input;
case '!':
return (s[1] == '=') ? NotEqual : input;
case '|':
return (s[1] == '|') ? LogicalOr : input;
case '&':
return (s[1] == '&') ? LogicalAnd : input;
case '<':
return (s[1] == '=') ? LEqual : input;
case '>':
return (s[1] == '=') ? GEqual : input;
case '+':
return (s[1] == '+') ? Increment : input;
case '-':
return (s[1] == '-') ? Decrement : input;
case '#':
return (s[1] == '#') ? DoubleHash : input;
default:
return input;
}
default:
return input;
}
}
constexpr always_inline uint8_t perfect_hash(std::string_view s)
{
return s.size() * (s[0] - s.back() * 2);
}
static always_inline TokenType type_lookup(std::string_view s)
{
switch (perfect_hash(s)) {
case perfect_hash("do"):
return (s == "do") ? Do : Word;
case perfect_hash("if"):
return (s == "if") ? If : Word;
case perfect_hash("for"):
return (s == "for") ? For : Word;
case perfect_hash("case"):
return (s == "case") ? Case : Word;
case perfect_hash("else"):
return (s == "else") ? Else : Word;
case perfect_hash("enum"):
return (s == "enum") ? Enum : Word;
case perfect_hash("this"):
return (s == "this") ? This : Word;
case perfect_hash("break"):
return (s == "break") ? Break : Word;
case perfect_hash("class"):
return (s == "class") ? Class : Word;
case perfect_hash("const"):
return (s == "const") ? Const : Word;
case perfect_hash("union"):
return (s == "union") ? Union : Word;
case perfect_hash("using"):
return (s == "using") ? Using : Word;
case perfect_hash("while"):
return (s == "while") ? While : Word;
case perfect_hash("inline"):
return (s == "inline") ? Inline : Word;
case perfect_hash("public"):
return (s == "public") ? Public : Word;
case perfect_hash("return"):
return (s == "return") ? Return : Word;
case perfect_hash("static"):
return (s == "static") ? Static : Word;
case perfect_hash("struct"):
return (s == "struct") ? Struct : Word;
case perfect_hash("switch"):
return (s == "switch") ? Switch : Word;
case perfect_hash("private"):
return (s == "private") ? Private : Word;
case perfect_hash("continue"):
return (s == "continue") ? Continue : Word;
case perfect_hash("template"):
return (s == "template") ? Template : Word;
case perfect_hash("constexpr"):
return (s == "constexpr") ? Constexpr : Word;
case perfect_hash("namespace"):
return (s == "namespace") ? Namespace : Word;
default:
return Word;
}
}
void LexerBase::identify_keywords()
{
for (auto tok : *this) {
switch (tok.type()) {
case Word:
tok.type() = type_lookup(tok.str());
break;
case Number:
break;
default:
tok.type() = multi_tok_lookup(tok.type(), tok.str());
break;
}
}
}
void LexerBase::identify_template_tokens()
{
for (int i = 1; i < size(); ++i) {
TokenMut tok = (*this)[i];
TokenType type = tok.type();
if (type == '<' || type == '>') {
Token prev = (*this)[i - 1];
const bool preceded_by_space = prev.followed_by_whitespace();
/* Rely on the fact that template are formatted without spaces but comparison isn't. */
if (type == '<') {
if ((!preceded_by_space && prev != AngleOpen) || prev == Template) {
tok.type() = TemplateOpen;
}
}
else {
if (!preceded_by_space && (prev != AngleClose) && (prev != Minus)) {
tok.type() = TemplateClose;
}
}
}
}
}
void LexerBase::reset_template_tokens()
{
for (int i = 1; i < size(); ++i) {
TokenMut tok = (*this)[i];
switch (tok.type()) {
case TemplateOpen:
tok.type() = lexit::AngleOpen;
break;
case TemplateClose:
tok.type() = lexit::AngleClose;
break;
default:
break;
}
}
}
struct ScopeStack {
struct Item {
ScopeType type;
size_t start;
int index;
};
int scope_index = 0;
std::vector<ScopeStack::Item> scopes;
/* Output. */
std::vector<IndexRange> ranges;
std::vector<ScopeType> types;
ScopeStack(size_t predicted_scope_count)
{
/* Predicted max nesting depth. */
scopes.reserve(128);
ranges.reserve(predicted_scope_count);
types.reserve(predicted_scope_count);
}
void always_inline enter_scope(ScopeType type, size_t start_tok_id)
{
scopes.emplace_back(Item{type, start_tok_id, scope_index++});
ranges.emplace_back(start_tok_id, 1);
types.emplace_back(type);
};
void always_inline exit_scope(int end_tok_id)
{
if (scopes.empty()) {
return;
}
Item scope = scopes.back();
ranges[scope.index].size = end_tok_id - scope.start + 1;
scopes.pop_back();
};
Item always_inline back() const
{
return scopes.back();
}
bool always_inline empty() const
{
return scopes.empty();
}
};
void ParserBase::build_token_to_scope_map()
{
token_scope.clear();
token_scope.resize(scope_ranges[0].size);
int scope_id = 0;
for (const IndexRange &range : scope_ranges) {
std::fill(token_scope.begin() + range.start,
token_scope.begin() + range.start + range.size,
scope_id);
scope_id++;
}
update_string_view();
}
Token ParserBase::operator[](int i) const
{
return Token(*this, i);
}
void ParserBase::update_string_view()
{
assert(this->scope_types.data() != nullptr);
assert(this->scope_types.size() > 0);
this->scope_types_str = std::string_view(reinterpret_cast<char *>(this->scope_types.data()),
this->scope_types.size());
}
bool MutableString::apply_mutations(LexerBase &lexer, const bool all_mutation_ordered)
{
if (mutations_.empty()) {
return false;
}
if (!all_mutation_ordered) {
/* Order mutations so that they can be applied in one pass. */
std::stable_sort(mutations_.begin(), mutations_.end());
}
#ifndef NDEBUG
else {
assert(std::is_sorted(mutations_.begin(), mutations_.end()));
}
#endif
/* Make sure to pad the input string in case of insertion after the last char. */
bool added_trailing_new_line = false;
if (str_.back() != '\n') {
str_ += '\n';
added_trailing_new_line = true;
}
std::string result;
result.reserve(str_.size());
int64_t offset = 0;
for (const Mutation &mut : mutations_) {
size_t start = mut.src_range.start;
size_t end = start + mut.src_range.size;
/* Copy unchanged text. */
result.append(str_.data() + offset, start - offset);
/* Append replacement. */
result.append(mut.replacement);
offset = end;
}
result.append(str_.data() + offset, str_.size() - offset);
str_ = std::move(result);
mutations_.clear();
if (added_trailing_new_line) {
str_.pop_back();
}
/* String have changed. Update string view. */
lexer.str_ = str_;
return true;
}
} // namespace blender::gpu::shader::parser

View File

@@ -0,0 +1,407 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup shader_tool
*
* Very simple parsing of our shader file that are a subset of C++. It allows to traverse the
* semantic using tokens and scopes instead of trying to match string patterns throughout the whole
* input string.
*
* The goal of this representation is to output code that doesn't modify the style of the input
* string and keep the same line numbers (to match compilation error with input source).
*
* The `TokenStream` class contain a copy of the given string to apply string substitutions (called
* `Mutation`). It is usually faster to record all of them and apply them all at once after
* scanning through the whole semantic representation. In the rare case where mutation need to
* overlap (recursive processing), it is better to do them in passes until there is no mutation to
* do.
*
* `Token` and `Scope` are read only interfaces to the data stored inside the `TokenStream`.
* The data is stored as SoA (Structure of Arrays) for fast traversal.
* The types of token and scopes are defined as readable chars to easily create sequences of token
* type.
*
* The parsing phase doesn't apply any preprocessor. All preprocessor directive are parsed as
* `Preprocessor` scope but they are not expanded.
*/
#pragma once
#include "scope.hh"
#include "token.hh"
#include "token_stream.hh"
#include "utils.hh"
#include <algorithm>
#include <cassert>
#include <iostream>
namespace blender::gpu::shader::parser {
struct MutableString {
std::string str_;
struct Mutation {
/* Range of the original string to replace. */
IndexRange src_range;
/* The replacement string. */
std::string replacement;
Mutation(IndexRange src_range, std::string replacement)
: src_range(src_range), replacement(replacement)
{
assert(src_range.size >= 0);
assert(src_range.start >= 0);
}
/* Define operator in order to sort the mutation by starting position.
* Otherwise, applying them in one pass will not work. */
friend bool operator<(const Mutation &a, const Mutation &b)
{
return a.src_range.start < b.src_range.start;
}
};
std::vector<Mutation> mutations_;
MutableString(const std::string_view input) : str_(input) {}
/* Disable copy construction and assignment. */
MutableString(const MutableString &other) = delete;
MutableString &operator=(const MutableString &other) = delete;
/* Explicitly enable default construction, move construction and move assignment. */
MutableString(MutableString &&other) = default;
MutableString &operator=(MutableString &&other) = default;
/* Access internal string without applying pending mutations. */
std::string substr_range_inclusive(size_t start, size_t end)
{
return str_.substr(start, end - start + 1);
}
/* Access internal string without applying pending mutations. */
std::string substr_range_inclusive(Token start, Token end)
{
return substr_range_inclusive(start.str_index_start(), end.str_index_last());
}
/* Access internal string without applying pending mutations. */
std::string_view substr_range_inclusive_view(size_t start, size_t end)
{
return std::string_view(str_).substr(start, end - start + 1);
}
/* Access internal string without applying pending mutations. */
std::string_view substr_range_inclusive_view(Token start, Token end)
{
return substr_range_inclusive_view(start.str_index_start(), end.str_index_last());
}
/* Replace everything from `from` to `to` (inclusive).
* Return true on success. */
bool replace_try(size_t from, size_t to, const std::string &replacement)
{
IndexRange range = IndexRange(from, to + 1 - from);
for (const Mutation &mut : mutations_) {
if (mut.src_range.overlaps(range)) {
return false;
}
}
mutations_.emplace_back(range, replacement);
return true;
}
/* Replace everything from `from` to `to` (inclusive).
* Return true on success. */
bool replace_try(Token from,
Token to,
const std::string &replacement,
bool keep_trailing_whitespaces = false)
{
if (keep_trailing_whitespaces) {
return replace_try(from.str_index_start(), to.str_index_last_no_whitespace(), replacement);
}
return replace_try(from.str_index_start(), to.str_index_last(), replacement);
}
/* Replace everything from `from` to `to` (inclusive). */
void replace(size_t from, size_t to, const std::string &replacement)
{
#ifndef NDEBUG
bool success = replace_try(from, to, replacement);
assert(success);
(void)success;
#else
/* No check in release. */
IndexRange range = IndexRange(from, to + 1 - from);
mutations_.emplace_back(range, replacement);
#endif
}
/* Replace everything from `from` to `to` (inclusive). */
void replace(Token from,
Token to,
const std::string &replacement,
bool keep_trailing_whitespaces = false)
{
if (keep_trailing_whitespaces) {
replace(from.str_index_start(), to.str_index_last_no_whitespace(), replacement);
}
else {
replace(from.str_index_start(), to.str_index_last(), replacement);
}
}
/* Replace token by string. */
void replace(Token tok, const std::string &replacement, bool keep_trailing_whitespaces = false)
{
if (keep_trailing_whitespaces) {
replace(tok.str_index_start(), tok.str_index_last_no_whitespace(), replacement);
}
else {
replace(tok.str_index_start(), tok.str_index_last(), replacement);
}
}
/* Replace Scope by string. */
void replace(Scope scope, const std::string &replacement, bool keep_trailing_whitespaces = false)
{
if (keep_trailing_whitespaces) {
replace(scope.front().str_index_start(),
scope.back().str_index_last_no_whitespace(),
replacement);
}
else {
replace(scope.front(), scope.back(), replacement);
}
}
/* Replace the content from `from` to `to` (inclusive) by whitespaces without changing
* line count and keep the remaining indentation spaces. */
bool erase_try(size_t from, size_t to)
{
IndexRange range = IndexRange(from, to + 1 - from);
std::string content = str_.substr(range.start, range.size);
size_t lines = std::count(content.begin(), content.end(), '\n');
size_t spaces = content.find_last_of("\n");
if (spaces != std::string::npos) {
spaces = content.length() - (spaces + 1);
}
else {
spaces = content.length();
}
return replace_try(from, to, std::string(lines, '\n') + std::string(spaces, ' '));
}
void erase(size_t from, size_t to)
{
bool result = erase_try(from, to);
assert(result);
(void)result;
}
/* Replace the content from `from` to `to` (inclusive) by whitespaces without changing
* line count and keep the remaining indentation spaces. */
bool erase_try(Token from, Token to)
{
if (from.is_invalid() || to.is_invalid()) {
return true;
}
assert(from.index_ <= to.index_);
return erase_try(from.str_index_start(), to.str_index_last());
}
void erase(Token from, Token to)
{
bool result = erase_try(from, to);
assert(result);
(void)result;
}
/* Replace the content from `from` to `to` (inclusive) by whitespaces without changing
* line count and keep the remaining indentation spaces. */
bool erase_try(Token tok)
{
if (tok.is_invalid()) {
return true;
}
return erase_try(tok, tok);
}
void erase(Token tok)
{
bool result = erase_try(tok);
assert(result);
(void)result;
}
/* Replace the content of the scope by whitespaces without changing
* line count and keep the remaining indentation spaces. */
bool erase_try(Scope scope)
{
return erase_try(scope.front(), scope.back());
}
void erase(Scope scope)
{
bool result = erase_try(scope);
assert(result);
(void)result;
}
/* If prepend is true, will prepend the new content to the list of modifications.
* With this enabled, in case of overlapping mutation, the last one added will be first. */
void insert_before(size_t at, const std::string &content, bool prepend = false)
{
IndexRange range = IndexRange(at, 0);
if (prepend) {
mutations_.insert(mutations_.begin(), {range, content});
}
else {
mutations_.emplace_back(range, content);
}
}
void insert_before(Token at, const std::string &content, bool prepend = false)
{
insert_before(at.str_index_start(), content, prepend);
}
void insert_after(size_t at, const std::string &content)
{
IndexRange range = IndexRange(at + 1, 0);
mutations_.emplace_back(range, content);
}
void insert_after(Token at, const std::string &content)
{
insert_after(at.str_index_last(), content);
}
void insert_line_number(size_t at, int line, std::string_view filename = "")
{
std::string str = "\n#line " + std::to_string(line);
if (!filename.empty()) {
str = str + " \"" + std::string(filename) + "\"";
}
insert_after(at, str + "\n");
}
void insert_line_number(Token at, int line, std::string_view filename = "")
{
insert_line_number(at.str_index_last(), line, filename);
}
/* Insert a preprocessor directive after the given token.
* This also insert a line directive to keep correct error reporting. */
void insert_directive(Token at, const std::string directive)
{
insert_after(at, "\n" + directive + "\n");
insert_line_number(at, at.line_number(true));
size_t line_break = str_.find_last_of("\n", at.str_index_last() + 1);
size_t spaces = at.str_index_last() - line_break;
insert_after(at, std::string(spaces, ' '));
}
/* Return true if any mutation was applied.
* Update lexer string view if needed. */
bool apply_mutations(LexerBase &lexer, const bool all_mutation_ordered = false);
/* Get internal string. Does not apply pending mutation. */
const std::string &str()
{
return str_;
}
/* For testing. */
std::string serialize_mutations() const
{
std::string out;
for (const Mutation &mut : mutations_) {
out += "Replace ";
out += std::to_string(mut.src_range.start);
out += " - ";
out += std::to_string(mut.src_range.size);
out += " \"";
out += str_.substr(mut.src_range.start, mut.src_range.size);
out += "\" by \"";
out += mut.replacement;
out += "\"\n";
}
return out;
}
};
inline std::ostream &operator<<(std::ostream &out, const std::vector<int> &v)
{
if (!v.empty()) {
out << '[';
for (auto val : v) {
out << val << ',';
}
out << "\b]";
}
return out;
}
/* Structure holding an intermediate form of the source code.
* It is made for fast traversal and mutation of source code. */
template<typename LexerFn, typename ParserFn>
struct IntermediateForm : MutableString, Parser<LexerFn, ParserFn> {
protected:
ErrorHandler &report_error;
public:
IntermediateForm(const std::string_view input, ErrorHandler &report_error)
: MutableString(input), report_error(report_error)
{
parse(report_error);
}
/* Main access operator. Returns the root scope (aka global scope). */
Scope operator()() const
{
if (this->scope_types.empty()) {
return Scope(*this);
}
return Scope(*this, 0);
}
/* Return true if any mutation was applied. */
bool only_apply_mutations(const bool all_mutation_ordered = false)
{
return static_cast<MutableString *>(this)->apply_mutations(*this, all_mutation_ordered);
}
/* Apply pending mutation and parse the resulting string.
* Return true if any mutation was applied. */
bool apply_mutations(const bool all_mutation_ordered = false)
{
bool applied = only_apply_mutations(all_mutation_ordered);
if (applied) {
parse(report_error);
}
return applied;
}
/* Apply mutations if any and get resulting string. */
const std::string &result_get(const bool all_mutation_ordered = false)
{
only_apply_mutations(all_mutation_ordered);
return str_;
}
void parse(ErrorHandler &report_error)
{
this->lexical_analysis(str_);
this->semantic_analysis(report_error);
}
void debug_print()
{
std::cout << "Input: \n" << str_ << " \nEnd of Input\n" << std::endl;
std::cout << "Token Types: \"" << this->token_types_str() << "\"" << std::endl;
std::cout << "Token scopes: \"" << this->token_scope << "\"" << std::endl;
std::cout << "Scope Types: \"" << this->scope_types_str << "\"" << std::endl;
}
void debug_print_tokens()
{
for (auto tok : *this) {
std::cout << "id:" << int(tok) << " start:" << this->offsets_[int(tok)]
<< " end:" << this->offsets_end_[int(tok)] << " type:" << tok.type()
<< " scope:" << this->token_scope[int(tok)] << "("
<< this->scope_types_str[this->token_scope[int(tok)]] << ")"
<< " atom:" << tok.atom() << " str:\"" << tok.str() << "\""
<< " followed_by_whitespace:" << tok.followed_by_whitespace() << "\n";
}
}
};
} // namespace blender::gpu::shader::parser

View File

@@ -0,0 +1,219 @@
/* SPDX-FileCopyrightText: 2026 Clement Foucault
*
* SPDX-License-Identifier: MIT */
#pragma once
#include <algorithm>
#include <array>
#include <cstdint>
#include <string_view>
#include <vector>
#include "types.hh"
#include "vector.hh"
#if defined(_MSC_VER)
# define INLINE_METHOD __forceinline
#else
# define INLINE_METHOD inline __attribute__((always_inline))
#endif
namespace lexit {
static constexpr uint64_t padded_string_masks[8] = {
uint64_t(0xFFFFFFFFFFFFFFFF),
uint64_t(0x00000000000000FF),
uint64_t(0x000000000000FFFF),
uint64_t(0x0000000000FFFFFF),
uint64_t(0x00000000FFFFFFFF),
uint64_t(0x000000FFFFFFFFFF),
uint64_t(0x0000FFFFFFFFFFFF),
uint64_t(0x00FFFFFFFFFFFFFF),
};
/**
* Copy of a small string onto aligned bytes.
* This avoids the cost of calling memcmp during comparison.
* For performance reason, this needs to be constructed on string whose size falls into
* the ((Size-1) * 8, Size * 8] range.
* IMPORTANT: The string view given to the constructor *MUST* point to at least Size * 8
* addressable byte region regardless of the string size.
*/
template<int Size> struct PaddedString {
uint64_t data[Size];
uint32_t size;
PaddedString() = default;
/* Caller need to ensure enough bytes are accessible after the end of string. */
explicit PaddedString(std::string_view str)
{
size = str.size();
assert(str.size() > 8 * (Size - 1));
assert(str.size() <= 8 * Size);
std::memcpy(data, (const char *)str.data(), sizeof(data));
/* Fast way of masking the excess chars. */
data[Size - 1] &= padded_string_masks[str.size() & (sizeof(data[0]) - 1)];
}
operator std::string_view() const
{
return {(const char *)data, size};
}
};
struct Keyword {
std::string_view str;
TokenType type;
TokenAtom atom;
Keyword() = default;
Keyword(std::string_view str, TokenType type, TokenAtom atom) : str(str), type(type), atom(atom)
{
}
};
struct IdentifierMap {
struct alignas(8) Identifier {
uint16_t next;
uint16_t size;
uint32_t hash;
/** WORKAROUND: MSVC does not support trailing null size array. */
uint64_t *data() const
{
return (uint64_t *)(&hash + 1);
}
/* Caller must ensure size matches. */
template<int Size> bool operator==(const PaddedString<Size> &str) const
{
if (size != str.size) {
return false;
}
return std::equal(data(), data() + Size, str.data);
}
bool operator==(std::string_view str) const
{
return std::string_view{(const char *)data(), size} == str;
}
};
lexit::Vector<Identifier> identifier_buffer;
/* Note: Must be power of two size. */
static constexpr uint32_t hash_table_size = 16384;
static constexpr uint32_t hash_table_index_mask = (hash_table_size - 1);
std::array<uint16_t, hash_table_size> hash_table;
IdentifierMap()
{
/* Set invalid values for all the table. */
std::memset(hash_table.data(), 0xFFu, sizeof(uint16_t) * hash_table.size());
}
void reserve(int token_count)
{
identifier_buffer.reserve(token_count);
}
/* Return the maximum value for the currently allocated atoms. */
TokenAtom max_atom_value() const
{
return identifier_buffer.size();
}
static constexpr uint32_t str_hash(std::string_view s)
{
uint32_t hash = 5381;
hash = ((hash << 5) + hash) + s.size();
hash = ((hash << 5) + hash) + static_cast<uint8_t>(s[0]);
hash = ((hash << 5) + hash) + static_cast<uint8_t>(s[s.size() / 2]);
hash = ((hash << 5) + hash) + static_cast<uint8_t>(s.back());
return static_cast<uint16_t>(hash);
}
template<typename StringT> INLINE_METHOD TokenAtom lookup_or_add(StringT str)
{
uint32_t hash = str_hash(str);
uint16_t index = hash_table[hash & hash_table_index_mask];
Identifier *id = nullptr;
for (; index != 0xFFFFu; index = id->next) {
id = &identifier_buffer[index];
if (id->hash == hash && *id == str) [[likely]] {
return index;
}
}
return add_after(hash, str, id);
}
TokenAtom add_after(uint32_t hash, std::string_view str, Identifier *id = nullptr)
{
/* Cache miss. Add new. */
uint16_t new_index = identifier_buffer.size();
if (id) {
/* Update previous element in the list. */
id->next = new_index;
}
else {
/* Update entry in table. */
hash_table[hash & hash_table_index_mask] = new_index;
}
{
/* Fast malloc replacement. */
int str_as_id_size = ((str.size() + (sizeof(Identifier) - 1)) / sizeof(Identifier));
identifier_buffer.reserve(new_index + 1 + str_as_id_size);
Identifier &id = *identifier_buffer.end();
identifier_buffer.increase_size_by_unchecked(1 + str_as_id_size);
/* Construct new identifier. */
id.next = 0xFFFFu;
id.size = str.size();
id.hash = hash;
/* Zero the end of the memcpy for the fast comparison. */
id.data()[((str.size() - 1) / sizeof(Identifier))] = 0;
std::memcpy(id.data(), str.data(), str.size());
}
return new_index;
}
Keyword make_keyword(std::string_view str, TokenType type)
{
return Keyword(str, type, lookup_or_add(str));
}
};
/* Convert Atom to Keyword types. */
struct KeywordTable {
/* Indexed by string TokenAtom. */
alignas(64) std::array<TokenType, 64> map;
KeywordTable(const std::vector<Keyword> &vector)
{
/* We only lookup words, so a word mismatch should be a Noop. */
map.fill(Word);
for (auto keyword : vector) {
/* Check for overflow. */
assert(keyword.atom < 128);
/* Check default case not being overwritten. */
assert(keyword.atom != 0);
map[keyword.atom / 2] = keyword.type;
}
}
TokenType operator[](TokenAtom atom) const
{
/* Identifier are always allocated in 2 or more consecutive slots. Which means TokenAtom values
* always increment by at least 2. Avoid wasting slots in the map by dividing the atom by 2.
* If atom is bigger than the table, revert to 0 atom (invalid) which becomes a Noop by
* returning Word. */
return map[(atom / 2) * (atom < 128)];
}
};
} // namespace lexit

View File

@@ -0,0 +1,965 @@
/* SPDX-FileCopyrightText: 2026 Clement Foucault
*
* SPDX-License-Identifier: MIT */
#include "lexit.hh"
#include "identifier.hh"
#include "simd.hh"
#include <algorithm>
#include <cassert>
#include <cstring>
#ifdef _MSC_VER
# include <intrin.h>
#endif
#if defined(__clang__) || defined(__GNUC__)
# define count_bits_i(i) __builtin_popcount(i)
#elif defined(_MSC_VER)
# define count_bits_i(i) __popcnt(i)
#else
# include <bitset>
# define count_bits_i(i) (std::bitset<8>{i}.count())
#endif
namespace lexit {
static int builtin_ctzll(uint64_t a)
{
#ifdef _MSC_VER
unsigned long ctz;
_BitScanForward64(&ctz, a);
return ctz;
#else
return __builtin_ctzll(a);
#endif
} // namespace lexit
static uint32_t divide_ceil(uint32_t a, uint32_t b)
{
return (a + b - 1) / b;
}
/* Helper function to realloc aligned array keeping elem_count data. */
template<typename T>
void realloc_aligned_array(AlignedArrayPtr<T> &ptr, size_t elem_count, size_t new_size)
{
assert(new_size >= elem_count);
AlignedArrayPtr<T> new_ptr(new_size);
if (ptr.get()) {
std::memcpy(new_ptr.get(), ptr.get(), elem_count * sizeof(T));
}
ptr = std::move(new_ptr);
}
void TokenBuffer::clear()
{
size_ = 0;
}
void TokenBuffer::reserve(const uint32_t count)
{
if (allocated_size_ >= count + 1) {
return;
}
allocated_size_ = count + 1;
realloc_aligned_array(types_, size_ + 1, allocated_size_);
realloc_aligned_array(offsets_, size_ + 1, allocated_size_);
realloc_aligned_array(offsets_end_, size_ + 1, allocated_size_);
realloc_aligned_array(atoms_, size_ + 1, allocated_size_);
realloc_aligned_array(lengths_, size_ + 1, allocated_size_);
}
#if defined(USE_NEON) || defined(USE_SSE4_2)
/* Shuffle table used for stream compaction.
* For a given 8bit pattern (where each 1 bit represents an element to keep)
* encode the index of the source register for each of the 8 destination registers.
* Every 0 bit (representing a discarded element) will be sourced from the 0th element.
* This is to be used with table. */
alignas(16) static const uint8_t shuffle_table_8[256][8] = {
/* [0b00000000] = */ {0, 0, 0, 0, 0, 0, 0, 0},
/* [0b00000001] = */ {0, 0, 0, 0, 0, 0, 0, 0},
/* [0b00000010] = */ {1, 0, 0, 0, 0, 0, 0, 0},
/* [0b00000011] = */ {0, 1, 0, 0, 0, 0, 0, 0},
/* [0b00000100] = */ {2, 0, 0, 0, 0, 0, 0, 0},
/* [0b00000101] = */ {0, 2, 0, 0, 0, 0, 0, 0},
/* [0b00000110] = */ {1, 2, 0, 0, 0, 0, 0, 0},
/* [0b00000111] = */ {0, 1, 2, 0, 0, 0, 0, 0},
/* [0b00001000] = */ {3, 0, 0, 0, 0, 0, 0, 0},
/* [0b00001001] = */ {0, 3, 0, 0, 0, 0, 0, 0},
/* [0b00001010] = */ {1, 3, 0, 0, 0, 0, 0, 0},
/* [0b00001011] = */ {0, 1, 3, 0, 0, 0, 0, 0},
/* [0b00001100] = */ {2, 3, 0, 0, 0, 0, 0, 0},
/* [0b00001101] = */ {0, 2, 3, 0, 0, 0, 0, 0},
/* [0b00001110] = */ {1, 2, 3, 0, 0, 0, 0, 0},
/* [0b00001111] = */ {0, 1, 2, 3, 0, 0, 0, 0},
/* [0b00010000] = */ {4, 0, 0, 0, 0, 0, 0, 0},
/* [0b00010001] = */ {0, 4, 0, 0, 0, 0, 0, 0},
/* [0b00010010] = */ {1, 4, 0, 0, 0, 0, 0, 0},
/* [0b00010011] = */ {0, 1, 4, 0, 0, 0, 0, 0},
/* [0b00010100] = */ {2, 4, 0, 0, 0, 0, 0, 0},
/* [0b00010101] = */ {0, 2, 4, 0, 0, 0, 0, 0},
/* [0b00010110] = */ {1, 2, 4, 0, 0, 0, 0, 0},
/* [0b00010111] = */ {0, 1, 2, 4, 0, 0, 0, 0},
/* [0b00011000] = */ {3, 4, 0, 0, 0, 0, 0, 0},
/* [0b00011001] = */ {0, 3, 4, 0, 0, 0, 0, 0},
/* [0b00011010] = */ {1, 3, 4, 0, 0, 0, 0, 0},
/* [0b00011011] = */ {0, 1, 3, 4, 0, 0, 0, 0},
/* [0b00011100] = */ {2, 3, 4, 0, 0, 0, 0, 0},
/* [0b00011101] = */ {0, 2, 3, 4, 0, 0, 0, 0},
/* [0b00011110] = */ {1, 2, 3, 4, 0, 0, 0, 0},
/* [0b00011111] = */ {0, 1, 2, 3, 4, 0, 0, 0},
/* [0b00100000] = */ {5, 0, 0, 0, 0, 0, 0, 0},
/* [0b00100001] = */ {0, 5, 0, 0, 0, 0, 0, 0},
/* [0b00100010] = */ {1, 5, 0, 0, 0, 0, 0, 0},
/* [0b00100011] = */ {0, 1, 5, 0, 0, 0, 0, 0},
/* [0b00100100] = */ {2, 5, 0, 0, 0, 0, 0, 0},
/* [0b00100101] = */ {0, 2, 5, 0, 0, 0, 0, 0},
/* [0b00100110] = */ {1, 2, 5, 0, 0, 0, 0, 0},
/* [0b00100111] = */ {0, 1, 2, 5, 0, 0, 0, 0},
/* [0b00101000] = */ {3, 5, 0, 0, 0, 0, 0, 0},
/* [0b00101001] = */ {0, 3, 5, 0, 0, 0, 0, 0},
/* [0b00101010] = */ {1, 3, 5, 0, 0, 0, 0, 0},
/* [0b00101011] = */ {0, 1, 3, 5, 0, 0, 0, 0},
/* [0b00101100] = */ {2, 3, 5, 0, 0, 0, 0, 0},
/* [0b00101101] = */ {0, 2, 3, 5, 0, 0, 0, 0},
/* [0b00101110] = */ {1, 2, 3, 5, 0, 0, 0, 0},
/* [0b00101111] = */ {0, 1, 2, 3, 5, 0, 0, 0},
/* [0b00110000] = */ {4, 5, 0, 0, 0, 0, 0, 0},
/* [0b00110001] = */ {0, 4, 5, 0, 0, 0, 0, 0},
/* [0b00110010] = */ {1, 4, 5, 0, 0, 0, 0, 0},
/* [0b00110011] = */ {0, 1, 4, 5, 0, 0, 0, 0},
/* [0b00110100] = */ {2, 4, 5, 0, 0, 0, 0, 0},
/* [0b00110101] = */ {0, 2, 4, 5, 0, 0, 0, 0},
/* [0b00110110] = */ {1, 2, 4, 5, 0, 0, 0, 0},
/* [0b00110111] = */ {0, 1, 2, 4, 5, 0, 0, 0},
/* [0b00111000] = */ {3, 4, 5, 0, 0, 0, 0, 0},
/* [0b00111001] = */ {0, 3, 4, 5, 0, 0, 0, 0},
/* [0b00111010] = */ {1, 3, 4, 5, 0, 0, 0, 0},
/* [0b00111011] = */ {0, 1, 3, 4, 5, 0, 0, 0},
/* [0b00111100] = */ {2, 3, 4, 5, 0, 0, 0, 0},
/* [0b00111101] = */ {0, 2, 3, 4, 5, 0, 0, 0},
/* [0b00111110] = */ {1, 2, 3, 4, 5, 0, 0, 0},
/* [0b00111111] = */ {0, 1, 2, 3, 4, 5, 0, 0},
/* [0b01000000] = */ {6, 0, 0, 0, 0, 0, 0, 0},
/* [0b01000001] = */ {0, 6, 0, 0, 0, 0, 0, 0},
/* [0b01000010] = */ {1, 6, 0, 0, 0, 0, 0, 0},
/* [0b01000011] = */ {0, 1, 6, 0, 0, 0, 0, 0},
/* [0b01000100] = */ {2, 6, 0, 0, 0, 0, 0, 0},
/* [0b01000101] = */ {0, 2, 6, 0, 0, 0, 0, 0},
/* [0b01000110] = */ {1, 2, 6, 0, 0, 0, 0, 0},
/* [0b01000111] = */ {0, 1, 2, 6, 0, 0, 0, 0},
/* [0b01001000] = */ {3, 6, 0, 0, 0, 0, 0, 0},
/* [0b01001001] = */ {0, 3, 6, 0, 0, 0, 0, 0},
/* [0b01001010] = */ {1, 3, 6, 0, 0, 0, 0, 0},
/* [0b01001011] = */ {0, 1, 3, 6, 0, 0, 0, 0},
/* [0b01001100] = */ {2, 3, 6, 0, 0, 0, 0, 0},
/* [0b01001101] = */ {0, 2, 3, 6, 0, 0, 0, 0},
/* [0b01001110] = */ {1, 2, 3, 6, 0, 0, 0, 0},
/* [0b01001111] = */ {0, 1, 2, 3, 6, 0, 0, 0},
/* [0b01010000] = */ {4, 6, 0, 0, 0, 0, 0, 0},
/* [0b01010001] = */ {0, 4, 6, 0, 0, 0, 0, 0},
/* [0b01010010] = */ {1, 4, 6, 0, 0, 0, 0, 0},
/* [0b01010011] = */ {0, 1, 4, 6, 0, 0, 0, 0},
/* [0b01010100] = */ {2, 4, 6, 0, 0, 0, 0, 0},
/* [0b01010101] = */ {0, 2, 4, 6, 0, 0, 0, 0},
/* [0b01010110] = */ {1, 2, 4, 6, 0, 0, 0, 0},
/* [0b01010111] = */ {0, 1, 2, 4, 6, 0, 0, 0},
/* [0b01011000] = */ {3, 4, 6, 0, 0, 0, 0, 0},
/* [0b01011001] = */ {0, 3, 4, 6, 0, 0, 0, 0},
/* [0b01011010] = */ {1, 3, 4, 6, 0, 0, 0, 0},
/* [0b01011011] = */ {0, 1, 3, 4, 6, 0, 0, 0},
/* [0b01011100] = */ {2, 3, 4, 6, 0, 0, 0, 0},
/* [0b01011101] = */ {0, 2, 3, 4, 6, 0, 0, 0},
/* [0b01011110] = */ {1, 2, 3, 4, 6, 0, 0, 0},
/* [0b01011111] = */ {0, 1, 2, 3, 4, 6, 0, 0},
/* [0b01100000] = */ {5, 6, 0, 0, 0, 0, 0, 0},
/* [0b01100001] = */ {0, 5, 6, 0, 0, 0, 0, 0},
/* [0b01100010] = */ {1, 5, 6, 0, 0, 0, 0, 0},
/* [0b01100011] = */ {0, 1, 5, 6, 0, 0, 0, 0},
/* [0b01100100] = */ {2, 5, 6, 0, 0, 0, 0, 0},
/* [0b01100101] = */ {0, 2, 5, 6, 0, 0, 0, 0},
/* [0b01100110] = */ {1, 2, 5, 6, 0, 0, 0, 0},
/* [0b01100111] = */ {0, 1, 2, 5, 6, 0, 0, 0},
/* [0b01101000] = */ {3, 5, 6, 0, 0, 0, 0, 0},
/* [0b01101001] = */ {0, 3, 5, 6, 0, 0, 0, 0},
/* [0b01101010] = */ {1, 3, 5, 6, 0, 0, 0, 0},
/* [0b01101011] = */ {0, 1, 3, 5, 6, 0, 0, 0},
/* [0b01101100] = */ {2, 3, 5, 6, 0, 0, 0, 0},
/* [0b01101101] = */ {0, 2, 3, 5, 6, 0, 0, 0},
/* [0b01101110] = */ {1, 2, 3, 5, 6, 0, 0, 0},
/* [0b01101111] = */ {0, 1, 2, 3, 5, 6, 0, 0},
/* [0b01110000] = */ {4, 5, 6, 0, 0, 0, 0, 0},
/* [0b01110001] = */ {0, 4, 5, 6, 0, 0, 0, 0},
/* [0b01110010] = */ {1, 4, 5, 6, 0, 0, 0, 0},
/* [0b01110011] = */ {0, 1, 4, 5, 6, 0, 0, 0},
/* [0b01110100] = */ {2, 4, 5, 6, 0, 0, 0, 0},
/* [0b01110101] = */ {0, 2, 4, 5, 6, 0, 0, 0},
/* [0b01110110] = */ {1, 2, 4, 5, 6, 0, 0, 0},
/* [0b01110111] = */ {0, 1, 2, 4, 5, 6, 0, 0},
/* [0b01111000] = */ {3, 4, 5, 6, 0, 0, 0, 0},
/* [0b01111001] = */ {0, 3, 4, 5, 6, 0, 0, 0},
/* [0b01111010] = */ {1, 3, 4, 5, 6, 0, 0, 0},
/* [0b01111011] = */ {0, 1, 3, 4, 5, 6, 0, 0},
/* [0b01111100] = */ {2, 3, 4, 5, 6, 0, 0, 0},
/* [0b01111101] = */ {0, 2, 3, 4, 5, 6, 0, 0},
/* [0b01111110] = */ {1, 2, 3, 4, 5, 6, 0, 0},
/* [0b01111111] = */ {0, 1, 2, 3, 4, 5, 6, 0},
/* [0b10000000] = */ {7, 0, 0, 0, 0, 0, 0, 0},
/* [0b10000001] = */ {0, 7, 0, 0, 0, 0, 0, 0},
/* [0b10000010] = */ {1, 7, 0, 0, 0, 0, 0, 0},
/* [0b10000011] = */ {0, 1, 7, 0, 0, 0, 0, 0},
/* [0b10000100] = */ {2, 7, 0, 0, 0, 0, 0, 0},
/* [0b10000101] = */ {0, 2, 7, 0, 0, 0, 0, 0},
/* [0b10000110] = */ {1, 2, 7, 0, 0, 0, 0, 0},
/* [0b10000111] = */ {0, 1, 2, 7, 0, 0, 0, 0},
/* [0b10001000] = */ {3, 7, 0, 0, 0, 0, 0, 0},
/* [0b10001001] = */ {0, 3, 7, 0, 0, 0, 0, 0},
/* [0b10001010] = */ {1, 3, 7, 0, 0, 0, 0, 0},
/* [0b10001011] = */ {0, 1, 3, 7, 0, 0, 0, 0},
/* [0b10001100] = */ {2, 3, 7, 0, 0, 0, 0, 0},
/* [0b10001101] = */ {0, 2, 3, 7, 0, 0, 0, 0},
/* [0b10001110] = */ {1, 2, 3, 7, 0, 0, 0, 0},
/* [0b10001111] = */ {0, 1, 2, 3, 7, 0, 0, 0},
/* [0b10010000] = */ {4, 7, 0, 0, 0, 0, 0, 0},
/* [0b10010001] = */ {0, 4, 7, 0, 0, 0, 0, 0},
/* [0b10010010] = */ {1, 4, 7, 0, 0, 0, 0, 0},
/* [0b10010011] = */ {0, 1, 4, 7, 0, 0, 0, 0},
/* [0b10010100] = */ {2, 4, 7, 0, 0, 0, 0, 0},
/* [0b10010101] = */ {0, 2, 4, 7, 0, 0, 0, 0},
/* [0b10010110] = */ {1, 2, 4, 7, 0, 0, 0, 0},
/* [0b10010111] = */ {0, 1, 2, 4, 7, 0, 0, 0},
/* [0b10011000] = */ {3, 4, 7, 0, 0, 0, 0, 0},
/* [0b10011001] = */ {0, 3, 4, 7, 0, 0, 0, 0},
/* [0b10011010] = */ {1, 3, 4, 7, 0, 0, 0, 0},
/* [0b10011011] = */ {0, 1, 3, 4, 7, 0, 0, 0},
/* [0b10011100] = */ {2, 3, 4, 7, 0, 0, 0, 0},
/* [0b10011101] = */ {0, 2, 3, 4, 7, 0, 0, 0},
/* [0b10011110] = */ {1, 2, 3, 4, 7, 0, 0, 0},
/* [0b10011111] = */ {0, 1, 2, 3, 4, 7, 0, 0},
/* [0b10100000] = */ {5, 7, 0, 0, 0, 0, 0, 0},
/* [0b10100001] = */ {0, 5, 7, 0, 0, 0, 0, 0},
/* [0b10100010] = */ {1, 5, 7, 0, 0, 0, 0, 0},
/* [0b10100011] = */ {0, 1, 5, 7, 0, 0, 0, 0},
/* [0b10100100] = */ {2, 5, 7, 0, 0, 0, 0, 0},
/* [0b10100101] = */ {0, 2, 5, 7, 0, 0, 0, 0},
/* [0b10100110] = */ {1, 2, 5, 7, 0, 0, 0, 0},
/* [0b10100111] = */ {0, 1, 2, 5, 7, 0, 0, 0},
/* [0b10101000] = */ {3, 5, 7, 0, 0, 0, 0, 0},
/* [0b10101001] = */ {0, 3, 5, 7, 0, 0, 0, 0},
/* [0b10101010] = */ {1, 3, 5, 7, 0, 0, 0, 0},
/* [0b10101011] = */ {0, 1, 3, 5, 7, 0, 0, 0},
/* [0b10101100] = */ {2, 3, 5, 7, 0, 0, 0, 0},
/* [0b10101101] = */ {0, 2, 3, 5, 7, 0, 0, 0},
/* [0b10101110] = */ {1, 2, 3, 5, 7, 0, 0, 0},
/* [0b10101111] = */ {0, 1, 2, 3, 5, 7, 0, 0},
/* [0b10110000] = */ {4, 5, 7, 0, 0, 0, 0, 0},
/* [0b10110001] = */ {0, 4, 5, 7, 0, 0, 0, 0},
/* [0b10110010] = */ {1, 4, 5, 7, 0, 0, 0, 0},
/* [0b10110011] = */ {0, 1, 4, 5, 7, 0, 0, 0},
/* [0b10110100] = */ {2, 4, 5, 7, 0, 0, 0, 0},
/* [0b10110101] = */ {0, 2, 4, 5, 7, 0, 0, 0},
/* [0b10110110] = */ {1, 2, 4, 5, 7, 0, 0, 0},
/* [0b10110111] = */ {0, 1, 2, 4, 5, 7, 0, 0},
/* [0b10111000] = */ {3, 4, 5, 7, 0, 0, 0, 0},
/* [0b10111001] = */ {0, 3, 4, 5, 7, 0, 0, 0},
/* [0b10111010] = */ {1, 3, 4, 5, 7, 0, 0, 0},
/* [0b10111011] = */ {0, 1, 3, 4, 5, 7, 0, 0},
/* [0b10111100] = */ {2, 3, 4, 5, 7, 0, 0, 0},
/* [0b10111101] = */ {0, 2, 3, 4, 5, 7, 0, 0},
/* [0b10111110] = */ {1, 2, 3, 4, 5, 7, 0, 0},
/* [0b10111111] = */ {0, 1, 2, 3, 4, 5, 7, 0},
/* [0b11000000] = */ {6, 7, 0, 0, 0, 0, 0, 0},
/* [0b11000001] = */ {0, 6, 7, 0, 0, 0, 0, 0},
/* [0b11000010] = */ {1, 6, 7, 0, 0, 0, 0, 0},
/* [0b11000011] = */ {0, 1, 6, 7, 0, 0, 0, 0},
/* [0b11000100] = */ {2, 6, 7, 0, 0, 0, 0, 0},
/* [0b11000101] = */ {0, 2, 6, 7, 0, 0, 0, 0},
/* [0b11000110] = */ {1, 2, 6, 7, 0, 0, 0, 0},
/* [0b11000111] = */ {0, 1, 2, 6, 7, 0, 0, 0},
/* [0b11001000] = */ {3, 6, 7, 0, 0, 0, 0, 0},
/* [0b11001001] = */ {0, 3, 6, 7, 0, 0, 0, 0},
/* [0b11001010] = */ {1, 3, 6, 7, 0, 0, 0, 0},
/* [0b11001011] = */ {0, 1, 3, 6, 7, 0, 0, 0},
/* [0b11001100] = */ {2, 3, 6, 7, 0, 0, 0, 0},
/* [0b11001101] = */ {0, 2, 3, 6, 7, 0, 0, 0},
/* [0b11001110] = */ {1, 2, 3, 6, 7, 0, 0, 0},
/* [0b11001111] = */ {0, 1, 2, 3, 6, 7, 0, 0},
/* [0b11010000] = */ {4, 6, 7, 0, 0, 0, 0, 0},
/* [0b11010001] = */ {0, 4, 6, 7, 0, 0, 0, 0},
/* [0b11010010] = */ {1, 4, 6, 7, 0, 0, 0, 0},
/* [0b11010011] = */ {0, 1, 4, 6, 7, 0, 0, 0},
/* [0b11010100] = */ {2, 4, 6, 7, 0, 0, 0, 0},
/* [0b11010101] = */ {0, 2, 4, 6, 7, 0, 0, 0},
/* [0b11010110] = */ {1, 2, 4, 6, 7, 0, 0, 0},
/* [0b11010111] = */ {0, 1, 2, 4, 6, 7, 0, 0},
/* [0b11011000] = */ {3, 4, 6, 7, 0, 0, 0, 0},
/* [0b11011001] = */ {0, 3, 4, 6, 7, 0, 0, 0},
/* [0b11011010] = */ {1, 3, 4, 6, 7, 0, 0, 0},
/* [0b11011011] = */ {0, 1, 3, 4, 6, 7, 0, 0},
/* [0b11011100] = */ {2, 3, 4, 6, 7, 0, 0, 0},
/* [0b11011101] = */ {0, 2, 3, 4, 6, 7, 0, 0},
/* [0b11011110] = */ {1, 2, 3, 4, 6, 7, 0, 0},
/* [0b11011111] = */ {0, 1, 2, 3, 4, 6, 7, 0},
/* [0b11100000] = */ {5, 6, 7, 0, 0, 0, 0, 0},
/* [0b11100001] = */ {0, 5, 6, 7, 0, 0, 0, 0},
/* [0b11100010] = */ {1, 5, 6, 7, 0, 0, 0, 0},
/* [0b11100011] = */ {0, 1, 5, 6, 7, 0, 0, 0},
/* [0b11100100] = */ {2, 5, 6, 7, 0, 0, 0, 0},
/* [0b11100101] = */ {0, 2, 5, 6, 7, 0, 0, 0},
/* [0b11100110] = */ {1, 2, 5, 6, 7, 0, 0, 0},
/* [0b11100111] = */ {0, 1, 2, 5, 6, 7, 0, 0},
/* [0b11101000] = */ {3, 5, 6, 7, 0, 0, 0, 0},
/* [0b11101001] = */ {0, 3, 5, 6, 7, 0, 0, 0},
/* [0b11101010] = */ {1, 3, 5, 6, 7, 0, 0, 0},
/* [0b11101011] = */ {0, 1, 3, 5, 6, 7, 0, 0},
/* [0b11101100] = */ {2, 3, 5, 6, 7, 0, 0, 0},
/* [0b11101101] = */ {0, 2, 3, 5, 6, 7, 0, 0},
/* [0b11101110] = */ {1, 2, 3, 5, 6, 7, 0, 0},
/* [0b11101111] = */ {0, 1, 2, 3, 5, 6, 7, 0},
/* [0b11110000] = */ {4, 5, 6, 7, 0, 0, 0, 0},
/* [0b11110001] = */ {0, 4, 5, 6, 7, 0, 0, 0},
/* [0b11110010] = */ {1, 4, 5, 6, 7, 0, 0, 0},
/* [0b11110011] = */ {0, 1, 4, 5, 6, 7, 0, 0},
/* [0b11110100] = */ {2, 4, 5, 6, 7, 0, 0, 0},
/* [0b11110101] = */ {0, 2, 4, 5, 6, 7, 0, 0},
/* [0b11110110] = */ {1, 2, 4, 5, 6, 7, 0, 0},
/* [0b11110111] = */ {0, 1, 2, 4, 5, 6, 7, 0},
/* [0b11111000] = */ {3, 4, 5, 6, 7, 0, 0, 0},
/* [0b11111001] = */ {0, 3, 4, 5, 6, 7, 0, 0},
/* [0b11111010] = */ {1, 3, 4, 5, 6, 7, 0, 0},
/* [0b11111011] = */ {0, 1, 3, 4, 5, 6, 7, 0},
/* [0b11111100] = */ {2, 3, 4, 5, 6, 7, 0, 0},
/* [0b11111101] = */ {0, 2, 3, 4, 5, 6, 7, 0},
/* [0b11111110] = */ {1, 2, 3, 4, 5, 6, 7, 0},
/* [0b11111111] = */ {0, 1, 2, 3, 4, 5, 6, 7},
};
/* Popcount for a uint8_t. */
alignas(16) static const uint8_t mask_popcount[256] = {
0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3,
4, 4, 5, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4,
4, 5, 4, 5, 5, 6, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 2, 3, 3, 4, 3, 4, 4,
5, 3, 4, 4, 5, 4, 5, 5, 6, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 3, 4, 4, 5,
4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 2,
3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5,
5, 6, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4,
5, 4, 5, 5, 6, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 3, 4, 4, 5, 4, 5, 5, 6,
4, 5, 5, 6, 5, 6, 6, 7, 4, 5, 5, 6, 5, 6, 6, 7, 5, 6, 6, 7, 6, 7, 7, 8};
template<int Size> struct ShuffleIndicesResult {
simd::u8_base<Size> indices;
int popcount;
};
inline ShuffleIndicesResult<1> shuffle_indices_from_emit_mask(uint16_t emit_mask)
{
const uint8_t emit_mask_lo = emit_mask & 0xFFu;
const uint8_t emit_mask_hi = emit_mask >> 8;
const uint8_t mask_popcount_lo = mask_popcount[emit_mask_lo];
const uint8_t mask_popcount_hi = mask_popcount[emit_mask_hi];
/* Lookup the shuffle vector in 2 halves. */
uint64_t v0 = *(uint64_t *)shuffle_table_8[emit_mask_lo];
uint64_t v1 = *(uint64_t *)shuffle_table_8[emit_mask_hi] | uint64_t(0x0808080808080808);
/* Combine the two halves into one contiguous index array.
* We don't care about values after the last valid index. */
alignas(16) uint8_t combined[16];
*(uint64_t *)combined = v0;
*(uint64_t *)(combined + mask_popcount_lo) = v1;
return {simd::u8x16::load((const uint8_t *)&combined), mask_popcount_lo + mask_popcount_hi};
}
inline ShuffleIndicesResult<4> shuffle_indices_from_emit_mask(uint64_t emit_mask)
{
const uint8_t emit_mask_0 = (emit_mask >> 0) & 0xFFu;
const uint8_t emit_mask_1 = (emit_mask >> 8) & 0xFFu;
const uint8_t emit_mask_2 = (emit_mask >> 16) & 0xFFu;
const uint8_t emit_mask_3 = (emit_mask >> 24) & 0xFFu;
const uint8_t emit_mask_4 = (emit_mask >> 32) & 0xFFu;
const uint8_t emit_mask_5 = (emit_mask >> 40) & 0xFFu;
const uint8_t emit_mask_6 = (emit_mask >> 48) & 0xFFu;
const uint8_t emit_mask_7 = (emit_mask >> 56) & 0xFFu;
const uint8_t mask_popcount_0 = mask_popcount[emit_mask_0];
const uint8_t mask_popcount_1 = mask_popcount[emit_mask_1];
const uint8_t mask_popcount_2 = mask_popcount[emit_mask_2];
const uint8_t mask_popcount_3 = mask_popcount[emit_mask_3];
const uint8_t mask_popcount_4 = mask_popcount[emit_mask_4];
const uint8_t mask_popcount_5 = mask_popcount[emit_mask_5];
const uint8_t mask_popcount_6 = mask_popcount[emit_mask_6];
const uint8_t mask_popcount_7 = mask_popcount[emit_mask_7];
/* Lookup the shuffle vector in multiple part. */
uint64_t v0 = *(uint64_t *)shuffle_table_8[emit_mask_0];
uint64_t v1 = *(uint64_t *)shuffle_table_8[emit_mask_1] | uint64_t(0x0808080808080808);
uint64_t v2 = *(uint64_t *)shuffle_table_8[emit_mask_2] | uint64_t(0x1010101010101010);
uint64_t v3 = *(uint64_t *)shuffle_table_8[emit_mask_3] | uint64_t(0x1818181818181818);
uint64_t v4 = *(uint64_t *)shuffle_table_8[emit_mask_4] | uint64_t(0x2020202020202020);
uint64_t v5 = *(uint64_t *)shuffle_table_8[emit_mask_5] | uint64_t(0x2828282828282828);
uint64_t v6 = *(uint64_t *)shuffle_table_8[emit_mask_6] | uint64_t(0x3030303030303030);
uint64_t v7 = *(uint64_t *)shuffle_table_8[emit_mask_7] | uint64_t(0x3838383838383838);
/* Combine the parts into one contiguous index array.
* We don't care about values after the last valid index. */
alignas(64) uint8_t combined[64];
int popcount = 0;
*(uint64_t *)combined = v0, popcount += mask_popcount_0;
*(uint64_t *)(combined + popcount) = v1, popcount += mask_popcount_1;
*(uint64_t *)(combined + popcount) = v2, popcount += mask_popcount_2;
*(uint64_t *)(combined + popcount) = v3, popcount += mask_popcount_3;
*(uint64_t *)(combined + popcount) = v4, popcount += mask_popcount_4;
*(uint64_t *)(combined + popcount) = v5, popcount += mask_popcount_5;
*(uint64_t *)(combined + popcount) = v6, popcount += mask_popcount_6;
*(uint64_t *)(combined + popcount) = v7, popcount += mask_popcount_7;
return {simd::u8x64::load((const uint8_t *)&combined), popcount};
}
#endif
inline TokenType select(char char_value, char char_class, bool cond)
{
return TokenType((cond) ? char_class : char_value);
}
inline void TokenBuffer::tokenize_scalar(uint32_t &__restrict offset,
uint32_t &__restrict cursor_begin,
uint32_t &__restrict cursor_end,
CharClass &__restrict prev_char_class,
bool &__restrict prev_whitespace,
uint32_t end,
const CharClass char_class_table[128])
{
for (; offset < end; offset += 1) {
const char c = str_[offset];
const CharClass curr_char_class = char_class_table[c];
const TokenType curr_tok_type = select(
c, char(curr_char_class), curr_char_class > CharClass::ClassToTypeThreshold);
/* It is faster to overwrite the previous value with the same value
* as having a condition. */
types_[cursor_begin] = curr_tok_type;
offsets_[cursor_begin] = offset;
offsets_end_[cursor_end] = offset;
/**
* Split if no class in common.
* Example:
* str : i n t i 2 = 0 . 0 f ; i 2 + + ;
* emit : 1 0 0 1 1 0 1 1 1 1 1 1 0 1 1 1 0 1 0 1
*/
const bool emit = (uint8_t(curr_char_class) & uint8_t(prev_char_class) &
uint8_t(CharClass::CanMerge)) == 0;
prev_char_class = curr_char_class;
/**
* These are the emit mask we want to achieve:
* str : i n t i 2 = 0 . 0 f ; i 2 + + ;
* emit start : 1 0 0 0 1 0 0 1 0 1 1 1 0 1 0 1 0 1 0 1
* emit end : 0 0 0 1 0 0 1 0 1 0 1 1 0 1 1 0 0 1 0 1
*/
/* : 1 1 1 0 1 1 0 1 0 1 1 1 1 1 0 1 1 1 1 1 */
const bool curr_ws = (curr_char_class == CharClass::WhiteSpace);
/* : 0 0 0 1 0 0 1 0 1 0 0 0 0 0 1 0 0 0 0 0 */
const bool emit_ws = emit && curr_ws;
/* : 1 0 0 0 1 0 0 1 0 1 1 1 0 1 0 1 0 1 0 1 */
const bool emit_start = emit && !curr_ws;
/* : 0 0 0 0 0 0 0 0 0 0 1 1 0 1 0 0 0 1 0 1 */
const bool follow_non_ws = emit_start && !prev_whitespace;
/* : 0 0 0 1 0 0 1 0 1 0 1 1 0 1 1 0 0 1 0 1 */
const bool emit_end = emit_ws || follow_non_ws;
prev_whitespace = curr_ws;
#ifdef LEXIT_DEBUG
if (emit_start) {
int start = offsets_[cursor_begin - 1];
int end = offsets_[cursor_begin];
token_str_with_whitespace_debug_.emplace_back(str_.data() + start, end - start);
}
if (emit_end) {
int start = offsets_[cursor_end];
int end = offsets_end_[cursor_end];
token_str_debug_.emplace_back(str_.data() + start, end - start);
}
#endif
cursor_begin += emit_start;
cursor_end += emit_end;
}
}
void TokenBuffer::tokenize(const CharClass char_class_table[128])
{
/* Ensure enough space for the worse scenario, which is one token per character.
* This is done in order to avoid allocation and check inside the hot loop. */
reserve(str_.size());
if (str_.size() == 0) {
size_ = 0;
types_[0] = EndOfFile;
return;
}
/**
* The goal of this function is to build the offset and type buffers describing the tokens.
* The type of the token is decided by the character class of its first character. This is a
* simple lookup from the `char_class_table` combined with a `select`. It is then merged with the
* preceding character if the classes are compatible (see `CharClass::CanMerge`).
*
* The offsets define the boundaries of the tokens.
* If whitespaces are treated as tokens, one offset is emitted for the beginning of each token.
*
* str: i n t a = 0 ; EndOfFile
* emit: 1 0 0 0 1 1 1 1 1 1
* offsets: 0 4 6 7 8 9
*
* If whitespaces are merged with preceding tokens, one offset is emitted for the begining of
* each token *and* at either the start of the next token or the start of the next whitespace.
*
* str: i n t a = 0 ; EndOfFile
* emit start: 1 0 0 0 1 1 1 1 0 1
* emit end: 1 0 0 1 0 1 1 1 1 1
* offsets start: 0 4 5 6 7 9
* offsets end: 3 5 6 7 8 9
*/
CharClass prev_value = CharClass::None;
bool prev_whitespace;
{
/* First iteration needs to always emit start of the token. */
const CharClass curr = char_class_table[str_[0]];
types_[0] = select(str_[0], char(curr), curr > CharClass::ClassToTypeThreshold);
offsets_[0] = 0;
offsets_end_[0] = 0;
prev_value = curr;
prev_whitespace = curr == CharClass::WhiteSpace;
}
uint32_t offset = 1, cursor = 1, cursor_end = prev_whitespace ? 1 : 0;
int stride = 64;
/* Process until alignment to SIMD size is met. */
size_t index_at_align = ((stride - (uintptr_t(str_.data() + 1) & (stride - 1))) & (stride - 1)) +
1;
tokenize_scalar(offset,
cursor,
cursor_end,
prev_value,
prev_whitespace,
str_.size() > index_at_align ? index_at_align : str_.size(),
char_class_table);
#if defined(USE_NEON) || defined(USE_SSE4_2)
using namespace lexit::simd;
const uint8_t *str = (const uint8_t *)str_.data();
const u8x128_table char_to_class = u8x128_table::load((const uint8_t *)char_class_table);
for (; offset + stride <= str_.size(); offset += stride) {
# ifdef LEXIT_DEBUG
const std::string_view char_simd{str_.data() + offset, size_t(stride)};
# endif
const u8x64 c = u8x64::load(str + offset);
const u8x64 curr_char_class = char_to_class[c];
/* Shift and add the last iteration end token at the start of the vector. */
const u8x64 prev_char_class = shift_lanes_right<1>(curr_char_class, uint8_t(prev_value));
const u8x64 to_type_threshold{uint8_t(CharClass::ClassToTypeThreshold)};
const u8x64 can_merge{uint8_t(CharClass::CanMerge)};
const u8x64 curr_tok_type = select(c, curr_char_class, curr_char_class > to_type_threshold);
const u8x64 emit = is_zero(curr_char_class & prev_char_class & can_merge);
/* Store for next iteration. */
prev_value = CharClass(curr_char_class.last());
/* Start and end of token. See scalar version for documentation. */
const u8x64 curr_ws = (curr_char_class == uint8_t(CharClass::WhiteSpace));
const u8x64 curr_non_ws = ~curr_ws;
const u8x64 emit_ws = emit & curr_ws;
const u8x64 emit_start = emit & curr_non_ws;
const u8x64 prev_non_ws = shift_lanes_right<1>(curr_non_ws, prev_whitespace ? 0x0 : 0xFF);
const u8x64 follow_non_ws = emit_start & prev_non_ws;
const u8x64 emit_end = emit_ws | follow_non_ws;
/* Store for next iteration. */
prev_whitespace = !curr_non_ws.last();
uint64_t emit_end_mask = movemask(emit_end);
uint64_t emit_start_mask = movemask(emit_start);
/* Stream compaction of data based on the emit mask (0xFF == emit, 0x00 == skip).
* Stores `data` compacted inside `data_out` starting from `data_out + cursor` and advance
* `cursor` by the number of element compacted. */
{
auto [shuffle, popcount] = shuffle_indices_from_emit_mask(emit_start_mask);
/* Move data to destination elements (compaction). */
const u8x64 data_packed = u8x64_table(curr_tok_type)[shuffle];
/* Write 16 types in the stream. */
data_packed.store_unaligned((uint8_t *)&types_[cursor]);
/* The offsets are contained inside the 8 bit shuffle vector.
* We need to promote it to 32 bit before adding the base offset. */
(u32x16(shuffle.lane(0)) + offset).store_unaligned(&offsets_[cursor + 0]);
(u32x16(shuffle.lane(1)) + offset).store_unaligned(&offsets_[cursor + 16]);
(u32x16(shuffle.lane(2)) + offset).store_unaligned(&offsets_[cursor + 32]);
(u32x16(shuffle.lane(3)) + offset).store_unaligned(&offsets_[cursor + 48]);
# ifdef LEXIT_DEBUG
for (int i = cursor; i < cursor + popcount; i++) {
int start = offsets_[i - 1];
int end = offsets_[i];
token_str_with_whitespace_debug_.emplace_back(str_.data() + start, end - start);
}
# endif
cursor += popcount;
}
{
auto [shuffle, popcount] = shuffle_indices_from_emit_mask(emit_end_mask);
/* The offsets are contained inside the 8 bit shuffle vector.
* We need to promote it to 32 bit before adding the base offset. */
(u32x16(shuffle.lane(0)) + offset).store_unaligned(&offsets_end_[cursor_end + 0]);
(u32x16(shuffle.lane(1)) + offset).store_unaligned(&offsets_end_[cursor_end + 16]);
(u32x16(shuffle.lane(2)) + offset).store_unaligned(&offsets_end_[cursor_end + 32]);
(u32x16(shuffle.lane(3)) + offset).store_unaligned(&offsets_end_[cursor_end + 48]);
# ifdef LEXIT_DEBUG
for (int i = cursor_end; i < cursor_end + popcount; i++) {
int start = offsets_[i];
int end = offsets_end_[i];
token_str_debug_.emplace_back(str_.data() + start, end - start);
}
# endif
cursor_end += popcount;
}
assert(cursor_end == cursor || cursor_end + 1 == cursor);
}
#endif
assert(cursor_end == cursor || cursor_end + 1 == cursor);
/* Finish tail using scalar loop. */
tokenize_scalar(
offset, cursor, cursor_end, prev_value, prev_whitespace, str_.size(), char_class_table);
assert(cursor_end == cursor || cursor_end + 1 == cursor);
/* Set end of last token. */
offsets_[cursor] = str_.size();
offsets_end_[cursor_end] = str_.size();
/* Set end of file token. */
types_[cursor] = EndOfFile;
size_ = cursor;
}
void TokenBuffer::compute_lengths()
{
uint32_t tok_id = 0;
#if defined(USE_NEON) || defined(USE_SSE4_2)
using namespace simd;
static constexpr int stride = 64;
for (; tok_id + stride <= size_; tok_id += stride) {
const u32x64 str_start = u32x64::load(&offsets_[tok_id]);
const u32x64 str_end = u32x64::load(&offsets_end_[tok_id]);
const u32x64 str_size_32 = str_end - str_start;
const u8x64 str_large = u8x64(str_size_32 > 127);
/* Saturate the size to max int8_t since SSE comparison is signed. */
const u8x64 str_size = select(u8x64(str_size_32), u8x64(127), str_large);
str_size.store(&lengths_[tok_id]);
}
/* Finish tail using scalar loop. */
#endif
for (; tok_id < size_; ++tok_id) {
const uint32_t str_start = offsets_[tok_id];
const uint32_t str_end = offsets_end_[tok_id];
const uint32_t str_size_32 = str_end - str_start;
/* Saturate the size to max int8_t since SSE comparison is signed. */
const uint8_t str_size = select(uint8_t(str_size_32), 127, str_size_32 > 127);
lengths_[tok_id] = str_size;
}
}
void TokenBuffer::atomize_words(IdentifierMap &identifiers, const KeywordTable &keywords)
{
static constexpr int stride = 64;
struct Masks {
uint64_t mask8, mask16, mask24, mask32;
};
/* First scan to create a bitmap of potential matches to avoid wasting cycles iterating over
* non-words. */
lexit::Vector<Masks> masks_small_id;
lexit::Vector<uint64_t> masks_large_id;
masks_small_id.resize(divide_ceil(size_, stride));
masks_large_id.resize(divide_ceil(size_, stride));
{
uint32_t chunk_id = 0;
uint32_t tok_id = 0;
#if defined(USE_NEON) || defined(USE_SSE4_2)
using namespace simd;
for (; tok_id + stride <= size_; tok_id += stride, ++chunk_id) {
const u8x64 size = u8x64::load(&lengths_[tok_id]);
const u8x64 type = u8x64::load((uint8_t *)&types_[tok_id]);
const uint64_t is_word = movemask(type == Word);
const uint64_t less_8 = movemask(size < 9);
const uint64_t less_16 = movemask(size < 17);
const uint64_t less_24 = movemask(size < 25);
const uint64_t less_32 = movemask(size < 33);
Masks small;
small.mask8 = is_word & less_8;
small.mask16 = is_word & less_16 & ~less_8;
small.mask24 = is_word & less_24 & ~less_16;
small.mask32 = is_word & less_32 & ~less_24;
masks_small_id[chunk_id] = small;
masks_large_id[chunk_id] = is_word & ~less_32;
}
#endif
for (; tok_id < size_; tok_id += stride, ++chunk_id) {
uint64_t is_word = 0, less_8 = 0, less_16 = 0, less_24 = 0, less_32 = 0;
for (int i = 0; i < stride && tok_id + i < size_; ++i) {
const uint8_t size = lengths_[tok_id + i];
const TokenType type = types_[tok_id + i];
const uint64_t bit = uint64_t(1) << i;
/* Generate a masks of all 1s if true, all 0s if false. */
is_word |= bit & -uint64_t(type == Word);
less_8 |= bit & -uint64_t(size < 9);
less_16 |= bit & -uint64_t(size < 17);
less_24 |= bit & -uint64_t(size < 25);
less_32 |= bit & -uint64_t(size < 33);
}
Masks small;
small.mask8 = is_word & less_8;
small.mask16 = is_word & less_16 & ~less_8;
small.mask24 = is_word & less_24 & ~less_16;
small.mask32 = is_word & less_32 & ~less_24;
masks_small_id[chunk_id] = small;
masks_large_id[chunk_id] = is_word & ~less_32;
}
}
{
/* Iterate over the bitmasks. */
const int end = divide_ceil(size_, stride);
/* The atomize_short_tokens_in_mask can read past the end of each token by 8 bytes.
* For this reason we process the last chunks that contains the last 8 tokens separately. */
const int end_safe = divide_ceil(int(size_) - 8, stride) - 1;
int chunk = 0;
for (; chunk < end_safe; ++chunk) {
const Masks small = masks_small_id[chunk];
atomize_short_tokens_in_mask<1>(small.mask8, chunk * stride, identifiers, keywords);
atomize_short_tokens_in_mask<2>(small.mask16, chunk * stride, identifiers, keywords);
atomize_short_tokens_in_mask<3>(small.mask24, chunk * stride, identifiers, keywords);
atomize_short_tokens_in_mask<4>(small.mask32, chunk * stride, identifiers, keywords);
atomize_tokens_in_mask(masks_large_id[chunk], chunk * stride, identifiers, keywords);
}
for (; chunk < end; ++chunk) {
const Masks small = masks_small_id[chunk];
atomize_tokens_in_mask(small.mask8, chunk * stride, identifiers, keywords);
atomize_tokens_in_mask(small.mask16, chunk * stride, identifiers, keywords);
atomize_tokens_in_mask(small.mask24, chunk * stride, identifiers, keywords);
atomize_tokens_in_mask(small.mask32, chunk * stride, identifiers, keywords);
atomize_tokens_in_mask(masks_large_id[chunk], chunk * stride, identifiers, keywords);
}
}
}
INLINE_METHOD void TokenBuffer::atomize_tokens_in_mask(uint64_t mask,
uint32_t tok_id_base,
IdentifierMap &id_map,
const KeywordTable &kw_table)
{
if (mask == 0) [[likely]] {
return;
}
while (mask != 0) {
const int index = builtin_ctzll(mask);
const int tok_id = tok_id_base + index;
const int str_start = offsets_[tok_id];
const int str_size = offsets_end_[tok_id] - str_start;
const std::string_view str = {str_.data() + str_start, size_t(str_size)};
const TokenAtom atom = id_map.lookup_or_add(str);
atoms_[tok_id] = atom;
types_[tok_id] = kw_table[atom];
/* Pop last bit. */
mask &= (mask - 1);
}
}
template<int Size>
INLINE_METHOD void TokenBuffer::atomize_short_tokens_in_mask(uint64_t mask,
uint32_t tok_id_base,
IdentifierMap &id_map,
const KeywordTable &kw_table)
{
while (mask != 0) {
const int index = builtin_ctzll(mask);
const int tok_id = tok_id_base + index;
const int str_start = offsets_[tok_id];
const int str_size = lengths_[tok_id]; /* PaddedString is for small identifier. */
const std::string_view str = {str_.data() + str_start, size_t(str_size)};
const PaddedString<Size> padded_str(str);
const TokenAtom atom = id_map.lookup_or_add(padded_str);
atoms_[tok_id] = atom;
types_[tok_id] = kw_table[atom];
/* Pop last bit. */
mask &= (mask - 1);
}
}
template void TokenBuffer::atomize_short_tokens_in_mask<1>(uint64_t,
uint32_t,
IdentifierMap &,
const KeywordTable &);
template void TokenBuffer::atomize_short_tokens_in_mask<2>(uint64_t,
uint32_t,
IdentifierMap &,
const KeywordTable &);
template void TokenBuffer::atomize_short_tokens_in_mask<3>(uint64_t,
uint32_t,
IdentifierMap &,
const KeywordTable &);
template void TokenBuffer::atomize_short_tokens_in_mask<4>(uint64_t,
uint32_t,
IdentifierMap &,
const KeywordTable &);
static void lex_string(const TokenType *types, uint32_t &cursor)
{
const TokenType *ptr = types + cursor;
while (true) {
cursor++;
ptr++;
if (*ptr == '\\') {
/* Escaped character. Skip next. */
cursor++;
ptr++;
continue;
}
if (*ptr == String || *ptr == EndOfFile) {
return;
}
}
}
static void lex_float(const std::string_view str,
const TokenType *types,
const uint32_t *offsets,
const uint32_t *offsets_end_,
uint32_t &cursor)
{
const TokenType *type = types + cursor;
const uint32_t *offset = offsets + cursor;
const uint32_t *offset_end_ = offsets_end_ + cursor;
/* If number is followed by whitespace itself. */
const bool followed_by_whitespace = offset_end_[0] != offset[1];
if (followed_by_whitespace) {
return;
}
while (true) {
cursor++;
type++;
offset++;
offset_end_++;
/* Check if the previous char was an exponent "e" char. */
if ((*type == '+' || *type == '-') && str[*offset - 1] != 'e') {
break;
}
if (!(*type == Word || *type == Number || *type == '.' || *type == '+' || *type == '-')) {
break;
}
/* Break if this token is part of the number but followed by a whitespace. */
const bool followed_by_whitespace = offset_end_[0] != offset[1];
if (followed_by_whitespace) {
/* Note: we don't want to do the cursor roll back since we want to merge this token. */
return;
}
}
/* We need to evaluate the token we broke on. */
cursor--;
}
static void lex_comment(const std::string_view str,
const TokenType *types,
const uint32_t *offsets,
uint32_t &cursor,
TokenType &out_type)
{
const uint32_t start = offsets[cursor];
const TokenType *type = types + cursor;
const char c = str[start + 1];
size_t end_pos;
if (c == '/') {
/* Single-line comment. Search for end of line. */
end_pos = str.find('\n', start + 2);
}
else if (c == '*') {
/* Multi-line comment. Search for termination. */
end_pos = str.find("*/", start + 2);
/* Search for the closing slash. */
end_pos += (end_pos != std::string::npos) ? 1 : 0;
}
else {
/* Not a comment. */
return;
}
out_type = Comment;
while (*type != EndOfFile) {
const uint32_t tok_start = offsets[cursor];
/* Skip tokens until we find the one that starts after the end of the comment. */
if (tok_start > end_pos) {
break;
}
cursor++;
type++;
}
/* We need to evaluate the token we broke on. */
cursor--;
}
/* Ideally this should not be needed and the `tokenize` step should just parse them correctly.
* But this would much harder to implement. */
void TokenBuffer::merge_complex_literals()
{
const TokenType *in_types = types_.get();
TokenType *out_type = types_.get();
const uint32_t *in_offsets = offsets_.get();
const uint32_t *in_offset_end = offsets_end_.get();
uint32_t *out_offset = offsets_.get();
uint32_t *out_offset_end = offsets_end_.get();
for (uint32_t i = 0; i < size_; i++, out_type++, out_offset++, out_offset_end++) {
const TokenType type = in_types[i];
const uint32_t offset = in_offsets[i];
const uint32_t offset_end = in_offset_end[i];
*out_type = type;
*out_offset = offset;
*out_offset_end = offset_end;
switch (type) {
case String:
lex_string(in_types, i);
break;
case Number:
lex_float(str_, in_types, in_offsets, in_offset_end, i);
break;
case Slash:
lex_comment(str_, in_types, in_offsets, i, *out_type);
break;
default:
continue;
}
/* Set the correct end for the complex token that have just been lexed. */
*out_offset_end = in_offset_end[i];
}
assert(in_types <= out_type);
assert(out_type - in_types < 0xFFFFFFFFu);
size_ = out_type - in_types;
types_[size_] = EndOfFile;
offsets_[size_] = str_.size();
}
} // namespace lexit

View File

@@ -0,0 +1,409 @@
/* SPDX-FileCopyrightText: 2026 Clement Foucault
*
* SPDX-License-Identifier: MIT */
/**
* LexIt is a lexer tool library focus on simplicity and efficiency.
*
* It is aimed at building source code processors without requiring huge dependencies like LLVM.
* It only supports unextended-ASCII inputs that are under 4GB (because of 32bit offsets).
*/
#pragma once
#include <cassert>
#include <climits>
#include <cstdint>
#include <iostream>
#include <iterator>
#include <string_view>
#include "identifier.hh"
#include "types.hh"
#include "vector.hh"
// #define LEXIT_DEBUG
#ifdef LEXIT_DEBUG
/* Make it a warning to avoid shipping with it. */
# warning "Lexit debug mode enabled"
# include <vector>
#endif
#if defined(_MSC_VER)
# define INLINE_METHOD __forceinline
#else
# define INLINE_METHOD inline __attribute__((always_inline))
#endif
namespace lexit {
struct TokenBuffer;
struct Token {
#ifdef LEXIT_DEBUG
std::string_view debug_str_;
const TokenType *debug_type_;
const TokenAtom *debug_atom_;
#endif
const TokenBuffer *buf_;
int32_t index_;
/* General purpose flag. */
int32_t flag = 0;
Token(const TokenBuffer *buf, int32_t index);
/* Invalid token that can still be compared with other tokens. */
static Token invalid(const TokenBuffer *buf);
explicit operator int32_t() const
{
return index_;
}
bool is_valid() const
{
return type() != EndOfFile;
}
bool is_invalid() const
{
return type() == EndOfFile;
}
const TokenType &type() const;
const TokenAtom &atom() const;
std::string_view str() const;
std::string_view str_with_whitespace() const;
bool followed_by_whitespace() const;
Token next(int i = 1) const;
Token prev(int i = 1) const;
friend bool operator==(const Token &a, const Token &b)
{
assert(a.buf_ == b.buf_);
return a.index_ == b.index_;
}
friend bool operator!=(const Token &a, const Token &b)
{
assert(a.buf_ == b.buf_);
return a.index_ != b.index_;
}
friend bool operator==(const Token &a, TokenType b)
{
return a.type() == b;
}
friend bool operator!=(const Token &a, TokenType b)
{
return a.type() != b;
}
};
/* Same as Token but allow type assignment. */
struct TokenMut : public Token {
TokenMut(TokenBuffer *buf, int32_t index) : Token(buf, index) {}
TokenType &type();
TokenAtom &atom();
};
template<typename T> struct AlignedDeleter {
void operator()(T *p) const
{
::operator delete[](p, std::align_val_t{64});
}
};
struct TokenBuffer {
/* Input string. */
std::string_view str_;
/* Type of each token. */
AlignedArrayPtr<TokenType> types_;
/* Starting character index of each token. */
AlignedArrayPtr<uint32_t> offsets_;
/* Original character index of each token before whitespace merging. */
AlignedArrayPtr<uint32_t> offsets_end_;
/* Length in characters of each token. A value of 127 means the real size is over 126. */
AlignedArrayPtr<uint8_t> lengths_;
/* Unique id for identifiers (Words). Externally set (optional). */
AlignedArrayPtr<TokenAtom> atoms_;
/* Number of tokens inside the buffer excluding the terminating EndOfFile token. */
uint32_t size_ = 0;
/* Number of tokens that can be contained. */
uint32_t allocated_size_ = 0;
#ifdef LEXIT_DEBUG
std::vector<std::string_view> token_str_debug_;
std::vector<std::string_view> token_str_with_whitespace_debug_;
#endif
TokenBuffer() = default;
TokenBuffer(const std::string_view str, const CharClass char_class_table[128])
{
process(str, char_class_table);
}
/*
* The given string lifetime should outlive the #TokenBuffer. No copy is done.
*/
void process(const std::string_view str, const CharClass char_class_table[128])
{
assert(str.size() < UINT_MAX);
str_ = str;
clear();
tokenize(char_class_table);
compute_lengths();
}
/**
* \brief Discard all currently held data. Does not reallocate.
*/
void clear();
/**
* \brief Allocate backing memory for the given number of tokens and move currently held data.
*
* Does nothing if allocation is already large enough.
*/
void reserve(const uint32_t count);
/**
* \brief Tokenizes the input string by grouping contiguous characters of the same class.
*
* This function iterates through the input string and identifies "runs" of characters
* that map to the same CharClass. For each new group, it records the type and the
* starting byte offset into the result arrays.
*
* Only characters with the #CanMerge flag are merged together.
* Characters with a class greater than #ClassToTypeThreshold will just be assigned their class
* as #TokenType. Otherwise, the first character of the token will be used as #TokenType.
*
* If the input string contains characters that are not inside the ASCII range, the result of
* the operation is undefined and might cause segmentation fault.
*
* \param char_class_table A lookup table mapping ASCII values (0-127) to an 8-bit CharClass.
*/
void tokenize(const CharClass char_class_table[128]);
/**
* \brief Merge complex literals such as floats, strings and comments.
*/
void merge_complex_literals();
/**
* \brief Assign keyword types and atoms for a small set of identifier.
*/
void atomize_words(IdentifierMap &identifiers, const KeywordTable &keywords);
/**
* \brief Compute small token length for speeding up certain tasks.
*/
void compute_lengths();
/**
* \brief Return the amount of token inside the buffer.
*/
uint32_t size() const
{
return size_;
}
/**
* \brief Return the substring between the start and end tokens (included).
*
* \param with_trailing_whitespaces If true, include the trailing whitespaces.
*/
std::string_view substr(const Token &start,
const Token &end,
const bool with_trailing_whitespaces = false) const
{
int start_char = offsets_[int(start)];
int end_char = (!with_trailing_whitespaces) ? offsets_end_[int(end)] : offsets_[int(end) + 1];
return str_.substr(start_char, end_char - start_char);
}
/**
* \brief Append a Token at the end of the buffer.
*/
void append(TokenType type, TokenAtom atom, int32_t str_size, int32_t str_size_with_witespaces)
{
reserve(size_ + 1);
types_[size_] = type;
atoms_[size_] = atom;
offsets_[size_ + 1] = offsets_[size_] + str_size_with_witespaces;
offsets_end_[size_] = offsets_[size_] + str_size;
size_++;
}
Token operator[](int index) const
{
return Token(this, index);
}
TokenMut operator[](int index)
{
return TokenMut(this, index);
}
/**
* \brief Token iterator.
*/
struct TokenIt {
using iterator_category = std::forward_iterator_tag;
private:
TokenBuffer *buf_;
int32_t index_;
public:
TokenIt(TokenBuffer *buf, const int index) : buf_(buf), index_(index) {}
TokenMut operator*() const
{
return TokenMut(buf_, index_);
}
TokenIt &operator++()
{
index_++;
return *this;
}
int32_t index() const
{
return index_;
}
bool operator==(const TokenIt &other) const
{
return index_ == other.index_;
}
bool operator!=(const TokenIt &other) const
{
return index_ != other.index_;
}
bool operator<(const TokenIt &other) const
{
return index_ < other.index_;
}
};
TokenIt begin()
{
return TokenIt(this, 0);
}
TokenIt end()
{
return TokenIt(this, size_);
}
Token front()
{
return (*this)[0];
}
Token back()
{
return (*this)[size_ - 1];
}
private:
inline void tokenize_scalar(uint32_t &__restrict offset,
uint32_t &__restrict cursor_begin,
uint32_t &__restrict cursor_end,
CharClass &__restrict prev_char_class,
bool &__restrict prev_whitespace,
uint32_t end,
const CharClass char_class_table[128]);
template<int Size = 0>
INLINE_METHOD void atomize_short_tokens_in_mask(uint64_t mask,
uint32_t tok_id_base,
IdentifierMap &id_map,
const KeywordTable &kw_table);
INLINE_METHOD void atomize_tokens_in_mask(uint64_t mask,
uint32_t tok_id_base,
IdentifierMap &id_map,
const KeywordTable &kw_table);
};
inline Token::Token(const TokenBuffer *buf, int32_t index) : buf_(buf)
{
assert(buf_ != nullptr);
/* Set to Invalid / EndOfFile token if out of range. */
index_ = (index < 0 || index > buf_->size_) ? buf_->size_ : index;
#ifdef LEXIT_DEBUG
debug_str_ = str_with_whitespace();
debug_type_ = &type();
debug_atom_ = &atom();
#endif
}
inline Token Token::invalid(const TokenBuffer *buf)
{
return Token(buf, buf->size_);
}
inline const TokenType &Token::type() const
{
return buf_->types_[index_];
}
inline TokenType &TokenMut::type()
{
return const_cast<TokenBuffer *>(buf_)->types_[index_];
}
inline const TokenAtom &Token::atom() const
{
return buf_->atoms_[index_];
}
inline TokenAtom &TokenMut::atom()
{
return const_cast<TokenBuffer *>(buf_)->atoms_[index_];
}
inline Token Token::next(int i) const
{
return Token(buf_, index_ + i);
}
inline Token Token::prev(int i) const
{
return is_valid() ? Token(buf_, index_ - i) : Token(buf_, -1);
}
inline std::string_view Token::str() const
{
int start = buf_->offsets_[index_];
int end = buf_->offsets_end_[index_];
return {buf_->str_.data() + start, size_t(end - start)};
}
inline std::string_view Token::str_with_whitespace() const
{
int start = buf_->offsets_[index_];
int end = buf_->offsets_[index_ + 1];
return {buf_->str_.data() + start, size_t(end - start)};
}
inline bool Token::followed_by_whitespace() const
{
assert(is_valid());
return buf_->offsets_end_[index_] != buf_->offsets_[index_ + 1];
}
inline std::ostream &operator<<(std::ostream &os, const Token &tok)
{
os << "Token(";
os << "type='" << tok.type() << "', ";
os << "atom=" << tok.atom() << ", ";
os << "str=\"" << tok.str() << "\", ";
os << "str_with_whitespace=\"" << tok.str_with_whitespace() << "\"";
os << ")";
return os;
}
} // namespace lexit

View File

@@ -0,0 +1,829 @@
/* SPDX-FileCopyrightText: 2026 Blender Authors
*
* SPDX-License-Identifier: MIT */
/**
* Small SIMD library to avoid duplicating code.
*/
#pragma once
#include <cassert>
#include <cstdint>
#if defined(__ARM_NEON)
# define USE_NEON
# include <arm_neon.h>
#endif
#if (defined(__x86_64__) || defined(_M_X64)) && defined(__SSE4_2__)
# define USE_SSE4_2
# include <immintrin.h>
#endif
#if defined(USE_NEON) || defined(USE_SSE4_2)
namespace lexit::simd {
/* Size must be power of 2. */
template<int Size> struct u8_base {
# if defined(USE_NEON)
uint8x16_t lanes[Size];
# elif defined(USE_SSE4_2)
__m128i lanes[Size];
# endif
u8_base() = default;
explicit u8_base(const uint8_t scalar)
{
for (int i = 0; i < Size; ++i) {
# if defined(USE_NEON)
lanes[i] = vdupq_n_u8(scalar);
# elif defined(USE_SSE4_2)
lanes[i] = _mm_set1_epi8(scalar);
# endif
}
}
static u8_base load_unaligned(const uint8_t *src)
{
u8_base result;
for (int i = 0; i < Size; ++i) {
# if defined(USE_NEON)
result.lanes[i] = vld1q_u8(src + i * 16);
# elif defined(USE_SSE4_2)
result.lanes[i] = _mm_loadu_si128((const __m128i *)src + i);
# endif
}
return result;
}
static u8_base load(const uint8_t *src)
{
assert((intptr_t(src) & (16 - 1)) == 0);
u8_base result;
for (int i = 0; i < Size; ++i) {
# if defined(USE_NEON)
result.lanes[i] = vld1q_u8(src + i * 16);
# elif defined(USE_SSE4_2)
result.lanes[i] = _mm_load_si128((const __m128i *)src + i);
# endif
}
return result;
}
void store_unaligned(uint8_t *dst) const
{
for (int i = 0; i < Size; ++i) {
# if defined(USE_NEON)
vst1q_u8(dst + i * 16, lanes[i]);
# elif defined(USE_SSE4_2)
_mm_storeu_si128((__m128i *)dst + i, lanes[i]);
# endif
}
}
void store(uint8_t *dst) const
{
for (int i = 0; i < Size; ++i) {
# if defined(USE_NEON)
vst1q_u8(dst + i * 16, lanes[i]);
# elif defined(USE_SSE4_2)
_mm_store_si128((__m128i *)dst + i, lanes[i]);
# endif
}
}
u8_base<1> lane(int i) const
{
u8_base<1> result;
result.lanes[0] = lanes[i];
return result;
}
/* Get content of end lane */
uint8_t last() const
{
# if defined(USE_NEON)
return vgetq_lane_u8(lanes[Size - 1], 15);
# elif defined(USE_SSE4_2)
auto lane = lanes[Size - 1];
return _mm_extract_epi8(lane, 15);
# endif
}
/* Get content of end lane */
uint8_t first() const
{
# if defined(USE_NEON)
return vgetq_lane_u8(lanes[0], 0);
# elif defined(USE_SSE4_2)
return _mm_extract_epi8(lanes[0], 0);
# endif
}
/* --- Bitwise Operators --- */
friend u8_base operator^(u8_base a, u8_base b)
{
u8_base res;
for (int i = 0; i < Size; ++i) {
# if defined(USE_NEON)
res.lanes[i] = veorq_u8(a.lanes[i], b.lanes[i]);
# elif defined(USE_SSE4_2)
res.lanes[i] = _mm_xor_si128(a.lanes[i], b.lanes[i]);
# endif
}
return res;
}
friend u8_base operator|(u8_base a, u8_base b)
{
u8_base res;
for (int i = 0; i < Size; ++i) {
# if defined(USE_NEON)
res.lanes[i] = vorrq_u8(a.lanes[i], b.lanes[i]);
# elif defined(USE_SSE4_2)
res.lanes[i] = _mm_or_si128(a.lanes[i], b.lanes[i]);
# endif
}
return res;
}
u8_base &operator|=(u8_base b)
{
*this = *this | b;
return *this;
}
friend u8_base operator&(u8_base a, u8_base b)
{
u8_base res;
for (int i = 0; i < Size; ++i) {
# if defined(USE_NEON)
res.lanes[i] = vandq_u8(a.lanes[i], b.lanes[i]);
# elif defined(USE_SSE4_2)
res.lanes[i] = _mm_and_si128(a.lanes[i], b.lanes[i]);
# endif
}
return res;
}
friend u8_base operator&(u8_base a, uint8_t b)
{
# if defined(USE_NEON)
uint8x16_t ref = vdupq_n_u8(b);
# elif defined(USE_SSE4_2)
__m128i ref = _mm_set1_epi8(b);
# endif
u8_base res;
for (int i = 0; i < Size; ++i) {
# if defined(USE_NEON)
res.lanes[i] = vandq_u8(a.lanes[i], ref);
# elif defined(USE_SSE4_2)
res.lanes[i] = _mm_and_si128(a.lanes[i], ref);
# endif
}
return res;
}
u8_base operator~() const
{
u8_base res;
for (int i = 0; i < Size; ++i) {
# if defined(USE_NEON)
res.lanes[i] = vmvnq_u8(lanes[i]);
# elif defined(USE_SSE4_2)
res.lanes[i] = _mm_xor_si128(lanes[i], _mm_set1_epi8(-1));
# endif
}
return res;
}
/* --- Arithmetic Operators --- */
friend u8_base operator+(u8_base a, u8_base b)
{
u8_base res;
for (int i = 0; i < Size; ++i) {
# if defined(USE_NEON)
res.lanes[i] = vaddq_u8(a.lanes[i], b.lanes[i]);
# elif defined(USE_SSE4_2)
res.lanes[i] = _mm_add_epi8(a.lanes[i], b.lanes[i]);
# endif
}
return res;
}
friend u8_base operator+(u8_base a, uint8_t b)
{
# if defined(USE_NEON)
uint8x16_t ref = vdupq_n_u8(b);
# elif defined(USE_SSE4_2)
__m128i ref = _mm_set1_epi8(b);
# endif
u8_base res;
for (int i = 0; i < Size; ++i) {
# if defined(USE_NEON)
res.lanes[i] = vaddq_u8(a.lanes[i], ref);
# elif defined(USE_SSE4_2)
res.lanes[i] = _mm_add_epi8(a.lanes[i], ref);
# endif
}
return res;
}
friend u8_base operator-(u8_base a, u8_base b)
{
u8_base res;
for (int i = 0; i < Size; ++i) {
# if defined(USE_NEON)
res.lanes[i] = vsubq_u8(a.lanes[i], b.lanes[i]);
# elif defined(USE_SSE4_2)
res.lanes[i] = _mm_sub_epi8(a.lanes[i], b.lanes[i]);
# endif
}
return res;
}
/* --- Comparison Operators --- */
/** WARNING: Signed comparison on SSE. Will not work for input greater than 127. */
friend u8_base operator>(u8_base a, u8_base b)
{
u8_base res;
for (int i = 0; i < Size; ++i) {
# if defined(USE_NEON)
res.lanes[i] = vcgtq_u8(a.lanes[i], b.lanes[i]);
# elif defined(USE_SSE4_2)
res.lanes[i] = _mm_cmpgt_epi8(a.lanes[i], b.lanes[i]);
# endif
}
return res;
}
/** WARNING: Signed comparison on SSE. Will not work for input greater than 127. */
friend u8_base operator<(u8_base a, u8_base b)
{
u8_base res;
for (int i = 0; i < Size; ++i) {
# if defined(USE_NEON)
res.lanes[i] = vcltq_u8(a.lanes[i], b.lanes[i]);
# elif defined(USE_SSE4_2)
res.lanes[i] = _mm_cmplt_epi8(a.lanes[i], b.lanes[i]);
# endif
}
return res;
}
/** WARNING: Signed comparison on SSE. Will not work for input greater than 127. */
friend u8_base operator<(u8_base a, uint8_t b)
{
# if defined(USE_NEON)
uint8x16_t ref = vdupq_n_u8(b);
# elif defined(USE_SSE4_2)
__m128i ref = _mm_set1_epi8(b);
# endif
u8_base res;
for (int i = 0; i < Size; ++i) {
# if defined(USE_NEON)
res.lanes[i] = vcltq_u8(a.lanes[i], ref);
# elif defined(USE_SSE4_2)
res.lanes[i] = _mm_cmplt_epi8(a.lanes[i], ref);
# endif
}
return res;
}
friend u8_base operator==(u8_base a, uint8_t b)
{
# if defined(USE_NEON)
uint8x16_t ref = vdupq_n_u8(b);
# elif defined(USE_SSE4_2)
__m128i ref = _mm_set1_epi8(b);
# endif
u8_base res;
for (int i = 0; i < Size; ++i) {
# if defined(USE_NEON)
res.lanes[i] = vceqq_u8(a.lanes[i], ref);
# elif defined(USE_SSE4_2)
res.lanes[i] = _mm_cmpeq_epi8(a.lanes[i], ref);
# endif
}
return res;
}
};
using u8x16 = u8_base<1>;
using u8x32 = u8_base<2>;
using u8x64 = u8_base<4>;
struct u8x16_table {
u8x16 table;
u8x16_table() = default;
u8x16_table(u8x16 table) : table(table) {}
static u8x16_table load_unaligned(const uint8_t *src)
{
u8x16_table table;
table.table = u8x16::load_unaligned(src);
return table;
}
static u8x16_table load(const uint8_t *src)
{
u8x16_table table;
table.table = u8x16::load(src);
return table;
}
template<int Size> u8_base<Size> operator[](u8_base<Size> index) const
{
u8_base<Size> result;
for (int i = 0; i < Size; ++i) {
# if defined(USE_NEON)
result.lanes[i] = vqtbl1q_u8(table.lanes[0], index.lanes[i]);
# elif defined(USE_SSE4_2)
/* Make sure to mimic the NEON behavior and return 0 on overflow.
* _mm_shuffle_epi8 will only return zero if the MSB is high. */
__m128i out_of_range = _mm_cmpgt_epi8(index.lanes[i], _mm_set1_epi8(15));
__m128i safe_indices = _mm_or_si128(index.lanes[i], out_of_range);
result.lanes[i] = _mm_shuffle_epi8(table.lanes[0], safe_indices);
# endif
}
return result;
}
/* Only valid if all indices are less than 16. */
template<int Size> u8_base<Size> unsafe_shuffle(u8_base<Size> index) const
{
u8_base<Size> result;
for (int i = 0; i < Size; ++i) {
# if defined(USE_NEON)
result.lanes[i] = vqtbl1q_u8(table.lanes[0], index.lanes[i]);
# elif defined(USE_SSE4_2)
result.lanes[i] = _mm_shuffle_epi8(table.lanes[0], index.lanes[i]);
# endif
}
return result;
}
};
struct u8x64_table {
# if defined(USE_NEON)
uint8x16x4_t table;
# elif defined(USE_SSE4_2)
u8x16_table tables[4];
# endif
u8x64_table() = default;
# if defined(USE_NEON)
u8x64_table(u8x64 table)
: table({table.lanes[0], table.lanes[1], table.lanes[2], table.lanes[3]})
{
}
# elif defined(USE_SSE4_2)
u8x64_table(u8x64 table)
{
tables[0].table = table.lane(0);
tables[1].table = table.lane(1);
tables[2].table = table.lane(2);
tables[3].table = table.lane(3);
}
# endif
static u8x64_table load_unaligned(const uint8_t *src)
{
u8x64_table table;
# if defined(USE_NEON)
table.table = vld1q_u8_x4(src);
# elif defined(USE_SSE4_2)
for (int i = 0; i < 4; ++i) {
table.tables[i] = u8x16_table::load_unaligned(src + i * 16);
}
# endif
return table;
}
static u8x64_table load(const uint8_t *src)
{
assert((intptr_t(src) & 63) == 0);
u8x64_table table;
# if defined(USE_NEON)
table.table = vld1q_u8_x4(src);
# elif defined(USE_SSE4_2)
for (int i = 0; i < 4; ++i) {
table.tables[i] = u8x16_table::load(src + i * 16);
}
# endif
return table;
}
template<int Size> u8_base<Size> operator[](u8_base<Size> index) const
{
u8_base<Size> result;
# if defined(USE_NEON)
for (int i = 0; i < Size; ++i) {
result.lanes[i] = vqtbl4q_u8(table, index.lanes[i]);
}
# elif defined(USE_SSE4_2)
result = tables[0][index];
result |= tables[1][index ^ u8_base<Size>(0x10)];
result |= tables[2][index ^ u8_base<Size>(0x20)];
result |= tables[3][index ^ u8_base<Size>(0x30)];
# endif
return result;
}
};
struct u8x128_table {
u8x64_table tables[2];
static u8x128_table load_unaligned(const uint8_t *src)
{
u8x128_table table;
for (int i = 0; i < 2; ++i) {
table.tables[i] = u8x64_table::load_unaligned(src + i * 64);
}
return table;
}
static u8x128_table load(const uint8_t *src)
{
u8x128_table table;
for (int i = 0; i < 2; ++i) {
table.tables[i] = u8x64_table::load(src + i * 64);
}
return table;
}
/** Perform a 128 bytes table lookup for each lane of the input vector. */
template<int Size> u8_base<Size> operator[](u8_base<Size> index) const
{
/* https://lemire.me/blog/2019/07/23/arbitrary-byte-to-byte-maps-using-arm-neon/
* Table lookup on NEON will return 0 on overflow. Leverage this using XOR to swap which range
* we are looking up and combine result using OR.
* Note we make sure that SSE lookup have the same behavior Which is more costly
* (more than 3x the number of instructions) it is then preferable to avoid this path. */
return tables[0][index] | tables[1][index ^ u8_base<Size>(0x40)];
}
};
/* Select A if mask is 0, B otherwise.
* Mask is expected to be 0xFF or 0x00 for each component. */
template<int Size>
inline u8_base<Size> select(u8_base<Size> a, u8_base<Size> b, u8_base<Size> mask)
{
u8_base<Size> result;
for (int i = 0; i < Size; ++i) {
# if defined(USE_NEON)
result.lanes[i] = vbslq_u8(mask.lanes[i], b.lanes[i], a.lanes[i]);
# elif defined(USE_SSE4_2)
result.lanes[i] = _mm_blendv_epi8(a.lanes[i], b.lanes[i], mask.lanes[i]);
# endif
}
return result;
}
/* fill_value is the lanes to shift in. */
template<int Shift, int Size>
inline u8_base<Size> shift_lanes_right(u8_base<Size> a, uint8_t fill_value)
{
u8_base<Size> result;
for (int i = Size - 1; i > 0; --i) {
# if defined(USE_NEON)
result.lanes[i] = vextq_u8(a.lanes[i - 1], a.lanes[i], 16 - Shift);
# elif defined(USE_SSE4_2)
result.lanes[i] = _mm_alignr_epi8(a.lanes[i], a.lanes[i - 1], 16 - Shift);
# endif
}
u8_base<1> fill{fill_value};
# if defined(USE_NEON)
result.lanes[0] = vextq_u8(fill.lanes[0], a.lanes[0], 16 - Shift);
# elif defined(USE_SSE4_2)
result.lanes[0] = _mm_alignr_epi8(a.lanes[0], fill.lanes[0], 16 - Shift);
# endif
return result;
}
template<int Size> inline u8_base<Size> is_zero(u8_base<Size> a)
{
# if defined(USE_SSE4_2)
__m128i zero = _mm_setzero_si128();
# endif
u8_base<Size> result;
for (int i = 0; i < Size; ++i) {
# if defined(USE_NEON)
result.lanes[i] = vceqzq_u8(a.lanes[i]);
# elif defined(USE_SSE4_2)
result.lanes[i] = _mm_cmpeq_epi8(a.lanes[i], zero);
# endif
}
return result;
}
/* Create a bitmask from a u8x64 mask containing 0xFF or 0x00 inside each lane. */
inline uint64_t movemask(u8x64 mask)
{
uint64_t result;
# if defined(USE_NEON)
const uint8x16_t bits = {1, 2, 4, 8, 16, 32, 64, 128, 1, 2, 4, 8, 16, 32, 64, 128};
/* Merge lanes with their neighbors (e.g. [1, 2, 4, 8, ...] > [3, 12, 48, ...]).
* This is equivalent to merging using | and >> operations.
* Doing it 3 time to collapse the 8 bits. */
uint8x16_t sum0 = vpaddq_u8(vandq_u8(mask.lanes[0], bits), vandq_u8(mask.lanes[1], bits));
uint8x16_t sum1 = vpaddq_u8(vandq_u8(mask.lanes[2], bits), vandq_u8(mask.lanes[3], bits));
sum0 = vpaddq_u8(sum0, sum1);
sum0 = vpaddq_u8(sum0, sum0);
result = vgetq_lane_u64(vreinterpretq_u64_u8(sum0), 0);
# elif defined(USE_SSE4_2)
result = _mm_movemask_epi8(mask.lanes[3]);
result = _mm_movemask_epi8(mask.lanes[2]) | (result << 16);
result = _mm_movemask_epi8(mask.lanes[1]) | (result << 16);
result = _mm_movemask_epi8(mask.lanes[0]) | (result << 16);
# endif
return result;
}
/* Create a bitmask from a u8x32 mask containing 0xFF or 0x00 inside each lane. */
inline uint32_t movemask(u8x32 mask)
{
uint32_t result;
# if defined(USE_NEON)
const uint8x16_t bits = {1, 2, 4, 8, 16, 32, 64, 128, 1, 2, 4, 8, 16, 32, 64, 128};
/* Merge lanes with their neighbors (e.g. [1, 2, 4, 8, ...] > [3, 12, 48, ...]).
* This is equivalent to merging using | and >> operations.
* Doing it 3 time to collapse the 8 bits. */
uint8x16_t sum = vpaddq_u8(vandq_u8(mask.lanes[0], bits), vandq_u8(mask.lanes[1], bits));
sum = vpaddq_u8(sum, sum);
sum = vpaddq_u8(sum, sum);
result = vgetq_lane_u64(vreinterpretq_u64_u8(sum), 0);
# elif defined(USE_SSE4_2)
result = _mm_movemask_epi8(mask.lanes[1]);
result = _mm_movemask_epi8(mask.lanes[0]) | (result << 16);
# endif
return result;
}
/* Create a bitmask from a u8x64 mask containing 0xFF or 0x00 inside each lane. */
inline uint16_t movemask(u8x16 mask)
{
uint16_t result;
# if defined(USE_NEON)
const uint8x16_t bits = {1, 2, 4, 8, 16, 32, 64, 128, 1, 2, 4, 8, 16, 32, 64, 128};
uint8x16_t sum = vandq_u8(mask.lanes[0], bits);
/* Merge lanes with their neighbors (e.g. [1, 2, 4, 8, ...] > [3, 12, 48, ...]).
* This is equivalent to merging using | and >> operations.
* Doing it 3 time to collapse the 8 bits. */
sum = vpaddq_u8(sum, sum);
sum = vpaddq_u8(sum, sum);
sum = vpaddq_u8(sum, sum);
result = vgetq_lane_u64(vreinterpretq_u64_u8(sum), 0);
# elif defined(USE_SSE4_2)
result = _mm_movemask_epi8(mask.lanes[0]);
# endif
return result;
}
/* Size must be power of 2. */
template<int Size> struct u16_base {
# if defined(USE_NEON)
uint16x8_t lanes[Size];
# elif defined(USE_SSE4_2)
__m128i lanes[Size];
# endif
u16_base() = default;
explicit u16_base(const u8_base<Size / 2> v)
{
# if defined(USE_SSE4_2)
__m128i zero = _mm_setzero_si128();
# endif
for (int i = 0; i < Size / 2; ++i) {
# if defined(USE_NEON)
lanes[i * 2 + 0] = vmovl_u8(vget_low_u8(v.lanes[i]));
lanes[i * 2 + 1] = vmovl_u8(vget_high_u8(v.lanes[i]));
# elif defined(USE_SSE4_2)
lanes[i * 2 + 0] = _mm_unpacklo_epi8(v.lanes[i], zero);
lanes[i * 2 + 1] = _mm_unpackhi_epi8(v.lanes[i], zero);
# endif
}
}
void store(uint16_t *dst) const
{
for (int i = 0; i < Size; ++i) {
# if defined(USE_NEON)
vst1q_u16(dst + i * 8, lanes[i]);
# elif defined(USE_SSE4_2)
_mm_storeu_si128((__m128i *)dst + i, lanes[i]);
# endif
}
}
};
using u16x16 = u16_base<2>;
using u16x64 = u16_base<8>;
/* Size must be power of 2. */
template<int Size> struct u32_base {
# if defined(USE_NEON)
uint32x4_t lanes[Size];
# elif defined(USE_SSE4_2)
__m128i lanes[Size];
# endif
u32_base() = default;
explicit u32_base(const u8_base<Size / 4> v)
{
for (int i = 0; i < Size / 4; ++i) {
# if defined(USE_NEON)
uint16x8_t tmp_lo = vmovl_u8(vget_low_u8(v.lanes[i]));
uint16x8_t tmp_hi = vmovl_u8(vget_high_u8(v.lanes[i]));
lanes[i * 4 + 0] = vmovl_u16(vget_low_u16(tmp_lo));
lanes[i * 4 + 1] = vmovl_u16(vget_high_u16(tmp_lo));
lanes[i * 4 + 2] = vmovl_u16(vget_low_u16(tmp_hi));
lanes[i * 4 + 3] = vmovl_u16(vget_high_u16(tmp_hi));
# elif defined(USE_SSE4_2)
lanes[i * 4 + 0] = _mm_cvtepu8_epi32(v.lanes[i]);
lanes[i * 4 + 1] = _mm_cvtepu8_epi32(_mm_srli_si128(v.lanes[i], 4));
lanes[i * 4 + 2] = _mm_cvtepu8_epi32(_mm_srli_si128(v.lanes[i], 8));
lanes[i * 4 + 3] = _mm_cvtepu8_epi32(_mm_srli_si128(v.lanes[i], 12));
# endif
}
}
/* NOTE: This does a signed saturation on SSE. */
explicit operator u8_base<Size / 4>() const
{
u8_base<Size / 4> res;
for (int i = 0; i < Size / 4; ++i) {
# if defined(USE_NEON)
uint16x4_t n0 = vmovn_u32(lanes[i * 4 + 0]);
uint16x4_t n1 = vmovn_u32(lanes[i * 4 + 1]);
uint16x4_t n2 = vmovn_u32(lanes[i * 4 + 2]);
uint16x4_t n3 = vmovn_u32(lanes[i * 4 + 3]);
uint16x8_t q01 = vcombine_u16(n0, n1);
uint16x8_t q23 = vcombine_u16(n2, n3);
res.lanes[i] = vcombine_u8(vmovn_u16(q01), vmovn_u16(q23));
# elif defined(USE_SSE4_2)
__m128i pack_16_lo = _mm_packs_epi32(lanes[i * 4 + 0], lanes[i * 4 + 1]);
__m128i pack_16_hi = _mm_packs_epi32(lanes[i * 4 + 2], lanes[i * 4 + 3]);
res.lanes[i] = _mm_packs_epi16(pack_16_lo, pack_16_hi);
# endif
}
return res;
}
static u32_base load_unaligned(const uint32_t *src)
{
u32_base result;
for (int i = 0; i < Size; ++i) {
# if defined(USE_NEON)
result.lanes[i] = vld1q_u32(src + i * 4);
# elif defined(USE_SSE4_2)
result.lanes[i] = _mm_loadu_si128((const __m128i *)src + i);
# endif
}
return result;
}
static u32_base load(const uint32_t *src)
{
assert((intptr_t(src) & (16 - 1)) == 0);
u32_base result;
for (int i = 0; i < Size; ++i) {
# if defined(USE_NEON)
result.lanes[i] = vld1q_u32(src + i * 4);
# elif defined(USE_SSE4_2)
result.lanes[i] = _mm_load_si128((const __m128i *)src + i);
# endif
}
return result;
}
void store_unaligned(uint32_t *dst) const
{
for (int i = 0; i < Size; ++i) {
# if defined(USE_NEON)
vst1q_u32(dst + i * 4, lanes[i]);
# elif defined(USE_SSE4_2)
_mm_storeu_si128((__m128i *)dst + i, lanes[i]);
# endif
}
}
void store(uint32_t *dst) const
{
assert((intptr_t(dst) & (16 - 1)) == 0);
for (int i = 0; i < Size; ++i) {
# if defined(USE_NEON)
vst1q_u32(dst + i * 4, lanes[i]);
# elif defined(USE_SSE4_2)
_mm_store_si128((__m128i *)dst + i, lanes[i]);
# endif
}
}
/* --- Arithmetic Operators --- */
/* Note: Signed comparison on SSE. */
friend u32_base operator<(u32_base a, u32_base b)
{
u32_base res;
for (int i = 0; i < Size; ++i) {
# if defined(USE_NEON)
res.lanes[i] = vcltq_u32(a.lanes[i], b.lanes[i]);
# elif defined(USE_SSE4_2)
res.lanes[i] = _mm_cmplt_epi32(a.lanes[i], b.lanes[i]);
# endif
}
return res;
}
/* Note: Signed comparison on SSE. */
friend u32_base operator<(u32_base a, uint32_t b)
{
# if defined(USE_NEON)
uint32x4_t tmp = vdupq_n_u32(b);
# elif defined(USE_SSE4_2)
__m128i tmp = _mm_set1_epi32(b);
# endif
u32_base res;
for (int i = 0; i < Size; ++i) {
# if defined(USE_NEON)
res.lanes[i] = vcltq_u32(a.lanes[i], tmp);
# elif defined(USE_SSE4_2)
res.lanes[i] = _mm_cmplt_epi32(a.lanes[i], tmp);
# endif
}
return res;
}
friend u32_base operator>(u32_base a, uint32_t b)
{
# if defined(USE_NEON)
uint32x4_t tmp = vdupq_n_u32(b);
# elif defined(USE_SSE4_2)
__m128i tmp = _mm_set1_epi32(b);
# endif
u32_base res;
for (int i = 0; i < Size; ++i) {
# if defined(USE_NEON)
res.lanes[i] = vcgtq_u32(a.lanes[i], tmp);
# elif defined(USE_SSE4_2)
res.lanes[i] = _mm_cmpgt_epi32(a.lanes[i], tmp);
# endif
}
return res;
}
friend u32_base operator+(u32_base a, uint32_t b)
{
# if defined(USE_NEON)
uint32x4_t tmp = vdupq_n_u32(b);
# elif defined(USE_SSE4_2)
__m128i tmp = _mm_set1_epi32(b);
# endif
u32_base res;
for (int i = 0; i < Size; ++i) {
# if defined(USE_NEON)
res.lanes[i] = vaddq_u32(a.lanes[i], tmp);
# elif defined(USE_SSE4_2)
res.lanes[i] = _mm_add_epi32(a.lanes[i], tmp);
# endif
}
return res;
}
friend u32_base operator-(u32_base a, u32_base b)
{
u32_base res;
for (int i = 0; i < Size; ++i) {
# if defined(USE_NEON)
res.lanes[i] = vsubq_u32(a.lanes[i], b.lanes[i]);
# elif defined(USE_SSE4_2)
res.lanes[i] = _mm_sub_epi32(a.lanes[i], b.lanes[i]);
# endif
}
return res;
}
};
using u32x16 = u32_base<4>;
using u32x32 = u32_base<8>;
using u32x64 = u32_base<16>;
} // namespace lexit::simd
#endif

View File

@@ -0,0 +1,146 @@
/* SPDX-FileCopyrightText: 2026 Clement Foucault
*
* SPDX-License-Identifier: MIT */
#pragma once
#include "types.hh"
namespace lexit {
/* Associates a token type to each ASCII character. */
alignas(128) const CharClass char_class_table[128] = {
/* 0x00, '\0' */ CharClass::None,
/* 0x01, SOH */ CharClass::None,
/* 0x02, STX */ CharClass::None,
/* 0x03, ETX */ CharClass::None,
/* 0x04, EOT */ CharClass::None,
/* 0x05, ENQ */ CharClass::None,
/* 0x06, ACK */ CharClass::None,
/* 0x07, '\a' */ CharClass::None,
/* 0x08, '\b' */ CharClass::None,
/* 0x09, '\t' */ CharClass::WhiteSpace,
/* 0x0A, '\n' */ CharClass::Separator,
/* 0x0B, '\v' */ CharClass::None,
/* 0x0C, '\f' */ CharClass::None,
/* 0x0D, '\r' */ CharClass::Separator,
/* 0x0E, SO */ CharClass::None,
/* 0x0F, SI */ CharClass::None,
/* 0x10, DLE */ CharClass::None,
/* 0x11, DC1 */ CharClass::None,
/* 0x12, DC2 */ CharClass::None,
/* 0x13, DC3 */ CharClass::None,
/* 0x14, DC4 */ CharClass::None,
/* 0x15, NAK */ CharClass::None,
/* 0x16, SYN */ CharClass::None,
/* 0x17, ETB */ CharClass::None,
/* 0x18, CAN */ CharClass::None,
/* 0x19, EM */ CharClass::None,
/* 0x1A, SUB */ CharClass::None,
/* 0x1B, ESC */ CharClass::None,
/* 0x1C, FS */ CharClass::None,
/* 0x1D, GS */ CharClass::None,
/* 0x1E, RS */ CharClass::None,
/* 0x1F, US */ CharClass::None,
/* 0x20, ' ' */ CharClass::WhiteSpace,
/* 0x21, '!' */ CharClass::MultiTok,
/* 0x22, '"' */ CharClass::Separator,
/* 0x23, '#' */ CharClass::Separator,
/* 0x24, '$' */ CharClass::Separator,
/* 0x25, '%' */ CharClass::MultiTok,
/* 0x26, '&' */ CharClass::MultiTok,
/* 0x27, '\'' */ CharClass::Separator,
/* 0x28, '(' */ CharClass::Separator,
/* 0x29, ')' */ CharClass::Separator,
/* 0x2A, '*' */ CharClass::MultiTok,
/* 0x2B, '+' */ CharClass::MultiTok,
/* 0x2C, ',' */ CharClass::Separator,
/* 0x2D, '-' */ CharClass::MultiTok,
/* 0x2E, '.' */ CharClass::MultiTok,
/* 0x2F, '/' */ CharClass::MultiTok,
/* 0x30, '0' */ CharClass::Numeric,
/* 0x31, '1' */ CharClass::Numeric,
/* 0x32, '2' */ CharClass::Numeric,
/* 0x33, '3' */ CharClass::Numeric,
/* 0x34, '4' */ CharClass::Numeric,
/* 0x35, '5' */ CharClass::Numeric,
/* 0x36, '6' */ CharClass::Numeric,
/* 0x37, '7' */ CharClass::Numeric,
/* 0x38, '8' */ CharClass::Numeric,
/* 0x39, '9' */ CharClass::Numeric,
/* 0x3A, ':' */ CharClass::Separator,
/* 0x3B, ';' */ CharClass::Separator,
/* 0x3C, '<' */ CharClass::MultiTok,
/* 0x3D, '=' */ CharClass::MultiTok,
/* 0x3E, '>' */ CharClass::MultiTok,
/* 0x3F, '?' */ CharClass::Separator,
/* 0x40, '@' */ CharClass::Separator,
/* 0x41, 'A' */ CharClass::Alpha,
/* 0x42, 'B' */ CharClass::Alpha,
/* 0x43, 'C' */ CharClass::Alpha,
/* 0x44, 'D' */ CharClass::Alpha,
/* 0x45, 'E' */ CharClass::Alpha,
/* 0x46, 'F' */ CharClass::Alpha,
/* 0x47, 'G' */ CharClass::Alpha,
/* 0x48, 'H' */ CharClass::Alpha,
/* 0x49, 'I' */ CharClass::Alpha,
/* 0x4A, 'J' */ CharClass::Alpha,
/* 0x4B, 'K' */ CharClass::Alpha,
/* 0x4C, 'L' */ CharClass::Alpha,
/* 0x4D, 'M' */ CharClass::Alpha,
/* 0x4E, 'N' */ CharClass::Alpha,
/* 0x4F, 'O' */ CharClass::Alpha,
/* 0x50, 'P' */ CharClass::Alpha,
/* 0x51, 'Q' */ CharClass::Alpha,
/* 0x52, 'R' */ CharClass::Alpha,
/* 0x53, 'S' */ CharClass::Alpha,
/* 0x54, 'T' */ CharClass::Alpha,
/* 0x55, 'U' */ CharClass::Alpha,
/* 0x56, 'V' */ CharClass::Alpha,
/* 0x57, 'W' */ CharClass::Alpha,
/* 0x58, 'X' */ CharClass::Alpha,
/* 0x59, 'Y' */ CharClass::Alpha,
/* 0x5A, 'Z' */ CharClass::Alpha,
/* 0x5B, '[' */ CharClass::Separator,
/* 0x5C, '\\' */ CharClass::Separator,
/* 0x5D, ']' */ CharClass::Separator,
/* 0x5E, '^' */ CharClass::MultiTok,
/* 0x5F, '_' */ CharClass::Alpha, /* Can also start identifiers. */
/* 0x60, '`' */ CharClass::Separator,
/* 0x61, 'a' */ CharClass::Alpha,
/* 0x62, 'b' */ CharClass::Alpha,
/* 0x63, 'c' */ CharClass::Alpha,
/* 0x64, 'd' */ CharClass::Alpha,
/* 0x65, 'e' */ CharClass::Alpha,
/* 0x66, 'f' */ CharClass::Alpha,
/* 0x67, 'g' */ CharClass::Alpha,
/* 0x68, 'h' */ CharClass::Alpha,
/* 0x69, 'i' */ CharClass::Alpha,
/* 0x6A, 'j' */ CharClass::Alpha,
/* 0x6B, 'k' */ CharClass::Alpha,
/* 0x6C, 'l' */ CharClass::Alpha,
/* 0x6D, 'm' */ CharClass::Alpha,
/* 0x6E, 'n' */ CharClass::Alpha,
/* 0x6F, 'o' */ CharClass::Alpha,
/* 0x70, 'p' */ CharClass::Alpha,
/* 0x71, 'q' */ CharClass::Alpha,
/* 0x72, 'r' */ CharClass::Alpha,
/* 0x73, 's' */ CharClass::Alpha,
/* 0x74, 't' */ CharClass::Alpha,
/* 0x75, 'u' */ CharClass::Alpha,
/* 0x76, 'v' */ CharClass::Alpha,
/* 0x77, 'w' */ CharClass::Alpha,
/* 0x78, 'x' */ CharClass::Alpha,
/* 0x79, 'y' */ CharClass::Alpha,
/* 0x7A, 'z' */ CharClass::Alpha,
/* 0x7B, '{' */ CharClass::Separator,
/* 0x7C, '|' */ CharClass::MultiTok,
/* 0x7D, '}' */ CharClass::Separator,
/* 0x7E, '~' */ CharClass::Separator,
/* 0x7F, Del */ CharClass::None,
};
} // namespace lexit

View File

@@ -0,0 +1,161 @@
/* SPDX-FileCopyrightText: 2026 Clement Foucault
*
* SPDX-License-Identifier: MIT */
#pragma once
#include <cstdint>
namespace lexit {
/**
* Class for each characters inside the ASCII table.
*
* The tokenizer identifies runs of characters with similar classes.
* A character is grouped with its predecessor if it shares a class.
* The Separator class is the exception which never group chars together.
*
* Note: The values were chosen to allow fast comparison, masking, and cast to printable TokenType.
*/
enum class CharClass : uint8_t {
/* Will decay into single char token. */
None = 0,
/* Will decay into single char of the token. */
Separator = (1 << 1),
/* Will decay into the first char of the token. */
MultiTok = (1 << 2),
WhiteSpace = (1 << 3),
/* Will decay into Word. Can start an identifier. */
Alpha = 'A', /* 0b01000001 */
/* Will decay into Number. Can continue an identifier. */
Numeric = '1', /* 0b00110001 */
/* These classes will merge characters together. */
CanMerge = Alpha | Numeric | MultiTok | WhiteSpace,
/* Classes above this value will cast to TokenType instead of using the character. */
ClassToTypeThreshold = Numeric - 1,
};
/* Make sure to declare this enum as being a char.
* This is allow casting to string possible. */
enum TokenType : uint8_t {
Invalid = 0,
Word = TokenType(CharClass::Alpha),
Number = TokenType(CharClass::Numeric),
/* Use printable ascii chars to store them in string, and for easy debugging / testing. */
NewLine = '\n',
Space = ' ',
Dot = '.',
Hash = '#',
Ampersand = '&',
DoubleQuote = '"',
SingleQuote = '\'',
ParOpen = '(',
ParClose = ')',
BracketOpen = '{',
BracketClose = '}',
SquareOpen = '[',
SquareClose = ']',
AngleOpen = '<',
AngleClose = '>',
Assign = '=',
SemiColon = ';',
Question = '?',
Not = '!',
Colon = ':',
Comma = ',',
Star = '*',
Plus = '+',
Minus = '-',
Divide = '/',
Tilde = '~',
Caret = '^',
Pipe = '|',
Percent = '%',
Backslash = '\\',
/* Mark end of stream. */
EndOfFile = '\0',
/* --- Keywords --- */
LogicalAnd = 'a',
// Word = 'A',
Break = 'b',
// Unused = 'B',
Const = 'c',
Constexpr = 'C',
Do = 'd',
Decrement = 'D',
NotEqual = 'e',
Equal = 'E',
For = 'f',
While = 'F',
LogicalOr = 'g',
GEqual = 'G',
Switch = 'h',
Case = 'H',
If = 'i',
Else = 'I',
Elif = 'j',
Endif = 'J',
Ifdef = 'k',
Ifndef = 'K',
Inline = 'l',
LEqual = 'L',
Static = 'm',
Enum = 'M',
Namespace = 'n',
Define = 'N',
Union = 'o',
Continue = 'O',
Line = 'p',
Increment = 'P',
Pragma = 'q',
DoubleHash = 'Q',
Return = 'r',
// Unused = 'R',
Struct = 's',
Class = 'S',
Template = 't',
This = 'T',
Using = 'u',
Undef = 'U',
Private = 'v',
Public = 'V',
// Unused = 'w',
// Unused = 'W',
// Unused = 'x',
// Unused = 'X',
// Unused = 'y',
TemplateOpen = 'Y',
TemplateClose = 'z',
Comment = 'Z',
// Number = '0',
// Unused = '1',
// Unused = '2',
// Unused = '3',
// Unused = '4',
// Unused = '5',
// Unused = '6',
// Unused = '7',
// Unused = '8',
// Unused = '9',
/* Aliases. */
Multiply = Star,
And = Ampersand,
Or = Pipe,
Xor = Caret,
GThan = AngleClose,
LThan = AngleOpen,
BitwiseNot = Tilde,
Modulo = Percent,
String = DoubleQuote,
Slash = Divide,
};
/* Unique identifier to a word token. */
using TokenAtom = uint16_t;
} // namespace lexit

View File

@@ -0,0 +1,145 @@
/* SPDX-FileCopyrightText: 2026 Clement Foucault
*
* SPDX-License-Identifier: MIT */
#pragma once
#include <cassert>
#include <cstring>
#include <memory>
#ifndef NDEBUG
# include <span>
#endif
namespace lexit {
template<typename T> class AlignedArrayPtr {
private:
T *ptr = nullptr;
public:
AlignedArrayPtr() = default;
AlignedArrayPtr(int size)
{
ptr = static_cast<T *>(operator new[](sizeof(T) * size, std::align_val_t{64}));
}
AlignedArrayPtr(const AlignedArrayPtr &other) = delete;
AlignedArrayPtr(AlignedArrayPtr &&other) : ptr(other.ptr)
{
other.ptr = nullptr;
}
~AlignedArrayPtr()
{
operator delete[](ptr, std::align_val_t{64});
}
AlignedArrayPtr &operator=(AlignedArrayPtr &&other)
{
if (this != &other) {
operator delete[](ptr, std::align_val_t{64});
ptr = other.ptr;
other.ptr = nullptr;
}
return *this;
}
T *get()
{
return ptr;
}
const T *get() const
{
return ptr;
}
T &operator[](int i)
{
return ptr[i];
}
const T &operator[](int i) const
{
return ptr[i];
}
};
template<typename T> struct Vector {
private:
std::unique_ptr<T[]> data_;
int size_ = 0;
int alloc_size_ = 0;
#ifndef NDEBUG
std::span<T> debug_view_;
#endif
public:
void reserve(int new_size)
{
if ((alloc_size_ >= new_size) && data_) {
return;
}
std::unique_ptr<T[]> new_ptr(new T[new_size]);
if (data_) {
std::memcpy(new_ptr.get(), data_.get(), alloc_size_ * sizeof(T));
}
data_ = std::move(new_ptr);
alloc_size_ = new_size;
#ifndef NDEBUG
debug_view_ = std::span<T>(data_.get(), size_);
#endif
}
void resize(int new_size)
{
reserve(new_size);
size_ = new_size;
#ifndef NDEBUG
debug_view_ = std::span<T>(data_.get(), size_);
#endif
}
T *data()
{
return data_.get();
}
const T *data() const
{
return data_.get();
}
T &operator[](int i)
{
return data_.get()[i];
}
const T &operator[](int i) const
{
return data_.get()[i];
}
T *end()
{
return data_.get() + size_;
}
const T *end() const
{
return data_.get() + size_;
}
int size() const
{
return size_;
}
void increase_size_by_unchecked(int elem_count)
{
assert(size_ + elem_count <= alloc_size_);
size_ += elem_count;
#ifndef NDEBUG
debug_view_ = std::span<T>(data_.get(), size_);
#endif
}
};
} // namespace lexit

View File

@@ -0,0 +1,277 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup shader_tool
*/
#include "metadata.hh"
namespace blender::gpu::shader::metadata {
std::string ParsedResource::serialize() const
{
std::string res_condition_lambda;
if (!res_condition.empty()) {
res_condition_lambda = ", [](blender::Span<CompilationConstant> constants) { ";
res_condition_lambda += res_condition;
res_condition_lambda += "}";
}
std::stringstream ss;
if (res_type == "legacy_info") {
ss << "ADDITIONAL_INFO(" << var_name << ")";
}
else if (res_type == "resource_table") {
if (!res_condition.empty()) {
ss << ".additional_info_with_condition(\"" << var_type << "\"" << res_condition_lambda
<< ")";
}
else {
ss << ".additional_info(\"" << var_type << "\")";
}
}
else if (res_type == "sampler") {
ss << ".sampler(" << res_slot;
ss << ", ImageType::" << var_type;
ss << ", \"" << var_name << "\"";
ss << ", Frequency::" << res_frequency;
ss << ", GPUSamplerState::internal_sampler()";
ss << res_condition_lambda << ")";
}
else if (res_type == "image") {
ss << ".image(" << res_slot;
ss << ", blender::gpu::TextureFormat::" << res_format;
ss << ", Qualifier::" << res_qualifier;
ss << ", ImageReadWriteType::" << var_type;
ss << ", \"" << var_name << "\"";
ss << ", Frequency::" << res_frequency;
ss << res_condition_lambda << ")";
}
else if (res_type == "uniform") {
ss << ".uniform_buf(" << res_slot;
ss << ", \"" << var_type << "\"";
ss << ", \"" << var_name << var_array << "\"";
ss << ", Frequency::" << res_frequency;
ss << res_condition_lambda << ")";
}
else if (res_type == "storage") {
ss << ".storage_buf(" << res_slot;
ss << ", Qualifier::" << res_qualifier;
ss << ", \"" << var_type << "\"";
ss << ", \"" << var_name << var_array << "\"";
ss << ", Frequency::" << res_frequency;
ss << res_condition_lambda << ")";
}
else if (res_type == "shared") {
ss << "GROUP_SHARED(" << var_type << ", " << var_name << var_array << ")";
}
else if (res_type == "push_constant") {
if (!var_array.empty()) {
ss << "PUSH_CONSTANT_ARRAY(" << var_type << ", " << var_name << ", "
<< var_array.substr(1, var_array.size() - 2) << ")";
}
else {
ss << "PUSH_CONSTANT(" << var_type << ", " << var_name << ")";
}
}
else if (res_type == "compilation_constant") {
/* Needs to be defined on the shader declaration. */
/* TODO(fclem): Add check that shader sets an existing compilation constant. */
// ss << "COMPILATION_CONSTANT(" << var_type << ", " << var_name << ", " << res_value << ")";
/* WORKAROUND: Avoid unused expression warning. */
ss << ".noop()\n";
}
else if (res_type == "specialization_constant") {
ss << "SPECIALIZATION_CONSTANT(" << var_type << ", " << var_name << ", " << res_value << ")";
}
return ss.str();
}
std::string ParsedAttribute::serialize() const
{
std::stringstream ss;
if (interpolation_mode == "flat") {
ss << "FLAT(" << var_type << ", " << var_name << ")";
}
else if (interpolation_mode == "smooth") {
ss << "SMOOTH(" << var_type << ", " << var_name << ")";
}
else if (interpolation_mode == "no_perspective") {
ss << "NO_PERSPECTIVE(" << var_type << ", " << var_name << ")";
}
return ss.str();
}
std::string StageInterface::serialize() const
{
std::stringstream ss;
ss << "GPU_SHADER_INTERFACE_INFO(" << name << "_t)\n";
for (const auto &res : *this) {
ss << res.serialize() << "\n";
}
ss << "GPU_SHADER_INTERFACE_END()\n";
return ss.str();
}
std::string ParsedFragOuput::serialize() const
{
std::stringstream ss;
if (!dual_source.empty()) {
ss << "FRAGMENT_OUT_DUAL(" << slot << ", " << var_type << ", " << var_name << ", "
<< "SRC_" << dual_source << ")";
}
else if (!raster_order_group.empty()) {
ss << "FRAGMENT_OUT_ROG(" << slot << ", " << var_type << ", " << var_name << ", "
<< raster_order_group << ")";
}
else {
ss << "FRAGMENT_OUT(" << slot << ", " << var_type << ", " << var_name << ")";
}
return ss.str();
}
std::string ParsedFragInput::serialize() const
{
std::stringstream ss;
ss << "SUBPASS_IN(" << slot << ", " << var_type << ", " << image_type << ", " << var_name << ", "
<< raster_order_group << ")";
return ss.str();
}
std::string FragmentOutputs::serialize() const
{
std::stringstream ss;
ss << "GPU_SHADER_CREATE_INFO(" << name << ")\n";
for (const auto &res : *this) {
ss << res.serialize() << "\n";
}
ss << "GPU_SHADER_CREATE_END()\n";
return ss.str();
}
std::string FragmentInputs::serialize() const
{
std::stringstream ss;
ss << "GPU_SHADER_CREATE_INFO(" << name << ")\n";
for (const auto &res : *this) {
ss << res.serialize() << "\n";
}
ss << "GPU_SHADER_CREATE_END()\n";
return ss.str();
}
std::string ParsedVertInput::serialize() const
{
std::stringstream ss;
ss << "VERTEX_IN(" << slot << ", " << var_type << ", " << var_name << ")";
return ss.str();
}
std::string VertexInputs::serialize() const
{
std::stringstream ss;
ss << "GPU_SHADER_CREATE_INFO(" << name << ")\n";
for (const auto &res : *this) {
ss << res.serialize() << "\n";
}
ss << "GPU_SHADER_CREATE_END()\n";
return ss.str();
}
std::string Source::serialize(const std::string &function_name) const
{
std::stringstream ss;
ss << "static void " << function_name
<< "(GPUSource &source, GPUFunctionDictionary *g_functions, GPUPrintFormatMap *g_formats) "
"{\n";
for (auto function : functions) {
ss << " {\n";
ss << " Vector<metadata::ArgumentFormat> args = {\n";
for (auto arg : function.arguments) {
ss << " "
<< "metadata::ArgumentFormat{"
<< "metadata::Qualifier(" << std::to_string(uint64_t(arg.qualifier)) << "LLU), "
<< "metadata::Type(" << std::to_string(uint64_t(arg.type)) << "LLU)"
<< "},\n";
}
ss << " };\n";
ss << " source.add_function(\"" << function.name << "\", args, g_functions);\n";
ss << " }\n";
}
for (auto builtin : builtins) {
ss << " source.add_builtin(metadata::Builtin(" << std::to_string(builtin) << "LLU));\n";
}
for (auto dependency : dependencies) {
ss << " source.add_dependency(\"" << dependency << "\");\n";
}
for (auto var : shared_variables) {
ss << " source.add_shared_variable(Type::" << var.type << "_t, \"" << var.name << "\");\n";
}
for (auto format : printf_formats) {
ss << " source.add_printf_format(uint32_t(" << std::to_string(format.hash) << "), "
<< format.format << ", g_formats);\n";
}
/* Avoid warnings. */
ss << " UNUSED_VARS(source, g_functions, g_formats);\n";
ss << "}\n";
return ss.str();
}
std::string Source::serialize_infos() const
{
std::stringstream ss;
ss << "#pragma once\n";
ss << "\n";
for (auto dependency : create_infos_dependencies) {
ss << "#include \"" << dependency << "\"\n";
}
ss << "\n";
for (auto define : create_infos_defines) {
ss << define;
}
ss << "\n";
for (auto vert_inputs : vertex_inputs) {
ss << vert_inputs.serialize() << "\n";
}
ss << "\n";
for (auto frag_outputs : fragment_outputs) {
ss << frag_outputs.serialize() << "\n";
}
for (auto frag_inputs : fragment_inputs) {
ss << frag_inputs.serialize() << "\n";
}
ss << "\n";
for (auto iface : stage_interfaces) {
ss << iface.serialize() << "\n";
}
ss << "\n";
for (auto res_table : resource_tables) {
ss << "GPU_SHADER_CREATE_INFO(" << res_table.name << ")\n";
if (res_table.empty()) {
/* Add unused define to avoid warning about unused expression. */
ss << "DEFINE(\"EMPTY_CREATE_INFO\")\n";
}
for (const auto &res : res_table) {
ss << res.serialize() << "\n";
}
ss << "GPU_SHADER_CREATE_END()\n";
}
ss << "\n";
for (auto declaration : create_infos_declarations) {
ss << declaration << "\n";
}
return ss.str();
}
} // namespace blender::gpu::shader::metadata

View File

@@ -0,0 +1,286 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup shader_tool
*/
#pragma once
#include <cctype>
#include <cstdint>
#include <sstream>
#include <string>
#include <vector>
/* Metadata extracted from shader source file.
* These are then converted to their GPU module equivalent. */
/* TODO(fclem): Make GPU enums standalone and directly use them instead of using separate enums
* and types. */
namespace blender::gpu::shader::metadata {
/* Compile-time hashing function which converts string to a 64bit hash. */
constexpr static uint64_t hash(const char *name)
{
uint64_t hash = 2166136261u;
while (*name) {
hash = hash * 16777619u;
hash = hash ^ *name;
++name;
}
return hash;
}
static inline uint64_t hash(const std::string &name)
{
return hash(name.c_str());
}
enum Builtin : uint64_t {
ClipDistance = hash("gl_ClipDistance"),
FragCoord = hash("gl_FragCoord"),
FragStencilRef = hash("gl_FragStencilRefARB"),
FrontFacing = hash("gl_FrontFacing"),
GlobalInvocationID = hash("gl_GlobalInvocationID"),
InstanceIndex = hash("gpu_InstanceIndex"),
BaseInstance = hash("gpu_BaseInstance"),
InstanceID = hash("gl_InstanceID"),
LocalInvocationID = hash("gl_LocalInvocationID"),
LocalInvocationIndex = hash("gl_LocalInvocationIndex"),
NumWorkGroup = hash("gl_NumWorkGroup"),
PointCoord = hash("gl_PointCoord"),
PointSize = hash("gl_PointSize"),
PrimitiveID = hash("gl_PrimitiveID"),
VertexID = hash("gl_VertexID"),
WorkGroupID = hash("gl_WorkGroupID"),
WorkGroupSize = hash("gl_WorkGroupSize"),
drw_debug = hash("drw_debug_"),
printf = hash("printf"),
assert = hash("assert"),
runtime_generated = hash("runtime_generated"),
};
enum Qualifier : uint64_t {
in = hash("in"),
out = hash("out"),
inout = hash("inout"),
};
enum Type : uint64_t {
float1 = hash("float"),
float2 = hash("float2"),
float3 = hash("float3"),
float4 = hash("float4"),
float3x3 = hash("float3x3"),
float4x4 = hash("float4x4"),
sampler1DArray = hash("sampler1DArray"),
sampler2DArray = hash("sampler2DArray"),
sampler2D = hash("sampler2D"),
sampler3D = hash("sampler3D"),
Closure = hash("Closure"),
};
struct ArgumentFormat {
Qualifier qualifier;
Type type;
};
struct FunctionFormat {
std::string name;
std::vector<ArgumentFormat> arguments;
};
struct PrintfFormat {
uint32_t hash;
std::string format;
};
struct SharedVariable {
std::string type;
std::string name;
};
struct ParsedResource {
/** Line this resource was defined. */
size_t line;
std::string var_type;
std::string var_name;
std::string var_array;
std::string res_type;
/** For images, storage, uniforms and samplers. */
std::string res_frequency = "PASS";
/** For images, storage, uniforms and samplers. */
std::string res_slot;
/** For images & storage. */
std::string res_qualifier;
/** For specialization & compilation constants. */
std::string res_value;
/** For images. */
std::string res_format;
/** Optional condition to enable this resource. */
std::string res_condition;
std::string serialize() const;
};
struct ResourceTable : std::vector<ParsedResource> {
std::string name;
};
struct ParsedAttribute {
/* Line this resource was defined. */
size_t line;
std::string var_type;
std::string var_name;
std::string interpolation_mode;
std::string serialize() const;
};
struct StageInterface : std::vector<ParsedAttribute> {
std::string name;
std::string serialize() const;
};
struct ParsedFragOuput {
/* Line this resource was defined. */
size_t line;
std::string var_type;
std::string var_name;
std::string slot;
std::string dual_source;
std::string raster_order_group;
std::string serialize() const;
};
struct FragmentOutputs : std::vector<ParsedFragOuput> {
std::string name;
std::string serialize() const;
};
struct ParsedFragInput {
/* Line this resource was defined. */
size_t line;
std::string var_type;
std::string var_name;
std::string slot;
std::string image_type;
std::string raster_order_group;
std::string serialize() const;
};
struct FragmentInputs : std::vector<ParsedFragInput> {
std::string name;
std::string serialize() const;
};
struct ParsedVertInput {
/* Line this resource was defined. */
size_t line;
std::string var_type;
std::string var_name;
std::string slot;
std::string serialize() const;
};
struct VertexInputs : std::vector<ParsedVertInput> {
std::string name;
std::string serialize() const;
};
struct TemplateDefinition {
std::string identifier;
std::string name_space;
std::string definition;
std::string filepath;
size_t definition_line;
bool is_method;
bool is_static;
bool is_struct;
bool operator==(const TemplateDefinition &other) const
{
return std::tie(identifier, name_space) == std::tie(other.identifier, other.name_space);
}
};
struct Symbol {
std::string identifier;
std::string name_space;
size_t definition_line;
bool is_method;
bool is_static;
bool is_struct;
/* For structures only. */
std::vector<std::pair<std::string, std::string>> members;
bool operator<(const Symbol &other) const
{
if (is_static != other.is_static) {
return is_static > other.is_static;
}
if (is_method != other.is_method) {
/* Methods are supposed to have more precedence.
* So make them smaller than anything else. */
return is_method > other.is_method;
}
if (name_space != other.name_space) {
return name_space > other.name_space;
}
if (definition_line != other.definition_line) {
return definition_line < other.definition_line;
}
if (identifier != other.identifier) {
return identifier < other.identifier;
}
if (is_struct != other.is_struct) {
return is_struct < other.is_struct;
}
return false;
}
};
struct Source {
std::vector<Builtin> builtins;
/* Note: Could be a set, but for now the order matters. */
std::vector<std::string> dependencies;
std::vector<SharedVariable> shared_variables;
std::vector<PrintfFormat> printf_formats;
std::vector<FunctionFormat> functions;
std::vector<std::string> create_infos;
std::vector<std::string> create_infos_declarations;
std::vector<std::string> create_infos_dependencies;
std::vector<std::string> create_infos_defines;
std::vector<ResourceTable> resource_tables;
std::vector<StageInterface> stage_interfaces;
std::vector<FragmentOutputs> fragment_outputs;
std::vector<FragmentInputs> fragment_inputs;
std::vector<VertexInputs> vertex_inputs;
std::vector<Symbol> symbol_table;
std::vector<TemplateDefinition> template_definitions;
/* Serialize Metadata for this source file. */
std::string serialize(const std::string &function_name) const;
/* Serialize Create Infos for this source file. */
std::string serialize_infos() const;
};
} // namespace blender::gpu::shader::metadata

View File

@@ -0,0 +1,414 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup shader_tool
*/
#include <set>
#include <unordered_set>
#include "intermediate.hh"
#include "metadata.hh"
#include "processor.hh"
namespace blender::gpu::shader {
using namespace std;
using namespace shader::parser;
using namespace metadata;
static string get_prefix(Scope ns_scope)
{
string prefix;
while (ns_scope.type() == ScopeType::Namespace || ns_scope.type() == ScopeType::Struct) {
prefix = ns_scope.front().prev().full_symbol_name() + "::" + prefix;
ns_scope = ns_scope.scope();
}
return prefix;
}
TemplateDefinition SourceProcessor::parse_template_definition(SourceProcessor::Parser &parser,
Token template_tok,
bool is_method,
Scope ns_scope,
const std::string &filepath)
{
Token def_start = template_tok;
Scope template_args = def_start.next().scope();
/* Skip arguments. */
Token tok_type = template_args.back().next();
Token body_start = template_tok.find_next(BracketOpen);
Token def_end = body_start.scope().back();
TemplateDefinition symbol;
symbol.filepath = filepath;
symbol.definition_line = tok_type.line_number();
symbol.is_method = is_method;
symbol.is_static = tok_type == Static;
symbol.is_struct = tok_type == Struct || tok_type == Class;
symbol.name_space = get_prefix(ns_scope);
if (symbol.is_struct) {
Token name = body_start.prev();
symbol.identifier = string(name.str());
}
else {
Token fn_args = body_start.prev() == Const ? body_start.prev(2) : body_start.prev();
Token fn_name = fn_args.scope().front().prev();
symbol.identifier = string(fn_name.str());
}
/* Capture end semicolon for structs. */
def_end = (symbol.is_struct) ? def_end.next() : def_end;
symbol.definition = parser.substr(def_start, def_end);
return symbol;
}
void SourceProcessor::parse_namespace_symbols(SourceProcessor::Parser &parser,
Scope ns,
metadata::Source &metadata,
const std::string &filepath)
{
ns.foreach_scope(ScopeType::Namespace, [&](const Scope &ns) {
parse_namespace_symbols(parser, ns, metadata, filepath);
});
auto process_symbol = [&](Scope ns_scope,
Token name,
string_view identifier,
size_t line,
bool is_method,
bool is_static,
bool is_struct,
std::vector<std::pair<std::string, std::string>> members = {}) {
if (name.scope() != ns_scope) {
return;
}
Symbol symbol;
symbol.name_space = get_prefix(ns_scope);
symbol.identifier = identifier;
symbol.definition_line = line;
symbol.is_method = is_method;
symbol.is_static = is_static;
symbol.is_struct = is_struct;
symbol.members = members;
metadata.symbol_table.emplace_back(symbol);
};
auto process_templates = [&](Scope ns_scope, Token t, bool is_method) {
if (t.scope() != ns_scope) {
return;
}
if (t.next() == '<') {
if (t.next(2) == '>') {
/* Template specialization. */
return;
}
TemplateDefinition symbol = SourceProcessor::parse_template_definition(
parser, t, is_method, ns_scope, filepath);
metadata.template_definitions.emplace_back(symbol);
return;
}
/* Line number of the instantiation should be the one of the definition.
* But it is very hard at this point to search for the definition.
* Instead we consider the instantiation to be at the top of the file.
* It is unlikely we will have name collision with an instantiated template. */
size_t line = 0;
if (t.next() == Struct || t.next() == Class) {
/* Struct. */
Token name = t.next().next();
Scope template_args = name.next().scope();
string resolved_name = string(name.str()) +
SourceProcessor::template_arguments_mangle(template_args);
process_symbol(ns_scope, name, resolved_name, line, false, false, true, {});
}
else {
/* Function. */
Token end = t.find_next(SemiColon);
Scope template_args = end.prev().scope().front().prev().scope();
Token name = template_args.front().prev();
string resolved_name = string(name.str()) +
SourceProcessor::template_arguments_mangle(template_args);
process_symbol(ns_scope, name, resolved_name, line, is_method, false, false);
}
};
ns.foreach_struct([&](Token, Scope, Token struct_name, Scope body) {
/* Parse member. */
std::vector<std::pair<std::string, std::string>> members;
body.foreach_declaration([&](Scope, Token, Token type, Scope, Token name, Scope, Token) {
/* For methods, the declaration line is the top of the struct. */
members.emplace_back(type.str(), name.str());
});
process_symbol(ns,
struct_name,
struct_name.str(),
struct_name.line_number(),
false,
false,
true,
members);
/* Methods. */
body.foreach_function([&](bool is_static, Token, Token name, Scope, bool, Scope) {
/* For methods, the declaration line is the top of the struct. */
process_symbol(body, name, name.str(), struct_name.line_number(), true, is_static, false);
});
/* Parse template instantiations. */
body.foreach_token(Template, [&](Token t) { process_templates(body, t, true); });
});
ns.foreach_function([&](bool, Token, Token name, Scope, bool, Scope) {
process_symbol(ns, name, name.str(), name.line_number(), false, false, false);
});
/* Parse template instantiations. */
ns.foreach_token(Template, [&](Token t) { process_templates(ns, t, false); });
}
void SourceProcessor::parse_local_symbols(Parser &parser)
{
parse_namespace_symbols(parser, parser(), metadata_, filepath_);
}
static void lower_namespace(string ns_prefix,
const Scope &scope,
SourceProcessor::Parser &parser,
ErrorHandler &error_handler,
const set<Symbol> &symbols_set)
{
string ns_name(scope.front().prev().str());
ns_prefix += ns_name + "::";
bool has_nested_scope = false;
scope.foreach_scope(ScopeType::Namespace, [&](const Scope &scope) {
lower_namespace(ns_prefix, scope, parser, error_handler, symbols_set);
has_nested_scope = true;
});
if (has_nested_scope) {
/* Process iteratively. */
return;
}
scope.foreach_token(Word, [&](const Token &token) {
/* Reject method calls. */
if (token.prev() == '.') {
return;
}
Token next = token.next();
/* Only process the end token of a namespace qualified identifier. */
if (next == ':') {
return;
}
bool is_pipeline_arg = false;
if (token.scope().type() == ScopeType::FunctionArg) {
std::string_view type = token.scope().scope().front().prev(2).str();
is_pipeline_arg = (type == "PipelineGraphic" || type == "PipelineCompute");
}
if (is_pipeline_arg) {
/* Special case for pipelines. We need to match the function names but they are arguments. */
}
/* Only process function calls or types. */
else if (next != '<' && /* Templated type or function. */
next != '{' && /* Type definition. */
next != '&' && /* Reference definition. */
next != Word && /* Variable definition. */
token.scope().type() != parser::ScopeType::TemplateArg && /* Template argument. */
next != '(' /* Function call or reference definition. */
)
{
return;
}
const bool is_fn = (token.next() == '(') ||
(token.next() == '<' && token.next().scope().back().next() == '(');
/* Reject method definition. */
if (is_fn && token.scope().type() == ScopeType::Struct) {
return;
}
string struct_name;
if (is_fn) {
/* If this is function call inside a struct, this could reference a method.
* In this case we need to add the struct name during the fully qualified name lookup. */
const Scope struct_scope = token.scope().first_scope_of_type(ScopeType::Struct);
if (struct_scope.is_valid()) {
struct_name = struct_scope.front().prev().full_symbol_name();
}
}
const int token_line = token.line_number();
for (const auto &symbol : symbols_set) {
if (token.str() != symbol.identifier) {
continue;
}
/* Only expand symbols that are visible inside this namespace. */
if (!symbol.name_space.starts_with(ns_prefix)) {
continue;
}
/* Reject symbols declared after the identifier.
* Note that static method have their definition line at the top of the struct. */
if (token_line < symbol.definition_line) {
continue;
}
/* Symbol as it could be specified from this namespace. */
string symbol_visible = symbol.name_space.substr(ns_prefix.size()) + symbol.identifier;
bool append_struct_ns = false;
string specified_symbol = token.full_symbol_name();
/* First try to match methods. */
if (symbol.is_method && !struct_name.empty()) {
if (!symbol.is_static) {
continue;
}
bool is_prev_ns_specifier = token.prev() == ':' && token.prev(2) == ':';
if (!is_prev_ns_specifier) {
/* For unspecified symbol, we append the struct namespace to try to match the method
* visible symbol. */
specified_symbol = struct_name + "::" + specified_symbol;
append_struct_ns = true;
}
if (specified_symbol != symbol_visible) {
continue;
}
/* Matched a static method. */
}
else {
/* Other symbols. */
if (specified_symbol != symbol_visible) {
continue;
}
}
/* Only for non-definition. */
if (token.prev() != Word) {
/* WORKAROUND: Since we do not have overload argument type infos, we cannot resolve
* them like C++ does. Instead, we error on ambiguity and let the user resolve it. */
for (const auto &overload : symbols_set) {
/* Searching for overload in other namespaces. */
if (overload.name_space == symbol.name_space || overload.identifier != symbol.identifier)
{
continue;
}
/* Reject symbols declared after the identifier.
* Note that static method have their definition line at the top of the struct. */
if (token_line < overload.definition_line) {
continue;
}
/* Only expand symbols that are visible inside this namespace. */
if (!ns_prefix.starts_with(overload.name_space)) {
continue;
}
if (specified_symbol != overload.identifier) {
continue;
}
error_handler.report(
token, "Call to function is ambiguous. Specify namespace to remove ambiguity.");
break;
}
}
/* Append current namespace. */
parser.insert_before(token.namespace_start(), ns_name + "::");
if (append_struct_ns) {
/* Append struct namespace for static methods. */
parser.insert_before(token.namespace_start(), struct_name + "::");
}
/* Only match a symbol once. */
break;
}
});
/* Pipeline declarations.
* Manually handle them. They are the only use-case of variable defined in global scope. */
scope.foreach_match("AA(A", [&](vector<Token> toks) {
if (toks[0].scope().type() != ScopeType::Namespace || !toks[0].str().starts_with("Pipeline")) {
return;
}
parser.insert_before(toks[1], ns_name + SourceProcessor::namespace_separator);
});
Token namespace_tok = scope.front().prev().namespace_start().prev();
if (namespace_tok == Namespace) {
parser.erase(namespace_tok, scope.front());
parser.erase(scope.back());
}
else {
error_handler.report(namespace_tok, "Expected namespace token.");
}
}
/* Lower namespaces by adding namespace prefix to all the contained structs and functions. */
void SourceProcessor::lower_namespaces(Parser &parser)
{
using namespace metadata;
/* Expand compound namespaces. Simplify lowering.
* Example: `namespace A::B {}` > `namespace A { namespace B {} }` */
parser().foreach_token(Namespace, [&](Token t) {
int nesting = 0;
Token name = t.next();
while (name.next() == ':') {
parser.replace(name.next(), name.next().next(), " { namespace ");
name = name.next().next().next();
nesting++;
}
Scope scope = name.next().scope();
for (int i = 0; i < nesting; i++) {
parser.insert_before(scope.back(), "}");
}
});
parser.apply_mutations();
/* Using an ordered set ordered by namespace make homonym symbols are resolve
* properly (closest from current namespace). */
set<Symbol> symbols_set;
{
/* Deduplicate symbols. Done this way because we want to keep line definition ordering
* inside the symbols_set. */
unordered_set<string> unique_symbols;
for (const auto &symbol : metadata_.symbol_table) {
auto [_, inserted] = unique_symbols.insert(symbol.name_space + symbol.identifier);
if (inserted) {
symbols_set.emplace(symbol);
}
}
}
do {
/* Parse each namespace declaration.
* Do it iteratively from the deepest namespace to the shallowest. */
parser().foreach_scope(ScopeType::Namespace, [&](const Scope &scope) {
lower_namespace("", scope, parser, error_handler, symbols_set);
});
} while (parser.apply_mutations());
}
void SourceProcessor::lower_scope_resolution_operators(Parser &parser)
{
parser().foreach_match<true>("::", [&](const vector<Token> &tokens) {
if (tokens[0].scope().type() == ScopeType::Attribute) {
return;
}
Token prev = tokens[0].prev();
if (prev != Word && !(prev == '>' && prev.followed_by_whitespace() == false)) {
/* Global namespace reference. */
parser.erase(tokens.front(), tokens.back());
}
else {
/* Specific namespace reference. */
parser.replace(tokens.front(), tokens.back(), namespace_separator);
}
});
parser.apply_mutations();
}
} // namespace blender::gpu::shader

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,417 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup shader_tool
*/
#pragma once
#include "intermediate.hh"
#include "metadata.hh"
namespace blender::gpu::shader {
enum class Language {
UNKNOWN = 0,
/* Shared header. */
CPP,
/* Metal Shading Language. */
MSL,
/* OpenGL Shading Language. */
GLSL,
/* Blender Shading Language. */
BSL,
/* Same as GLSL but enable partial C++ feature support like template, references,
* include system, etc ... */
BLENDER_GLSL,
};
static inline Language language_from_filename(const std::string &filename)
{
if (filename.find(".msl") != std::string::npos) {
return Language::MSL;
}
if (filename.find(".glsl") != std::string::npos || filename.find(".bsl.hh") != std::string::npos)
{
return Language::GLSL;
}
if (filename.find(".hh") != std::string::npos) {
return Language::CPP;
}
return Language::UNKNOWN;
}
/**
* Shader source preprocessor that allow to mutate shader sources into cross API source that can be
* interpreted by the different GPU backends. Some syntax are mutated or reported as incompatible.
*/
class SourceProcessor {
public:
using ErrorHandler = parser::ErrorHandler;
using Parser = parser::IntermediateForm<parser::FullLexer, parser::FullParser>;
using Scope = parser::Scope;
using Token = parser::Token;
using Tokens = std::vector<parser::Token>;
/* Cannot use `__` because of some compilers complaining about reserved symbols. */
static constexpr const char *namespace_separator = "_";
/* Add a prefix to all member functions so that they are not clashing with local variables. */
static constexpr const char *method_call_prefix = "_";
static constexpr const char *linted_struct_suffix = "_host_shared_";
static constexpr const char *uniform_struct_suffix = "uniform_";
private:
const std::string source_;
const std::string filepath_;
metadata::Source metadata_;
Language language_;
parser::ErrorHandler error_handler = {
.default_filename = filepath_.substr(filepath_.find_last_of('/') + 1)};
void report_error(Token tok, const std::string &message)
{
error_handler.report(tok, message);
}
void report_error(int row, int column, const std::string &line, const std::string &message)
{
error_handler.report(row, column, line, message);
}
public:
SourceProcessor(const std::string &source, const std::string &filepath, Language language)
: source_(source), filepath_(filepath), language_(language)
{
}
struct Result {
/* Resulting Intermediate Language source. */
std::string source;
/* Parsed metadata. */
metadata::Source metadata;
std::optional<parser::ErrorHandler::Error> error;
};
/* Convert to intermediate language. Also outputs metadata.
* symbols_set is the set of namespace symbols from external files / dependencies. */
Result convert(metadata::Source external_sources_symbols = {});
/* Lightweight parsing. Only Source::dependencies and Source::symbol_table are populated. */
metadata::Source parse_include_and_symbols();
/* Return the input string with comments removed. */
std::string remove_comments()
{
return remove_comments(source_);
}
/* String hash are outputted inside GLSL and needs to fit 32 bits. */
static uint32_t hash_string(const std::string &str)
{
uint64_t hash_64 = metadata::hash(str);
uint32_t hash_32 = uint32_t(hash_64 ^ (hash_64 >> 32));
return hash_32;
}
private:
Result convert_glsl();
Result convert_msl();
Result convert_bsl(metadata::Source external_sources_symbols);
/* --- Cleanup --- */
/** Remove single and multi-line comments to avoid this complexity during parsing. */
std::string remove_comments(const std::string &str);
/* Lower preprocessor directives containing `GPU_SHADER`.
* Avoid processing code that is not destined to be shader code and could contain unsupported
* syntax. */
std::string disabled_code_mutation(const std::string &str);
/* Remove trailing white spaces. */
template<typename ParserT> void cleanup_whitespace(ParserT &parser);
/* Successive mutations can introduce a lot of unneeded line directives. */
void cleanup_line_directives(Parser &parser);
/* Successive mutations can introduce a lot of unneeded blank lines. */
void cleanup_empty_lines(Parser &parser);
/* --- Parsing --- */
/* Parse defines in order to output them with the create infos.
* This allow the create infos to use shared defines values. */
void parse_defines(Parser &parser);
/* Populates metadata::symbol_table by scanning all namespaces.
* Does not parse global symbols. */
void parse_local_symbols(Parser &parser);
/* Legacy create info parsing and removing. */
void parse_legacy_create_info(Parser &parser);
/* Populates metadata::dependencies by scanning include directives. */
void parse_includes(Parser &parser);
/* Parse special pragma. */
void parse_pragma_runtime_generated(Parser &parser);
/** Populate metadata::functions for runtime node-tree compilation. */
void parse_library_functions(Parser &parser);
/* Populate metadata::builtins by scanning source for keywords. Can trigger false positive.
* This is mostly legacy path as most builtin should be explicitly defined inside the BSL entry
* points. */
void parse_builtins(const std::string &str, const std::string &filename, bool pure_glsl = false);
/* Legacy shared variable support. */
std::string threadgroup_variables_parse_and_remove(const std::string &str);
/* --- Linting --- */
/* Make sure `if`, `else`, `for` statements are followed by braces. */
void lint_unbraced_statements(Parser &parser);
/* Lint for BSL reserved tokens. */
void lint_reserved_tokens(Parser &parser);
/* Lint for valid BSL attributes. */
void lint_attributes(Parser &parser);
/* Assume formatted source with our code style. Cannot be applied to python shaders. */
void lint_global_scope_constants(Parser &parser);
/* Search for constructor definition in active code. These are not supported. */
void lint_constructors(Parser &parser);
/* Forward declaration of types are not supported and makes no sense in a shader program where
* there is no pointers. */
void lint_forward_declared_structs(Parser &parser);
/* --- Lowering --- */
/* Remove `maybe_unused` attribute. */
void lower_maybe_unused(Parser &parser);
/* Lower parameters that have no name (invalid in GLSL). */
void lower_namesless_parameters(Parser &parser);
/**
* Given our code-style, we don't need the disambiguation.
* Example: `x.template foo<int>()` > `x.foo<int>()`
*/
void lower_template_dependent_names(Parser &parser);
/* Lower template definition and instantiation by doing simple copy paste + argument
* substitution. */
void lower_templates(Parser &parser);
void lower_template_calls(Parser &parser);
void lower_template_specialization(Parser &parser);
/* Ensures pragma once is present in headers to comply to our include semantic. */
void lint_pragma_once(Parser &parser, const std::string &filename);
/* Unroll loops by copy pasting content. */
void lower_loop_unroll(Parser &parser);
/* Convert if statements marked as static to preprocessor #if statements. */
void lower_static_branch(Parser &parser);
/* Lower namespaces by adding namespace prefix to all the contained structs and functions. */
void lower_namespaces(Parser &parser);
/**
* Needs to run before namespace mutation so that `using` have more precedence.
* Otherwise the following would fail.
* \code{.cc}
* namespace B {
* int test(int a) {}
* }
*
* namespace A {
* int test(int a) {}
* int func(int a) {
* using B::test;
* return test(a); // Should reference B::test and not A::test
* }
* \endcode
*/
void lower_using(Parser &parser);
/* Example: `A::B` --> `A_B` */
void lower_scope_resolution_operators(Parser &parser);
/* Remove preprocessor directives unsupported by target shading languages.
* Examples `#includes`, `#pragma once`. */
void lower_preprocessor(Parser &parser);
/* Support for BLI swizzle syntax.
* Examples `a.xy()` --> `a.xy`. */
void lower_swizzle_methods(Parser &parser);
/* Support for binary literals.
* Examples `0b1001` --> `0x9`. */
void lower_binary_literals(Parser &parser);
/* Change printf calls to "recursive" call to implementation functions.
* This allows to emulate the variadic arguments of printf. */
void lower_printf(Parser &parser);
/* Turn assert into a printf. */
void lower_assert(Parser &parser, const std::string &filename);
/* Parse SRT and interfaces, remove their attributes and create init function for SRT structs. */
void lower_resource_table(Parser &parser);
/* Examples `string_t s = "a" "b"` --> `string_t s = "ab"`. */
void lower_strings_sequences(Parser &parser);
/* Replace string literals by their hash and store the original string in the file metadata. */
void lower_strings(Parser &parser);
/* `class` -> `struct` */
void lower_classes(Parser &parser);
/* Create default initializer (empty brace) for all classes. */
void lower_default_constructors(Parser &parser);
/* Make all members of a class to be referenced using `this->`. */
void lower_implicit_member(Parser &parser);
/* Move all method definition outside of struct definition blocks. */
void lower_method_definitions(Parser &parser);
/* Add padding member to empty structs. */
void lower_empty_struct(Parser &parser);
/* Transform `a.fn(b)` into `fn(a, b)`. */
void lower_method_calls(Parser &parser);
/* Transform `auto [a, b] = fn()` into `S _tmp = fn(); a = _tmp.A; b = _tmp.B;`. */
void lower_structured_bindings(Parser &parser);
/* Parse, convert to create infos, and erase declaration. */
void lower_pipeline_definition(Parser &parser, const std::string &filename);
/* Remove `[vertex|fragment|compute]` function attribute and add appropriate guards. */
void lower_stage_function(Parser &parser);
/* Add #ifdef directive around functions using SRT arguments.
* Need to run after `lower_entry_points_signature`. */
void lower_srt_arguments(Parser &parser);
/* Add ifdefs guards around scopes using resource accessors. */
void lower_resource_access_functions(Parser &parser);
/* Lower enums to constants. */
void lower_enums(Parser &parser);
/* Merge attribute scopes. They are equivalent in the C++ standard.
* This allow to simplify parsing later on.
* `[[a]] [[b]]` > `[[a, b]]` */
void lower_attribute_sequences(Parser &parser);
/* Lint host shared structure for padding and alignment.
* Remove the [[host_shared]] attribute. */
void lower_host_shared_structures(Parser &parser);
/* Remove noop keywords that makes subsequent lowering passes more complicated. */
void lower_noop_keywords(Parser &parser);
/* Example: `int a[] = {1,2,};` --> `int a[] = {1,2 };` */
void lower_trailing_comma_in_list(Parser &parser);
/* Allow easier parsing of struct member declaration.
* Example: `int a, b;` --> `int a; int b;` */
void lower_comma_separated_declarations(Parser &parser);
/* Example: `return {1, 2};` --> `T tmp = T{1, 2}; return tmp;`. */
void lower_implicit_return_types(Parser &parser);
/* Example: `int a{1};` --> `int a = int{1};`. */
void lower_initializer_implicit_types(Parser &parser);
/* Example: `T a{.a=1};` --> `T a; a.a=1;`. */
void lower_designated_initializers(Parser &parser);
/* Support for **full** aggregate initialization.
* They are converted to default constructor for GLSL. */
void lower_aggregate_initializers(Parser &parser);
/* Auto detect array length, and lower to GLSL compatible syntax.
* TODO(fclem): GLSL 4.3 already supports initializer list. So port the old GLSL syntax to
* initializer list instead. */
void lower_array_initializations(Parser &parser);
/**
* Expand functions with default arguments to function overloads.
* Expects formatted input and that function bodies are followed by newline.
*/
void lower_function_default_arguments(Parser &parser);
/* Limited union implementation. Create getters and setters to a raw data struct. */
void lower_unions(Parser &parser);
/**
* For safety reason, union members need to be declared with the union_t template.
* This avoid raw member access which we cannot emulate. Instead this forces the use of the `()`
* operator for accessing the members of the enum.
*
* Need to run before lower_unions.
*/
void lower_union_accessor_templates(Parser &parser);
/**
* For safety reason, nested resource tables need to be declared with the srt_t template.
* This avoid chained member access which isn't well defined with the preprocessing we are doing.
*
* This linting phase make sure that [[resource_table]] members uses it and that no incorrect
* usage is made. We also remove this template because it has no real meaning.
*
* Need to run before lower_resource_table.
*/
void lower_srt_accessor_templates(Parser &parser);
/* Add `srt_access` around all member access of SRT variables.
* Need to run before local reference mutations. */
void lower_srt_member_access(Parser &parser);
/* Parse entry point definitions and mutating all parameter usage to global resources. */
void lower_entry_points(Parser &parser);
/* Removes entry point arguments to make it compatible with the legacy code.
* Has to run after mutation related to function arguments. */
void lower_entry_points_signature(Parser &parser);
/* To be run after `lower_reference_arguments()`. */
void lower_reference_variables(Parser &parser);
/* To be run before `argument_decorator_macro_injection()`. */
void lower_reference_arguments(Parser &parser);
/* Example: `out float var[2]` > `_ref(float, var)[2]` */
void lower_argument_qualifiers(Parser &parser);
/* Example: `textureGather(t,c,1)` > `textureGather1(t,c)` */
void lower_gather_component(Parser &parser);
/* Lower test expect clauses to SSBO assignments. */
void lower_tests(Parser &parser);
/* --- Legacy passes for GLSL --- */
/* Example: `out float var[2]` > `out float _out_sta var _out_end[2]` */
std::string argument_decorator_macro_injection(const std::string &str);
/* Example: `= float[2](0.0, 0.0)` > `= ARRAY_T(float) ARRAY_V(0.0, 0.0)` */
std::string array_constructor_macro_injection(const std::string &str);
/* Used to make GLSL matrix constructor compatible with MSL in pyGPU shaders.
* This syntax is not supported in blender's own shaders. */
std::string matrix_constructor_mutation(const std::string &str);
/* --- Utilities --- */
/* Parse subscript scope with single integer literal and return the literal value.
* Return the fallback value in any case of non-literal value, or failed conversion. */
int static_array_size(const Scope &array, int fallback_value);
/* Process struct declaration and instantiate it in this file. */
void process_template_struct(metadata::TemplateDefinition &template_def,
SourceProcessor::Parser &parser);
/* Process templated function (or class method) declaration and instantiate it in this file. */
void process_template_function(metadata::TemplateDefinition &template_def,
SourceProcessor::Parser &parser,
/* If method, the end token of the template inside the struct. */
const Token method_end);
void lower_pre_template(Parser &parser);
void lower_template_instantiation(
Parser &parser,
/* If method, the end token of the template inside the struct. */
const Token method_end,
const Token &inst_start,
const Scope &inst_args,
const metadata::TemplateDefinition template_def,
const Token &symbol_name,
const std::vector<std::string> &arg_list,
const std::string &fn_decl,
const bool all_template_args_in_function_signature);
metadata::TemplateDefinition parse_template_definition(SourceProcessor::Parser &parser,
Token template_tok,
bool is_method,
Scope ns_scope,
const std::string &filepath);
void parse_namespace_symbols(SourceProcessor::Parser &parser,
Scope ns,
metadata::Source &metadata,
const std::string &filepath);
std::string template_full_specified_name(metadata::TemplateDefinition &template_def);
public:
/* Check for existence of preprocessor pragma in file. */
static bool has_pragma(Parser &parser, std::string_view pragma_str);
/** Remove trailing white-spaces. */
static std::string strip_whitespace(const std::string &str);
/* Example: `VertOut<float, 1>` > `VertOutTfloatT1` */
static std::string template_arguments_mangle(const Scope template_args);
/* Create placeholder for GLSL declarations generated by the GPU backends (VK/GL). */
static std::string get_create_info_placeholder(const std::string &name);
/* Make a scope only active based on the given condition using `#if` preprocessor directives.
* Processor contained return statements by returning 0 if scope is disabled.
* fn_type can be invalid token if scope is not a function scope. */
static void guarded_scope_mutation(Parser &parser,
Scope scope,
const std::string &condition,
Token fn_type);
/* Return `#line 1 filename\n`. */
static std::string line_directive_prefix(const std::string &filename);
};
} // namespace blender::gpu::shader

View File

@@ -0,0 +1,726 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup shader_tool
*/
#include "intermediate.hh"
#include "metadata.hh"
#include "processor.hh"
namespace blender::gpu::shader {
using namespace std;
using namespace shader::parser;
using namespace metadata;
/**
* For safety reason, nested resource tables need to be declared with the srt_t template.
* This avoid chained member access which isn't well defined with the preprocessing we are doing.
*
* This linting phase make sure that [[resource_table]] members uses it and that no incorrect
* usage is made. We also remove this template because it has no real meaning.
*
* Need to run before lower_resource_table.
*/
void SourceProcessor::lower_srt_accessor_templates(Parser &parser)
{
parser().foreach_token(Struct, [&](Token tok) {
Scope body = tok.find_next(lexit::BracketOpen).scope();
body.foreach_declaration([&](Scope attributes,
Token,
Token type,
Scope template_scope,
Token name,
Scope array,
Token) {
if (attributes[1].str() != "resource_table") {
if (type.str() == "srt_t") {
report_error(name,
"The srt_t<T> template is only to be used with members declared with the "
"[[resource_table]] attribute.");
}
return;
}
if (type.str() != "srt_t") {
report_error(type,
"Members declared with the [[resource_table]] attribute must wrap their type "
"with the srt_t<T> template.");
}
if (array.is_valid()) {
report_error(name, "[[resource_table]] members cannot be arrays.");
}
if (name.prev() == '&') {
report_error(name.prev(), "[[resource_table]] members cannot be references.");
}
/* Remove the template but not the wrapped type. */
parser.erase(type);
if (template_scope.is_valid()) {
parser.erase(template_scope.front());
parser.erase(template_scope.back());
}
});
});
parser.apply_mutations();
}
/* Add `srt_access` around all member access of SRT variables.
* Need to run before local reference mutations. */
void SourceProcessor::lower_srt_member_access(Parser &parser)
{
const string srt_attribute = "resource_table";
auto memher_access_mutation = [&](Scope attribute, Token type, Token var, Scope body_scope) {
if (attribute[2].str() != srt_attribute) {
return;
}
const bool is_func_prototype_decl = body_scope.is_invalid();
const bool is_local_reference = attribute.scope().type() != ScopeType::FunctionArgs &&
attribute.scope().type() != ScopeType::FunctionArg;
if (is_local_reference || is_func_prototype_decl) {
parser.replace(attribute, "");
}
/* Change references to copies to allow placeholder "*_new_()" function result to be passed
* as argument. Once these placeholder function are removed, we can pass the value as
* reference. */
if (!is_local_reference && var.prev() == '&') {
parser.erase(var.prev());
}
string srt_type(type.str());
string srt_var(var.str());
body_scope.foreach_match("A.A", [&](const vector<Token> toks) {
if (toks[0].str() != srt_var || toks[0].prev() == '.') {
return;
}
parser.replace(
toks[0], toks[2], "srt_access(" + srt_type + ", " + string(toks[2].str()) + ")", true);
});
};
parser().foreach_scope(ScopeType::FunctionArgs, [&](const Scope fn_args) {
/* Parse both function and prototypes. */
Scope fn_body = fn_args.next().type() == ScopeType::Function ? fn_args.next() : Scope(parser);
/* Function arguments. */
fn_args.foreach_match("[[..]]c?A&A", [&](const vector<Token> toks) {
memher_access_mutation(toks[0].scope(), toks[8], toks[10], fn_body);
});
fn_args.foreach_match("[[..]]c?AA", [&](const vector<Token> toks) {
if (toks[1].next().str() == srt_attribute) {
parser.erase(toks[0].scope());
report_error(toks[9], "Shader Resource Table arguments must be references.");
}
});
});
parser().foreach_scope(ScopeType::Function, [&](const Scope fn_body) {
/* Local references. */
fn_body.foreach_match("[[..]]c?A&A", [&](const vector<Token> toks) {
memher_access_mutation(toks[0].scope(), toks[8], toks[10], toks[10].scope());
});
/* Local variables. */
fn_body.foreach_match("[[..]]c?AA", [&](const vector<Token> toks) {
memher_access_mutation(toks[0].scope(), toks[8], toks[9], toks[9].scope());
});
});
parser.apply_mutations();
}
/* Add #ifdef directive around functions using SRT arguments.
* Need to run after `lower_entry_points_signature`. */
void SourceProcessor::lower_srt_arguments(Parser &parser)
{
/* SRT arguments. */
parser().foreach_function([&](bool, Token fn_type, Token, Scope fn_args, bool, Scope fn_body) {
string condition;
fn_args.foreach_match("[[..]]c?A", [&](const vector<Token> &tokens) {
if (tokens[1].next().str() != "resource_table") {
return;
}
string srt_cond;
tokens[1].scope().foreach_attribute([&](Token attribute_name, Scope attribute_parameters) {
if (attribute_name.str() == "condition") {
srt_cond = "SRT_CONSTANT_" + string(attribute_parameters.str_exclusive());
}
});
condition += " && ";
if (!srt_cond.empty()) {
/* If condition exists, ensure the function will be available if the condition is false */
condition += "(!(" + srt_cond + ") ||";
}
condition += "defined(CREATE_INFO_" + string(tokens[8].str()) + ")";
if (!srt_cond.empty()) {
condition += ")";
}
parser.replace(tokens[0].scope(), "");
});
if (!condition.empty()) {
/* Take attribute into account. */
Token first_tok = fn_type.prev() == ']' ? fn_type.prev().scope().front() : fn_type;
parser.insert_directive(first_tok.prev(), "#if " + condition.substr(4));
parser.insert_directive(fn_body.back(), "#endif");
}
});
parser.apply_mutations();
}
/* Add ifdefs guards around scopes using resource accessors. */
void SourceProcessor::lower_resource_access_functions(Parser &parser)
{
/* Legacy access macros. */
parser().foreach_function([&](bool, Token fn_type, Token, Scope, bool, Scope fn_body) {
fn_body.foreach_match("A(", [&](const vector<Token> &tokens) {
string_view func_name = tokens[0].str();
if (func_name != "specialization_constant_get" && func_name != "shared_variable_get" &&
func_name != "push_constant_get" && func_name != "interface_get" &&
func_name != "resource_table_get" && func_name != "attribute_get" &&
func_name != "buffer_get" && func_name != "sampler_get" && func_name != "image_get")
{
return;
}
if (tokens[1].next() != Word) {
report_error(tokens[1].next(), "Expecting symbol name");
return;
}
string info_name(tokens[1].next().str());
Scope scope = tokens[0].scope();
/* We can be in expression scope. Take parent scope until we find a local scope. */
while (scope.type() != ScopeType::Function && scope.type() != ScopeType::Local) {
scope = scope.scope();
}
string condition = "defined(CREATE_INFO_" + info_name + ")";
if (scope.type() == ScopeType::Function) {
guarded_scope_mutation(parser, scope, condition, fn_type);
}
else {
guarded_scope_mutation(parser, scope, condition, Token(parser));
}
});
});
parser.apply_mutations();
}
/**
* Needs to run before namespace mutation so that `using` have more precedence.
* Otherwise the following would fail.
* \code{.cc}
* namespace B {
* int test(int a) {}
* }
*
* namespace A {
* int test(int a) {}
* int func(int a) {
* using B::test;
* return test(a); // Should reference B::test and not A::test
* }
* \endcode
*/
void SourceProcessor::lower_using(Parser &parser)
{
parser().foreach_match("un", [&](const vector<Token> &tokens) {
report_error(tokens[0],
"Unsupported `using namespace`. "
"Add individual `using` directives for each needed symbol.");
});
auto process_using = [&](const Token &using_tok,
const Token &from,
const Token &to_start,
const Token &to_end,
const Token &end_tok) {
string to = parser.substr_range_inclusive(to_start, to_end);
string namespace_prefix = parser.substr_range_inclusive(to_start, to_end.prev().prev().prev());
Scope scope = from.scope();
/* Using the keyword in global or at namespace scope. */
if (scope.type() == ScopeType::Global) {
report_error(using_tok, "The `using` keyword is not allowed in global scope.");
return;
}
if (scope.type() == ScopeType::Namespace) {
/* Ensure we are bringing symbols from the same namespace.
* Otherwise we can have different shadowing outcome between shader and C++. */
string namespace_name = scope.front().prev().full_symbol_name();
if (namespace_name != namespace_prefix) {
report_error(
using_tok,
"The `using` keyword is only allowed in namespace scope to make visible symbols "
"from the same namespace declared in another scope, potentially from another "
"file.");
return;
}
}
/* Assignments do not allow to alias functions symbols. */
const bool use_alias = from.str() != to_end.str();
const bool replace_fn = !use_alias;
/** IMPORTANT: If replace_fn is true, this can replace any symbol type if there are functions
* and types with the same name. We could support being more explicit about the type of
* symbol to replace using an optional attribute [[gpu::using_function]]. */
/* Replace all occurrences of the non-namespace specified symbol. */
scope.foreach_token(Word, [&](const Token &token) {
/* Do not replace symbols before the using statement. */
if (token.index_ <= to_end.index_) {
return;
}
/* Reject symbols that contain the target symbol name. */
if (token.prev() == ':') {
return;
}
if (!replace_fn && token.next() == '(') {
return;
}
if (token.str() != from.str()) {
return;
}
parser.replace(token, to, true);
});
parser.erase(using_tok, end_tok);
};
parser().foreach_match("uA::A", [&](const vector<Token> &tokens) {
Token end = tokens.back().find_next(SemiColon);
process_using(tokens[0], end.prev(), tokens[1], end.prev(), end);
});
parser().foreach_match("uA=A::A", [&](const vector<Token> &tokens) {
Token end = tokens.back().find_next(SemiColon);
process_using(tokens[0], tokens[1], tokens[3], end.prev(), end);
});
parser.apply_mutations();
/* Verify all using were processed. */
parser().foreach_token(Using, [&](const Token &token) {
report_error(token, "Unsupported `using` keyword usage.");
});
}
/* Parse SRT and interfaces, remove their attributes and create init function for SRT structs. */
void SourceProcessor::lower_resource_table(Parser &parser)
{
enum class SrtType {
undefined,
none,
resource_table,
vertex_input,
vertex_output,
fragment_output,
fragment_input,
};
auto parse_resource = [&](Scope attributes, Token type, Token name, Scope array) {
/* FIXME(fclem): This is a hotfix to support multi dimensional array.
* Ideally, array should already contain all dimensions */
Scope array_end = array;
while (array_end.next().type() == ScopeType::Subscript) {
array_end = array_end.next();
}
string_view array_str;
if (array.is_valid()) {
array_str = parser.substr(array.front(), array_end.back(), true);
}
metadata::ParsedResource resource{
type.line_number(), string(type.str()), string(name.str()), string(array_str)};
attributes.foreach_scope(ScopeType::Attribute, [&](const Scope &attribute) {
string_view type = attribute[0].str();
if (type == "sampler") {
resource.res_type = type;
resource.res_slot = attribute[2].str();
}
else if (type == "image") {
resource.res_type = type;
resource.res_slot = attribute[2].str();
resource.res_qualifier = attribute[4].str();
resource.res_format = attribute[6].str();
}
else if (type == "uniform") {
resource.res_type = type;
resource.res_slot = attribute[2].str();
}
else if (type == "storage") {
resource.res_type = type;
resource.res_slot = attribute[2].str();
resource.res_qualifier = attribute[4].str();
}
else if (type == "shared") {
resource.res_type = type;
}
else if (type == "push_constant") {
resource.res_type = type;
}
else if (type == "compilation_constant") {
resource.res_type = type;
}
else if (type == "specialization_constant") {
resource.res_type = type;
resource.res_value = attribute[1].scope().str_exclusive();
}
else if (type == "condition") {
attribute[1].scope().foreach_token(Word, [&](const Token tok) {
resource.res_condition += "int " + string(tok.str()) + " = ";
resource.res_condition += "ShaderCreateInfo::find_constant(constants, \"" +
string(tok.str()) + "\"); ";
});
resource.res_condition += "return " + string(attribute[1].scope().str()) + ";";
}
else if (type == "frequency") {
resource.res_frequency = attribute[2].str();
}
else if (type == "resource_table") {
resource.res_type = type;
}
else if (type == "legacy_info") {
resource.res_type = type;
}
else {
report_error(attribute[0], "Invalid attribute in resource table");
}
});
return resource;
};
auto parse_vertex_input = [&](Scope attributes, Token type, Token name, Scope array) {
if (array.is_valid()) {
report_error(array[0], "Array are not supported as vertex attributes");
}
metadata::ParsedVertInput vert_in{type.line_number(), string(type.str()), string(name.str())};
if (vert_in.var_type == "float3x3" || vert_in.var_type == "float2x2" ||
vert_in.var_type == "float4x4" || vert_in.var_type == "float3x4")
{
report_error(name, "Matrices are not supported as vertex attributes");
}
attributes.foreach_scope(ScopeType::Attribute, [&](const Scope &attribute) {
string_view type = attribute[0].str();
if (type == "attribute") {
vert_in.slot = attribute[2].str();
}
else {
report_error(attribute[0], "Invalid attribute in vertex input interface");
}
});
return vert_in;
};
auto parse_vertex_output =
[&](Token struct_name, Scope attributes, Token type, Token name, Scope array) {
if (array.is_valid()) {
report_error(array[0], "Array are not supported in stage interface");
}
Token interpolation_mode = attributes[1];
metadata::ParsedAttribute attr{type.line_number(),
string(type.str()),
string(struct_name.str()) + "_" + string(name.str()),
string(interpolation_mode.str())};
if (attr.var_type == "float3x3" || attr.var_type == "float2x2" ||
attr.var_type == "float4x4" || attr.var_type == "float3x4")
{
report_error(name, "Matrices are not supported in stage interface");
}
if (attr.interpolation_mode != "smooth" && attr.interpolation_mode != "flat" &&
attr.interpolation_mode != "no_perspective")
{
report_error(attributes[0], "Invalid attribute in shader stage interface");
}
return attr;
};
auto parse_fragment_output =
[&](Token struct_name, Scope attributes, Token tok_type, Token name, Scope) {
metadata::ParsedFragOuput frag_out{tok_type.line_number(),
string(tok_type.str()),
string(struct_name.str()) + "_" + string(name.str())};
attributes.foreach_scope(ScopeType::Attribute, [&](const Scope &attribute) {
string_view type = attribute[0].str();
if (type == "frag_color") {
frag_out.slot = attribute[2].str();
}
else if (type == "raster_order_group") {
frag_out.raster_order_group = attribute[2].str();
}
else if (type == "index") {
frag_out.dual_source = attribute[2].str();
}
else {
report_error(attributes[0], "Invalid attribute in fragment output interface");
}
});
return frag_out;
};
auto parse_fragment_input =
[&](Token struct_name, Scope attributes, Token tok_type, Token name, Scope) {
metadata::ParsedFragInput frag_in{tok_type.line_number(),
string(tok_type.str()),
string(struct_name.str()) + "_" + string(name.str())};
attributes.foreach_scope(ScopeType::Attribute, [&](const Scope &attribute) {
string_view type = attribute[0].str();
if (type == "subpass_input") {
frag_in.slot = attribute[2].str();
frag_in.image_type = attribute[4].str();
}
else if (type == "raster_order_group") {
frag_in.raster_order_group = attribute[2].str();
}
else {
report_error(attributes[0], "Invalid attribute in fragment output interface");
}
});
return frag_in;
};
auto is_resource_table_attribute = [](Token attr) {
string_view type = attr.str();
return (type == "sampler" || type == "image" || type == "uniform" || type == "storage" ||
type == "shared" || type == "push_constant" || type == "compilation_constant" ||
type == "specialization_constant" || type == "legacy_info" ||
type == "resource_table");
};
auto is_vertex_input_attribute = [](Token attr) {
string_view type = attr.str();
return (type == "attribute");
};
auto is_vertex_output_attribute = [](Token attr) {
string_view type = attr.str();
return (type == "flat" || type == "smooth" || type == "no_perspective");
};
auto is_fragment_output_attribute = [](Token attr) {
string_view type = attr.str();
return (type == "frag_color" || type == "frag_depth" || type == "frag_stencil_ref");
};
auto is_fragment_input_attribute = [](Token attr) {
string_view type = attr.str();
return (type == "subpass_input");
};
parser().foreach_struct([&](Token struct_tok, Scope, Token struct_name, Scope body) {
SrtType srt_type = SrtType::undefined;
bool has_srt_members = false;
metadata::ResourceTable srt;
metadata::VertexInputs vertex_in;
metadata::StageInterface vertex_out;
metadata::FragmentOutputs fragment_out;
metadata::FragmentInputs fragment_in;
srt.name = struct_name.str();
vertex_in.name = struct_name.str();
vertex_out.name = struct_name.str();
fragment_out.name = struct_name.str();
fragment_in.name = struct_name.str();
body.foreach_declaration([&](Scope attributes,
Token const_tok,
Token type,
Scope /*template_scope*/, /* TODO */
Token name,
Scope array,
Token decl_end) {
SrtType decl_type = SrtType::undefined;
if (attributes.is_invalid()) {
decl_type = SrtType::none;
}
else if (is_resource_table_attribute(attributes[1])) {
decl_type = SrtType::resource_table;
}
else if (is_vertex_input_attribute(attributes[1])) {
decl_type = SrtType::vertex_input;
}
else if (is_vertex_output_attribute(attributes[1])) {
decl_type = SrtType::vertex_output;
}
else if (is_fragment_output_attribute(attributes[1])) {
decl_type = SrtType::fragment_output;
}
else if (is_fragment_input_attribute(attributes[1])) {
decl_type = SrtType::fragment_input;
}
else {
return;
}
if (srt_type == SrtType::undefined) {
srt_type = decl_type;
}
else if (srt_type != decl_type) {
switch (srt_type) {
case SrtType::resource_table:
report_error(struct_name, "Structure expected to contain resources...");
break;
case SrtType::vertex_input:
report_error(struct_name, "Structure expected to contain vertex inputs...");
break;
case SrtType::vertex_output:
report_error(struct_name, "Structure expected to contain vertex outputs...");
break;
case SrtType::fragment_output:
report_error(struct_name, "Structure expected to contain fragment outputs...");
break;
case SrtType::fragment_input:
report_error(struct_name, "Structure expected to contain fragment inputs...");
break;
case SrtType::none:
report_error(struct_name, "Structure expected to contain plain data...");
break;
case SrtType::undefined:
break;
}
switch (decl_type) {
case SrtType::resource_table:
report_error(attributes[1], "...but member declared as resource.");
break;
case SrtType::vertex_input:
report_error(attributes[1], "...but member declared as vertex input.");
break;
case SrtType::vertex_output:
report_error(attributes[1], "...but member declared as vertex output.");
break;
case SrtType::fragment_output:
report_error(attributes[1], "...but member declared as fragment output.");
break;
case SrtType::fragment_input:
report_error(attributes[1], "...but member declared as fragment input.");
break;
case SrtType::none:
report_error(name, "...but member declared as plain data.");
break;
case SrtType::undefined:
break;
}
}
switch (decl_type) {
case SrtType::resource_table:
srt.emplace_back(parse_resource(attributes, type, name, array));
if (attributes[1].str() == "resource_table") {
has_srt_members = true;
parser.erase(attributes.scope());
parser.erase(const_tok);
}
else {
parser.erase(attributes.front().line_start(), decl_end.line_end());
}
break;
case SrtType::vertex_input:
vertex_in.emplace_back(parse_vertex_input(attributes, type, name, array));
parser.erase(attributes.scope());
break;
case SrtType::vertex_output:
vertex_out.emplace_back(parse_vertex_output(struct_name, attributes, type, name, array));
parser.erase(attributes.scope());
break;
case SrtType::fragment_output:
fragment_out.emplace_back(
parse_fragment_output(struct_name, attributes, type, name, array));
parser.erase(attributes.scope());
break;
case SrtType::fragment_input:
fragment_in.emplace_back(
parse_fragment_input(struct_name, attributes, type, name, array));
parser.erase(attributes.scope());
break;
case SrtType::undefined:
case SrtType::none:
break;
}
});
switch (srt_type) {
case SrtType::resource_table:
metadata_.resource_tables.emplace_back(srt);
break;
case SrtType::vertex_input:
metadata_.vertex_inputs.emplace_back(vertex_in);
break;
case SrtType::vertex_output:
metadata_.stage_interfaces.emplace_back(vertex_out);
break;
case SrtType::fragment_output:
metadata_.fragment_outputs.emplace_back(fragment_out);
break;
case SrtType::fragment_input:
metadata_.fragment_inputs.emplace_back(fragment_in);
break;
case SrtType::undefined:
case SrtType::none:
break;
}
Token end_of_srt = body.back().prev();
if (srt_type == SrtType::resource_table) {
/* Add static constructor.
* These are only to avoid warnings on certain backend compilers. */
string ctor;
ctor += "\nstatic " + srt.name + " new_()\n";
ctor += "{\n";
ctor += " " + srt.name + " result;\n";
if (has_srt_members == false) {
ctor += " result._pad = 0;\n";
}
for (const auto &member : srt) {
if (member.res_type == "resource_table") {
ctor += " result." + member.var_name + " = " + member.var_type + "::new_();\n";
}
}
ctor += " return result;\n";
/* Avoid messing up the line count and keep empty struct empty. */
ctor += "#line " + to_string(end_of_srt.line_number()) + "\n";
ctor += "}\n";
parser.insert_after(end_of_srt, ctor);
string access_macros;
for (const auto &member : srt) {
if (member.res_type == "resource_table") {
access_macros += "#define access_" + srt.name + "_" + member.var_name + "() ";
access_macros += member.var_type + "::new_()\n";
}
else {
access_macros += "#define access_" + srt.name + "_" + member.var_name + "() ";
access_macros += member.var_name + "\n";
}
}
parser.insert_before(struct_tok, access_macros);
parser.insert_before(struct_tok, get_create_info_placeholder(srt.name));
parser.insert_before(struct_tok, "\n");
parser.insert_line_number(struct_tok.str_index_start() - 1, struct_tok.line_number());
/* Insert attribute so that method mutations know that this struct is an SRT. */
parser.insert_before(struct_tok, "[[resource_table]] ");
}
});
parser.apply_mutations();
}
} // namespace blender::gpu::shader

View File

@@ -0,0 +1,541 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup shader_tool
*
*/
#pragma once
#include "token.hh"
#include "token_stream.hh"
#include <cassert>
namespace blender::gpu::shader::parser {
struct ScopeParser;
struct Scope {
friend ScopeParser;
private:
#ifndef NDEBUG
/* String view for nicer debugging experience. Isn't actually used. */
std::string_view token_view_;
std::string_view str_view_;
#endif
/* Parser in which the scope resides. */
const ParserBase *parser_;
/* Scope index. */
int64_t index_;
public:
Scope() = delete;
Scope(const ParserBase &parser, int64_t index) : parser_(&parser), index_(index)
{
if (index < 0 || index >= parser.scope_types.size()) {
index = parser.scope_types.size();
return;
}
#ifndef NDEBUG
IndexRange index_range = parser.scope_ranges[index];
token_view_ = parser_->token_types_str().substr(index_range.start, index_range.size);
str_view_ = parser_->substr((*parser_)[index_range.start], (*parser_)[index_range.last()]);
#endif
}
/* Create an invalid scope. */
Scope(const ParserBase &parser) : Scope(parser, -1) {}
bool is_valid() const
{
return index_ < parser_->scope_types.size();
}
bool is_invalid() const
{
return index_ >= parser_->scope_types.size();
}
Token operator[](int i)
{
return is_invalid() ? Token(*parser_) : Token(*parser_, range().start + i);
}
/* Return first token of that scope. */
Token front() const
{
return is_invalid() ? Token(*parser_) : Token(*parser_, range().start);
}
/* Return last token of that scope. */
Token back() const
{
return is_invalid() ? Token(*parser_) : Token(*parser_, range().last());
}
IndexRange range() const
{
return is_invalid() ? IndexRange(0, 0) : parser_->scope_ranges[index_];
}
Token operator[](const int64_t index) const
{
return Token(*parser_, range().start + index);
}
size_t token_count() const
{
return is_invalid() ? 0 : range().size;
}
ScopeType type() const
{
return is_invalid() ? ScopeType::Invalid : ScopeType(parser_->scope_types[index_]);
}
/* WORKAROUND: Only used for semantic tagging of scopes after parsing pass.
* The type is only retained until the next parsing pass. */
void set_type(ScopeType type)
{
const_cast<ParserBase *>(parser_)->scope_types[index_] = type;
}
/* Returns the scope that contains this scope. */
Scope scope() const
{
if (is_invalid()) {
return Scope(*parser_);
}
const size_t scope_start = this->front().str_index_start();
Scope scope = *this;
while ((scope = scope.prev()).is_valid()) {
if (scope.back().str_index_last() > scope_start) {
return scope;
}
}
return scope;
}
/* Returns the parent node.
* Equivalent to scope(). Should ultimately replace it. */
Scope parent() const
{
if (is_invalid()) {
return Scope(*parser_);
}
return Scope(*parser_, parser_->scope_links[index_].parent_);
}
Scope prev_neighbor() const
{
if (is_invalid()) {
return Scope(*parser_);
}
return Scope(*parser_, parser_->scope_links[index_].prev_);
}
Scope next_neighbor() const
{
if (is_invalid()) {
return Scope(*parser_);
}
return Scope(*parser_, parser_->scope_links[index_].next_);
}
Scope child_first() const
{
if (is_invalid()) {
return Scope(*parser_);
}
return Scope(*parser_, parser_->scope_links[index_].child_first_);
}
/* Returns the previous scope before this scope. Can be either the container scope or the
* previous scope inside the same container. */
Scope prev() const
{
return is_invalid() ? Scope(*parser_) : front().prev().scope();
}
/* Returns the next scope after this scope. Can be either the container scope or the next scope
* inside the same container. */
Scope next() const
{
return is_invalid() ? Scope(*parser_) : back().next().scope();
}
bool contains(const Scope sub) const
{
Scope parent = sub.scope();
while (parent.type() != ScopeType::Global && parent != *this) {
parent = parent.scope();
}
return parent == *this;
}
/* Returns true if scope contains the sub-string. */
bool contains(const std::string &str) const
{
return this->str().find(str) != std::string::npos;
}
std::string_view str_with_whitespace() const
{
if (this->is_invalid()) {
return "";
}
return parser_->substr(front(), back(), true);
}
std::string_view str() const
{
if (this->is_invalid()) {
return "";
}
return parser_->substr(front(), back(), false);
}
/* Return the content without the first and last token. */
std::string_view str_exclusive() const
{
if (this->is_invalid() || this->token_count() <= 2) {
return "";
}
return parser_->substr(front().next(), back().prev(), false);
}
/* Return first occurrence of token_type inside this scope. */
Token find_token(const char token_type) const
{
if (this->is_invalid()) {
return Token(*parser_);
}
size_t pos = parser_->token_types_str().substr(range().start, range().size).find(token_type);
return (pos != std::string::npos) ? Token(*parser_, range().start + pos) : Token(*parser_);
}
bool contains_token(const char token_type) const
{
return find_token(token_type).is_valid();
}
/* Return the first container scope that has the given type (including itself).
* Returns invalid scope on failure. */
Scope first_scope_of_type(const ScopeType type) const
{
Scope scope = *this;
while (scope.type() != ScopeType::Global && scope.type() != type) {
scope = scope.scope();
}
return scope.type() == type ? scope : Scope(*parser_);
}
/**
* Small pattern matching engine.
* - pattern is expected to a be a sequence of #TokenType stored as a string.
* - single '?' after a token will make this token optional.
* - double '?' will match the question mark.
* - double '.' will skip to the end of the current matched scope.
* - callback is called for each matches with a vector of token the size of the input pattern.
* - control tokens ('..' and '?') and unmatched optional tokens will be set to invalid in match
* vector.
* IMPORTANT: 2 matches cannot overlap. The pattern matching algorithm skips the whole match
* after a match there is no readback. This could eventually be fixed.
*
* If `include_preprocessor` is true, try to match any token. Otherwise ignore tokens in
* preprocessor scopes.
*
* Callback should have this signature `void(const std::vector<Token>)`.
*/
template<bool include_preprocessor = false, typename CallbackFn>
void foreach_match(const std::string &pattern, CallbackFn callback) const
{
assert(!pattern.empty());
if (this->is_invalid()) {
return;
}
const std::string_view scope_tokens = parser_->token_types_str().substr(range().start,
range().size);
auto count_match = [](const std::string_view &s, const std::string_view &pattern) {
size_t pos = 0, occurrences = 0;
while ((pos = s.find(pattern, pos)) != std::string::npos) {
occurrences += 1;
pos += pattern.length();
}
return occurrences;
};
const int control_token_count = count_match(pattern, "?") * 2 + count_match(pattern, "..") * 2;
if (range().size < pattern.size() - control_token_count) {
return;
}
const size_t searchable_range = scope_tokens.size() -
(pattern.size() - 1 - control_token_count);
std::vector<Token> match(pattern.size(), Token(*parser_));
for (size_t pos = 0; pos < searchable_range; pos++) {
size_t cursor = range().start + pos;
for (int i = 0; i < pattern.size(); i++) {
bool is_last_token = i == pattern.size() - 1;
TokenType token_type = TokenType(parser_->types_[cursor]);
TokenType curr_search_token = TokenType(pattern[i]);
TokenType next_search_token = TokenType(is_last_token ? '\0' : pattern[i + 1]);
/* Scope skipping. */
if (!is_last_token && curr_search_token == '.' && next_search_token == '.') {
cursor = match[i - 1].scope().back().index_;
i++;
continue;
}
/* Regular token. */
if (curr_search_token == token_type) {
match[i] = Token(*parser_, cursor++);
}
else if (curr_search_token == '?' && next_search_token != '?') {
/* We just matched an optional token in previous iteration. Continue scanning. */
match[i] = Token(*parser_);
}
else if (!is_last_token && curr_search_token != '?' && next_search_token == '?') {
/* This was an optional token. Continue scanning. */
match[i] = Token(*parser_);
i++;
continue;
}
else {
/* Token mismatch. Test next position. */
break;
}
if constexpr (!include_preprocessor) {
if (match[i].scope().type() == ScopeType::Preprocessor) {
/* Scope mismatch. Test next position. */
break;
}
}
if (is_last_token) {
callback(match);
/* Avoid matching the same position if start of pattern is optional tokens. */
pos = cursor - range().start - 1;
}
}
}
}
/**
* Will iterate over all the scopes that are direct children.
* Callback should have this signature `void(Scope)`.
*/
template<typename CallbackFn> void foreach_scope(ScopeType type, CallbackFn callback) const
{
/* Makes no sense to iterate on global scope since it is the top level. */
assert(type != ScopeType::Global);
if (this->is_invalid()) {
return;
}
size_t pos = this->index_;
while ((pos = parser_->scope_types_str.find(char(type), pos)) != std::string::npos) {
Scope scope(*parser_, pos);
if (scope.front().index_ > this->back().index_) {
/* Found scope starts after this scope. End iteration. */
break;
}
/* Make sure found scope is direct child of this scope. */
Scope parent_scope = scope.scope();
if (parent_scope.index_ == this->index_) {
callback(scope);
}
pos += 1;
}
}
/**
* Will iterate over all the attribute if this scope is an ScopeType::Attributes.
* Callback should have this signature `void(Token attribute_name, Scope attribute_parameters)`.
*/
template<typename CallbackFn> void foreach_attribute(CallbackFn callback) const
{
assert(this->type() == ScopeType::Attributes);
this->foreach_scope(ScopeType::Attribute, [&](Scope attr) {
callback(attr[0], attr[1] == '(' ? attr[1].scope() : Scope(*parser_));
});
}
/**
* Will iterate over all tokens of the scope (and its contained scopes).
* Callback should have this signature `void(Token)`.
*/
template<typename Callback>
void foreach_token(const TokenType token_type, Callback callback) const
{
IndexRange index_range = parser_->scope_ranges[index_];
std::string_view view(parser_->token_types_str());
size_t offset = index_range.start;
for (const char c : view.substr(index_range.start, index_range.size)) {
if (token_type == TokenType(c)) {
callback(Token(*parser_, offset));
}
offset++;
}
}
/**
* Run a callback for all the function scopes that are direct children of this scope.
* Callback should have this signature
* `void(bool is_static, Token type, Token name, Scope args, bool is_const, Scope body)`.
*/
template<typename Callback> void foreach_function(Callback callback) const
{
foreach_match("m?AA(..)c?{..}", [&](const std::vector<Token> matches) {
callback(matches[0] == Static,
matches[2],
matches[3],
matches[4].scope(),
matches[8] == Const,
matches[10].scope());
});
foreach_match("m?A<..>A(..)c?{..}", [&](const std::vector<Token> matches) {
callback(matches[0] == Static,
matches[2],
matches[7],
matches[8].scope(),
matches[12] == Const,
matches[14].scope());
});
foreach_match("m?AA::A(..)c?{..}", [&](const std::vector<Token> matches) {
callback(matches[0] == Static,
matches[2],
matches[6],
matches[7].scope(),
matches[11] == Const,
matches[13].scope());
});
foreach_match("m?AA<..>(..)c?{..}", [&](const std::vector<Token> matches) {
callback(matches[0] == Static,
matches[2],
matches[3],
matches[8].scope(),
matches[12] == Const,
matches[14].scope());
});
}
/**
* Run a callback for all the struct scopes that are direct children of this scope.
* Callback should have this signature
* `void(Token struct_tok, Scope attributes, Token name, Scope body)`.
*/
template<typename Callback> void foreach_struct(Callback callback) const
{
foreach_match("sA{..}", [&](const std::vector<Token> matches) {
callback(matches[0], Scope(*parser_), matches[1], matches[2].scope());
});
foreach_match("sA<..>{..}", [&](const std::vector<Token> matches) {
callback(matches[0], Scope(*parser_), matches[1], matches[6].scope());
});
foreach_match("s[[..]]A{..}", [&](const std::vector<Token> matches) {
callback(matches[0], matches[2].scope(), matches[7], matches[8].scope());
});
foreach_match("s[[..]]A<..>{..}", [&](const std::vector<Token> matches) {
callback(matches[0], matches[2].scope(), matches[7], matches[12].scope());
});
}
/**
* Run a callback for all the variable declarations (without assignment) that are direct children
* Callback should have this signature
* `void(Scope attributes,
* Token const_tok,
* Token type,
* Scope template_scope,
* Token name,
* Scope array,
* Token decl_end)`.
*/
template<typename Callback> void foreach_declaration(Callback callback) const
{
auto attrs = [&](const std::vector<Token> &tokens) {
Token first = tokens[0].is_valid() ? tokens[0] : tokens[2];
Scope attributes = first.prev().prev().scope();
attributes = (attributes.type() == ScopeType::Attributes) ? attributes : Scope(*parser_);
return attributes;
};
auto cb = [&](Scope attributes,
Token const_tok,
Token type,
Scope template_scope,
Token name,
Scope array,
Token decl_end) {
if (type.scope() != *this) {
return;
}
callback(attributes, const_tok, type, template_scope, name, array, decl_end);
};
Scope invalid(*parser_);
/* TODO(fclem): This is getting out of hand... */
foreach_match("c?AA;", [&](const std::vector<Token> toks) {
cb(attrs(toks), toks[0], toks[2], invalid, toks[3], invalid, toks.back());
});
foreach_match("c?AA[..];", [&](const std::vector<Token> toks) {
cb(attrs(toks), toks[0], toks[2], invalid, toks[3], toks[4].scope(), toks.back());
});
foreach_match("c?AA[..][..];", [&](const std::vector<Token> toks) {
cb(attrs(toks), toks[0], toks[2], invalid, toks[3], toks[4].scope(), toks.back());
});
foreach_match("c?A<..>A;", [&](const std::vector<Token> toks) {
cb(attrs(toks), toks[0], toks[2], toks[3].scope(), toks[7], invalid, toks.back());
});
foreach_match("c?A<..>A[..];", [&](const std::vector<Token> toks) {
cb(attrs(toks), toks[0], toks[2], toks[3].scope(), toks[7], toks[8].scope(), toks.back());
});
foreach_match("c?A<..>A[..][..];", [&](const std::vector<Token> toks) {
cb(attrs(toks), toks[0], toks[2], toks[3].scope(), toks[7], toks[8].scope(), toks.back());
});
foreach_match("c?A&A;", [&](const std::vector<Token> toks) {
cb(attrs(toks), toks[0], toks[2], invalid, toks[4], invalid, toks.back());
});
foreach_match("c?A(&A)[..];", [&](const std::vector<Token> toks) {
cb(attrs(toks), toks[0], toks[2], invalid, toks[5], toks[7].scope(), toks.back());
});
foreach_match("c?A(&A)[..][..];", [&](const std::vector<Token> toks) {
cb(attrs(toks), toks[0], toks[2], invalid, toks[5], toks[7].scope(), toks.back());
});
foreach_match("c?A<..>&A;", [&](const std::vector<Token> toks) {
cb(attrs(toks), toks[0], toks[2], toks[3].scope(), toks[8], invalid, toks.back());
});
foreach_match("c?A<..>(&A)[..];", [&](const std::vector<Token> toks) {
cb(attrs(toks), toks[0], toks[2], toks[3].scope(), toks[9], toks[11].scope(), toks.back());
});
foreach_match("c?A<..>(&A)[..][..];", [&](const std::vector<Token> toks) {
cb(attrs(toks), toks[0], toks[2], toks[3].scope(), toks[9], toks[11].scope(), toks.back());
});
}
bool operator==(const Scope &other) const
{
return this->index_ == other.index_ && this->parser_ == other.parser_;
}
bool operator!=(const Scope &other) const
{
return !(*this == other);
}
};
} // namespace blender::gpu::shader::parser

View File

@@ -0,0 +1,235 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup shader_tool
*/
#include <algorithm>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <string>
#include "processor.hh"
using namespace blender::gpu::shader;
static std::vector<std::string> list_files(const std::string &dir)
{
std::vector<std::string> files;
for (const auto &entry : std::filesystem::directory_iterator(std::filesystem::path(dir))) {
if (entry.is_regular_file()) {
std::string filename(entry.path().string());
/* We only allow including header files or shader files. */
if (filename.find(".hh") != std::string::npos ||
filename.find(".msl") != std::string::npos ||
filename.find(".glsl") != std::string::npos)
{
files.push_back(filename);
}
}
}
return files;
}
static metadata::Source scan_external_symbols(const std::vector<std::string> &file_list,
std::vector<std::string> &visited_files,
const std::string &file_buffer,
const std::string &file_name)
{
Language language = language_from_filename(file_name);
SourceProcessor processor(file_buffer, file_name, language);
metadata::Source include_data = processor.parse_include_and_symbols();
bool errors = false;
for (const auto &dep : include_data.dependencies) {
std::string file;
for (const auto &filename : file_list) {
if (filename.find(dep) != std::string::npos) {
file = filename;
}
}
if (file.empty()) {
std::cout << "Error: Included file not found " << dep << std::endl;
errors = true;
}
else if (std::find(visited_files.begin(), visited_files.end(), file) == visited_files.end()) {
visited_files.emplace_back(file);
std::ifstream input_file(file);
if (!input_file) {
std::cerr << "Error: Could not open file " << file << std::endl;
errors = true;
}
else {
std::stringstream buffer;
buffer << input_file.rdbuf();
metadata::Source source = scan_external_symbols(
file_list, visited_files, buffer.str(), file);
/* Extend list. */
include_data.symbol_table.insert(include_data.symbol_table.end(),
source.symbol_table.begin(),
source.symbol_table.end());
include_data.template_definitions.insert(include_data.template_definitions.end(),
source.template_definitions.begin(),
source.template_definitions.end());
}
}
}
if (errors) {
exit(1);
}
return include_data;
}
int main(int argc, char **argv)
{
using namespace blender;
if (argc < 6) {
std::cerr << "Usage: shader_tool <data_file_from> <data_file_to> <metadata_file_to> "
"<infos_file_to> <dep_file_to> <include_dir1> <include_dir2> ..."
<< std::endl;
exit(1);
}
const char *input_file_name = argv[1];
const char *output_file_name = argv[2];
const char *metadata_file_name = argv[3];
const char *infos_file_name = argv[4];
const char *dep_file_name = argv[5];
/* Open the input file for reading */
std::ifstream input_file(input_file_name);
if (!input_file) {
std::cerr << "Error: Could not open input file " << input_file_name << std::endl;
exit(1);
}
/* We make the required directories here rather than having the build system
* do the work for us, as having cmake do it leads to several thousand cmake
* instances being launched, leading to significant overhead, see pr #141404
* for details. */
std::filesystem::path parent_dir = std::filesystem::path(output_file_name).parent_path();
std::error_code ec;
if (!std::filesystem::create_directories(parent_dir, ec)) {
if (ec) {
std::cerr << "Unable to create " << parent_dir << " : " << ec.message() << std::endl;
exit(1);
}
}
/* Open the output file for writing */
std::ofstream output_file(output_file_name, std::ofstream::out | std::ofstream::binary);
if (!output_file) {
std::cerr << "Error: Could not open output file " << output_file_name << std::endl;
input_file.close();
exit(1);
}
/* Open the output file for writing */
std::ofstream metadata_file(metadata_file_name, std::ofstream::out | std::ofstream::binary);
if (!metadata_file) {
std::cerr << "Error: Could not open output file " << metadata_file_name << std::endl;
input_file.close();
exit(1);
}
/* Open the output file for writing */
std::ofstream infos_file(infos_file_name, std::ofstream::out | std::ofstream::binary);
if (!infos_file) {
std::cerr << "Error: Could not open output file " << infos_file_name << std::endl;
input_file.close();
exit(1);
}
/* Open the output file for writing */
std::ofstream dep_file(dep_file_name, std::ofstream::out | std::ofstream::binary);
if (!dep_file) {
std::cerr << "Error: Could not open output file " << dep_file_name << std::endl;
input_file.close();
exit(1);
}
/* List of files available for include. */
std::vector<std::string> file_list;
for (int i = 6; i < argc; i++) {
auto list = list_files(std::string(argv[i]));
/* Extend list. */
file_list.insert(file_list.end(), list.begin(), list.end());
}
std::stringstream buffer;
buffer << input_file.rdbuf();
std::string filename(input_file_name);
const bool is_info = filename.ends_with("infos.hh") || filename.ends_with(".bsl.hh");
using namespace gpu::shader;
Language language = language_from_filename(filename);
if (language == Language::GLSL) {
/* All build-time GLSL files should be considered blender-GLSL. */
language = Language::BLENDER_GLSL;
}
metadata::Source external_symbols;
std::vector<std::string> visited_files{input_file_name};
if (language == Language::BLENDER_GLSL) {
external_symbols = scan_external_symbols(file_list, visited_files, buffer.str(), filename);
}
/* Escape path according to the depfile syntax. */
auto escape_path = [](std::string filepath) {
size_t pos = 0;
while ((pos = filepath.find(' ', pos)) != std::string::npos) {
filepath.replace(pos, 1, "\\ ");
pos += 2;
}
return filepath;
};
dep_file << output_file_name << " : ";
for (const auto &file : visited_files) {
dep_file << escape_path(file) << " ";
}
dep_file << "\n";
SourceProcessor processor(buffer.str(), input_file_name, language);
auto [result, metadata, error] = processor.convert(external_symbols);
output_file << result;
/* TODO(fclem): Don't use regex for that. */
size_t last_slash = filename.find_last_of('/');
std::string name = (last_slash == std::string::npos) ? filename :
filename.substr(last_slash + 1);
std::string metadata_function_name = "metadata_" + name + "_tmp";
std::ranges::replace(metadata_function_name, '.', '_');
metadata_file << metadata.serialize(metadata_function_name);
if (is_info) {
infos_file << metadata.serialize_infos();
}
input_file.close();
output_file.close();
metadata_file.close();
infos_file.close();
dep_file.close();
if (error) {
std::cerr << error.value().full_report << std::endl;
}
return error.has_value() ? 1 : 0;
}

View File

@@ -0,0 +1,118 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup shader_tool
*/
#include "intermediate.hh"
#include "metadata.hh"
#include "processor.hh"
namespace blender::gpu::shader {
using namespace std;
using namespace shader::parser;
using namespace metadata;
void SourceProcessor::lower_strings_sequences(Parser &parser)
{
do {
parser().foreach_match("\"\"", [&](const vector<Token> &tokens) {
string first(tokens[0].str());
string second(tokens[1].str());
string between = parser.substr_range_inclusive(tokens[0].str_index_last_no_whitespace() + 1,
tokens[1].str_index_start() - 1);
string trailing = parser.substr_range_inclusive(tokens[1].str_index_last_no_whitespace() + 1,
tokens[1].str_index_last());
string merged = first.substr(0, first.length() - 1) + second.substr(1) + between + trailing;
parser.replace_try(tokens[0], tokens[1], merged);
});
} while (parser.apply_mutations());
}
/* Turn assert into a printf. */
void SourceProcessor::lower_assert(Parser &parser, [[maybe_unused]] const string &filename)
{
/* Example: `assert(i < 0)` > `if (!(i < 0)) { printf(...); }` */
parser().foreach_match("A(..)", [&](const vector<Token> &tokens) {
if (tokens[0].str() != "assert") {
return;
}
string replacement;
#ifdef WITH_GPU_SHADER_ASSERT
string condition = string(tokens[1].scope().str());
auto escape = [](string s) {
string result;
for (char c : s) {
if (c == '%') {
result += "%%";
}
else if (c == '\\') {
result += "\\\\";
}
else if (c == '\"') {
result += "\\\"";
}
else {
result += c;
}
}
return result;
};
replacement += "if (!" + condition + ") ";
replacement += "{";
replacement += " printf(\"";
replacement += "Assertion failed: " + escape(condition) + ", ";
replacement += "file " + filename + ", ";
replacement += "line " + to_string(tokens[1].line_number()) + ", ";
replacement += "thread (%u,%u,%u).\\n";
replacement += "\"";
replacement += ", GPU_THREAD.x, GPU_THREAD.y, GPU_THREAD.z); ";
replacement += "}";
#endif
parser.replace(tokens[0], tokens[4], replacement);
});
parser.apply_mutations();
}
/* Replace string literals by their hash and store the original string in the file metadata. */
void SourceProcessor::lower_strings(Parser &parser)
{
parser().foreach_token(String, [&](const Token &token) {
if (token.scope().type() == ScopeType::Preprocessor) {
return;
}
uint32_t hash = hash_string(string(token.str()));
metadata::PrintfFormat format = {hash, string(token.str())};
metadata_.printf_formats.emplace_back(format);
parser.replace(token, "string_t(" + to_string(hash) + "u)", true);
});
parser.apply_mutations();
}
/* Change printf calls to "recursive" call to implementation functions.
* This allows to emulate the variadic arguments of printf. */
void SourceProcessor::lower_printf(Parser &parser)
{
parser().foreach_match("A(..)", [&](const vector<Token> &tokens) {
if (tokens[0].str() != "printf") {
return;
}
int arg_count = 0;
tokens[1].scope().foreach_scope(ScopeType::FunctionParam, [&](const Scope &) { arg_count++; });
string unrolled = "print_start(" + to_string(arg_count) + "u)";
tokens[1].scope().foreach_scope(ScopeType::FunctionParam, [&](const Scope &attribute) {
unrolled = "print_data(" + unrolled + ", " + string(attribute.str()) + ")";
});
parser.replace(tokens.front(), tokens.back(), unrolled);
});
parser.apply_mutations();
}
} // namespace blender::gpu::shader

View File

@@ -0,0 +1,543 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup shader_tool
*/
#include <unordered_set>
#include "intermediate.hh"
#include "metadata.hh"
#include "processor.hh"
namespace blender::gpu::shader {
using namespace std;
using namespace shader::parser;
using namespace metadata;
/* `class` -> `struct` */
void SourceProcessor::lower_classes(Parser &parser)
{
parser().foreach_token(Class, [&](const Token &token) {
if (token.prev() != Enum) {
parser.replace(token, "struct ");
}
});
}
/* Search for constructor definition in active code. These are not supported. */
void SourceProcessor::lint_constructors(Parser &parser)
{
parser().foreach_struct([&](Token, Scope, Token struct_name, Scope struct_scope) {
struct_scope.foreach_match("A(..)", [&](const Tokens &t) {
if (t[0].scope() != struct_scope) {
return;
}
if (t[0].str() == struct_name.str()) {
report_error(t[0], "Constructors are not supported.");
}
});
});
}
/* Forward declaration of types are not supported and makes no sense in a shader program where
* there is no pointers. */
void SourceProcessor::lint_forward_declared_structs(Parser &parser)
{
parser().foreach_match("sA;", [&](const Tokens &t) {
if (t[0].scope().type() == ScopeType::Global) {
report_error(t[0], "Forward declaration of types are not supported.");
}
});
}
/* Create default initializer (empty brace) for all classes. */
void SourceProcessor::lower_default_constructors(Parser &parser)
{
unordered_set<string> builtin_types = {
"bool32_t", "float2", "packed_float2", "float3", "packed_float3", "float4",
"packed_float4", "float2x2", "float2x3", "float2x4", "float3x2", "float3x3",
"float3x4", "float4x2", "float4x3", "float4x4", "float2x2", "float3x3",
"float4x4", "int2", "int3", "packed_int3", "int4", "uint2",
"uint3", "packed_uint3", "uint4", "bool2", "bool3", "bool4",
};
parser().foreach_struct([&](Token, Scope attributes, Token name, Scope body) {
/* Don't do host shared structures. */
if (attributes.is_valid()) {
return;
}
int decl_count = 0;
string decl;
body.foreach_declaration([&](Scope, Token, Token type, Scope, Token name, Scope array, Token) {
auto default_value = [&](const string_view type) -> string {
if (type == "float") {
return "0.0f";
}
if (type == "uint" || type == "uchar") {
return "0u";
}
if (type == "int" || type == "char") {
return "0";
}
if (type == "bool") {
return "false";
}
if (builtin_types.contains(string(type))) {
return string(type) + "(0)";
}
return string(type) + "{}";
};
if (array.is_valid()) {
int array_len = static_array_size(array, 0);
if (array_len == 0) {
decl += "for(int i=0;i < " + string(array.str_exclusive()) + ";i++){";
decl += "r." + string(name.str()) + "[i]=" + default_value(type.str()) + ";";
decl += "}";
}
else {
for (int i = 0; i < array_len; i++) {
decl += "r." + string(name.str()) + "[" + to_string(i) + "]";
decl += "=" + default_value(type.str()) + ";";
}
}
}
else {
/* Assigning members one by one as the foreach decl iterator can be out of order. */
decl += "r." + string(name.str()) + "=" + default_value(type.str()) + ";";
}
decl_count++;
});
if (decl_count == 0) {
/* Empty struct will have a padding int. */
decl += "r._pad=0;";
}
decl = "static " + string(name.str()) + " ctor_() {" + string(name.str()) + " r;" + decl +
"return r;}";
parser.insert_after(body.front().str_index_last_no_whitespace(), decl);
});
}
/* Make all members of a class to be referenced using `this->`. */
void SourceProcessor::lower_implicit_member(Parser &parser)
{
parser().foreach_struct([&](Token, Scope, Token, Scope body) {
vector<Token> members_tokens;
vector<Token> methods_tokens;
auto is_class_token = [&](const vector<Token> &members, const string_view token) {
for (const Token &member : members) {
if (token == member.str()) {
return true;
}
}
return false;
};
auto check_shadowing = [&](const Tokens &toks) {
if (is_class_token(members_tokens, toks[1].str())) {
report_error(toks[1], "Class member shadowing.");
}
};
body.foreach_declaration([&](Scope, Token, Token type, Scope, Token name, Scope, Token) {
/* Do not match legacy infos in order to allow resource getter to work. */
if (name.scope() == body && type.str() != "ShaderCreateInfo") {
members_tokens.emplace_back(name);
}
});
body.foreach_function(
[&](bool is_static, Token, Token fn_name, Scope fn_args, bool, Scope fn_body) {
if (is_static) {
return;
}
fn_args.foreach_match("AA", check_shadowing);
fn_args.foreach_match("&A", check_shadowing);
fn_body.foreach_match("AA", check_shadowing);
fn_body.foreach_match("&A", check_shadowing);
methods_tokens.emplace_back(fn_name);
});
body.foreach_function([&](bool is_static, Token, Token, Scope, bool, Scope fn_body) {
if (is_static) {
return;
}
fn_body.foreach_token(Word, [&](Token tok) {
if (!(tok.prev().prev() == '-' && tok.prev() == '>') && tok.prev() != Dot &&
/* Reject namespace qualified symbols. */
(tok.prev() != Colon || tok.prev().prev() != Colon))
{
bool is_method = tok.next() == '(';
if (tok.next() == '<' && !tok.next().followed_by_whitespace()) {
/* Might be templated method call. */
is_method = tok.next().scope().back().next() == '(';
}
if (!is_class_token(is_method ? methods_tokens : members_tokens, tok.str())) {
return;
}
parser.insert_before(tok, "this->");
}
});
});
});
parser.apply_mutations();
}
/* Move all method definition outside of struct definition blocks. */
void SourceProcessor::lower_method_definitions(Parser &parser)
{
/* NOTE: We need to avoid the case of `a * this->b` being replaced as 2 dereferences. */
/* `(*this)` -> `(this_)` */
parser().foreach_match("*T)", [&](const Tokens &t) { parser.replace(t[0], t[1], "this_"); });
/* `return *this;` -> `return this_;` */
parser().foreach_match("*T;", [&](const Tokens &t) { parser.replace(t[0], t[1], "this_"); });
/* `this->` -> `this_.` */
parser().foreach_match("T->", [&](const Tokens &t) { parser.replace(t[0], t[2], "this_."); });
parser.apply_mutations();
parser().foreach_match("sA:", [&](const Tokens &toks) {
if (toks[2] == ':') {
report_error(toks[2], "class inheritance is not supported");
return;
}
});
parser().foreach_match("cAA(..)c?{..}", [&](const Tokens &toks) {
if (toks[0].prev() == Const) {
report_error(toks[0],
"function return type is marked `const` but it makes no sense for values "
"and returning reference is not supported");
return;
}
});
/* Add `this` parameter and fold static keywords into function name. */
parser().foreach_struct([&](Token struct_tok,
Scope,
const Token struct_name,
const Scope struct_scope) {
const Scope attributes = struct_tok.prev().scope();
const bool is_resource_table = (attributes.type() == ScopeType::Subscript) &&
(attributes.str() == "[[resource_table]]");
if (is_resource_table) {
parser.replace(attributes, "");
}
struct_scope.foreach_function(
[&](bool is_static, Token fn_type, Token fn_name, Scope fn_args, bool is_const, Scope) {
const Token static_tok = is_static ? fn_type.prev() : Token(parser);
const Token const_tok = is_const ? fn_args.back().next() : Token(parser);
if (fn_name.str()[0] == '_') {
report_error(fn_name, "function name starting with an underscore are reserved");
}
if (is_static) {
parser.replace(
fn_name, string(struct_name.str()) + namespace_separator + string(fn_name.str()));
/* WORKAROUND: Erase the static keyword as it conflicts with the wrapper class
* member accesses MSL. */
parser.erase(static_tok);
}
else {
const bool has_no_args = fn_args.token_count() == 2;
const char *suffix = (has_no_args ? "" : ", ");
const string prefix = (is_resource_table ? "[[resource_table]] " : "");
/* Add a prefix to all member functions. */
parser.insert_before(fn_name, method_call_prefix);
parser.erase(const_tok);
if (is_const && !is_resource_table) {
parser.insert_after(fn_args.front(),
prefix + "const " + string(struct_name.str()) + " this_" +
suffix);
}
else {
parser.insert_after(fn_args.front(),
prefix + string(struct_name.str()) + " &this_" + suffix);
}
if (fn_name.str().length() > 1 &&
(fn_name.str().find_first_not_of("xyzw") == string::npos ||
fn_name.str().find_first_not_of("rgba") == string::npos))
{
report_error(fn_name, "Method name matching swizzles accessor are forbidden.");
}
}
});
});
parser.apply_mutations();
/* Copy method functions outside of struct scope. */
parser().foreach_struct([&](Token, Scope, const Token, const Scope struct_scope) {
const Token struct_end = struct_scope.back().next();
int method_len = 0;
struct_scope.foreach_function([&](bool, Token, Token, Scope, bool, Scope) { method_len++; });
if (method_len == 0) {
/* Avoid unnecessary preprocessor directives. */
return;
}
/* Add prototypes to allow arbitrary order of definition inside a class.
* Can be skipped if there is only one method. */
if (method_len > 1) {
/* First output prototypes. Not needed on metal because of wrapper class. */
parser.insert_after(struct_end, "\n#ifndef GPU_METAL\n");
struct_scope.foreach_function(
[&](bool is_static, Token fn_type, Token, Scope fn_args, bool, Scope) {
const Token fn_start = is_static ? fn_type.prev() : fn_type;
string proto_str = parser.substr_range_inclusive(fn_start, fn_args.back());
proto_str = strip_whitespace(proto_str) + ";\n";
Parser proto(proto_str, error_handler);
parser.insert_after(struct_end, proto.result_get());
});
parser.insert_after(struct_end, "#endif\n");
}
struct_scope.foreach_function(
[&](bool is_static, Token fn_type, Token, Scope, bool, Scope fn_body) {
const Token fn_start = is_static ? fn_type.prev() : fn_type;
string fn_str = parser.substr_range_inclusive(fn_start, fn_body.back());
fn_str = string(fn_start.char_number(), ' ') + fn_str + "\n";
parser.erase(fn_start, fn_body.back());
parser.insert_line_number(struct_end, fn_start.line_number());
parser.insert_after(struct_end, fn_str);
});
parser.insert_line_number(struct_end, struct_end.line_number(true));
});
parser.apply_mutations();
}
/* Transform `a.fn(b)` into `fn(a, b)`. */
void SourceProcessor::lower_method_calls(Parser &parser)
{
do {
parser().foreach_scope(ScopeType::Function, [&](Scope scope) {
scope.foreach_match(".A(", [&](const vector<Token> &tokens) {
const Token dot = tokens[0];
const Token func = tokens[1];
const Token par_open = tokens[2];
const Token end_of_this = dot.prev();
Token start_of_this = end_of_this;
while (true) {
if (start_of_this == ')') {
/* Function call. Take argument scope and function name. No recursion. */
start_of_this = start_of_this.scope().front().prev();
break;
}
if (start_of_this == ']') {
/* Array subscript. Take scope and continue. */
start_of_this = start_of_this.scope().front().prev();
continue;
}
if (start_of_this == Word) {
/* Member. */
if (start_of_this.prev() == '.') {
start_of_this = start_of_this.prev().prev();
/* Continue until we find root member. */
continue;
}
/* End of chain. */
break;
}
report_error(start_of_this.line_number(),
start_of_this.char_number(),
start_of_this.line_str(),
"lower_method_call parsing error");
break;
}
string this_str = parser.substr_range_inclusive(start_of_this, end_of_this);
string func_str = method_call_prefix + string(func.str());
const bool has_no_arg = par_open.next() == ')';
/* `a.fn(b)` -> `_fn(a, b)` */
parser.replace_try(
start_of_this, par_open, func_str + "(" + this_str + (has_no_arg ? "" : ", "));
});
});
} while (parser.apply_mutations());
}
void SourceProcessor::lower_structured_bindings(Parser &parser)
{
auto get_function_return_type = [&](string_view fn_name) {
string return_type;
parser().foreach_function([&](bool, Token type, Token name, Scope, bool, Scope) {
if (name.str() != fn_name) {
return;
}
return_type = type.str();
});
return return_type;
};
auto get_argument_type = [&](Scope args, string_view arg_name) {
string return_type;
args.foreach_scope(ScopeType::FunctionArg, [&](Scope arg) {
const Token name = arg.back();
if (name == ']') {
/* No array support for now. */
return;
}
if (name.str() == arg_name) {
Token type = name.prev() == '&' ? name.prev(2) : name.prev();
return_type = type.str();
}
});
return return_type;
};
auto get_local_symbol_type = [&](Scope fn_body, Token symbol) {
string return_type;
string_view symbol_str = symbol.str();
fn_body.foreach_token(Word, [&](Token tok) {
if (tok != Word || tok.str() != symbol_str || tok.index_ >= symbol.index_) {
return;
}
/* Check if it is in a visible scope. */
Scope tok_scope = tok.scope();
if (tok_scope != symbol.scope() && !tok_scope.contains(symbol.scope())) {
return;
}
/* Check if it is a definition. */
/* TODO(fclem): Comma declaration. */
bool is_reference = tok.prev() == Ampersand;
if (is_reference ? (tok.next() != '=' || tok.prev(2) != Word) : (tok.prev() != Word)) {
return;
}
Token type_tok = tok.prev(is_reference ? 2 : 1);
return_type = type_tok.str();
});
return return_type;
};
auto get_struct_members = [&](string_view struct_name) {
/* Search in symbol table first. */
for (const auto &symbol : metadata_.symbol_table) {
if (symbol.is_struct && symbol.identifier == struct_name) {
return symbol.members;
}
}
/* Search symbols inside this file. This can help find instanciated structs. */
vector<pair<string, string>> members;
parser().foreach_match("sA{", [&](const Tokens &t) {
if (t[1].str() != struct_name) {
return;
}
Scope body = t.back().scope();
body.foreach_declaration([&](Scope, Token, Token type, Scope, Token name, Scope, Token) {
members.emplace_back(type.str(), name.str());
});
});
return members;
};
parser().foreach_function([&](bool, Token, Token, Scope args, bool, Scope body) {
/* Unique index per binding to avoid to mess with scopes. */
int index = 0;
body.foreach_match("A[..]=", [&](const Tokens &t) {
if (t[0].str() != "auto") {
return;
}
Token symbol_name = t.back().next(1);
/* For now only support expanding form a single function call. */
if (symbol_name != Word) {
report_error(symbol_name, "Expected either a function call or a single variable");
return;
}
Token after_symbol = Token::invalid(&parser);
string struct_type;
if (symbol_name.next(1) == '(') {
after_symbol = symbol_name.next(1).scope().back().next();
if (after_symbol != ';') {
report_error(after_symbol, "Expected single function call");
return;
}
struct_type = get_function_return_type(symbol_name.str());
if (struct_type.empty()) {
report_error(symbol_name,
"Couldn't infer function return type. Can be caused by overload.");
return;
}
}
else {
after_symbol = symbol_name.next(1);
if (after_symbol != ';') {
report_error(after_symbol, "Expected single symbol to unpack");
return;
}
struct_type = get_local_symbol_type(body, symbol_name);
if (struct_type.empty()) {
struct_type = get_argument_type(args, symbol_name.str());
}
if (struct_type.empty()) {
report_error(symbol_name, "Couldn't infer local symbol type.");
return;
}
}
string struct_var = "_u" + to_string(index);
parser.replace(t[0], t[4], struct_type + " " + struct_var);
vector<pair<string, string>> struct_members = get_struct_members(struct_type);
if (struct_members.empty()) {
report_error(symbol_name, "Couldn't find type to unpack.");
return;
}
Scope var_list = t[1].scope();
string assignments = ";";
int member_index = 0;
var_list.foreach_token(Word, [&](Token tok) {
if (struct_members.size() > member_index) {
auto [member_type, member_name] = struct_members[member_index];
assignments += member_type + " " + string(tok.str()) + "=" + struct_var + "." +
member_name + ";";
member_index++;
}
else {
report_error(tok, "Too many parameters in structured binding");
}
});
if (struct_members.size() != member_index) {
report_error(t[4], "Missing parameters in structured binding");
}
/* Drop trailing semicolon. */
assignments = assignments.substr(0, assignments.size() - 1);
parser.insert_after(after_symbol.prev(), assignments);
index++;
});
});
parser.apply_mutations();
}
} // namespace blender::gpu::shader

View File

@@ -0,0 +1,511 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup shader_tool
*/
#include <algorithm>
#include <unordered_map>
#include "intermediate.hh"
#include "processor.hh"
namespace blender::gpu::shader {
using namespace std;
using namespace shader::parser;
using namespace metadata;
string SourceProcessor::template_arguments_mangle(const Scope template_args)
{
string args_concat;
template_args.foreach_scope(ScopeType::TemplateArg, [&](const Scope &scope) {
string str;
if (scope[1] == '<') {
str = string(scope[0].str()) + template_arguments_mangle(scope[1].scope());
}
else {
str = scope.str();
}
/* In order to support negative integer literals. Replace minus sign by underscore. */
replace(str.begin(), str.end(), '-', '_');
args_concat += 'T' + str;
});
return args_concat;
}
string SourceProcessor::template_full_specified_name(metadata::TemplateDefinition &template_def)
{
SourceProcessor::Parser name_parser(template_def.name_space + template_def.identifier,
error_handler);
lower_scope_resolution_operators(name_parser);
return name_parser.result_get();
}
static void parse_template_definition_args(const Scope arg,
vector<string> &arg_list,
const Scope fn_args,
bool &all_template_args_in_function_signature,
ErrorHandler &error_handler)
{
const Token type = arg.front();
const Token name = type.str() == "enum" ? type.next().next() : type.next();
const string_view name_str = name.str();
const string_view type_str = type.str();
arg_list.emplace_back(name_str);
if (type_str == "typename") {
bool found = false;
/* Search argument list for type-names. If type-name matches, the template argument is
* present inside the function signature. */
fn_args.foreach_match("AA", [&](const vector<Token> &tokens) {
if (tokens[0].str() == name_str) {
found = true;
}
});
fn_args.foreach_match("A&A", [&](const vector<Token> &tokens) {
if (tokens[0].str() == name_str) {
found = true;
}
});
all_template_args_in_function_signature &= found;
}
else if (type_str == "enum" || type_str == "bool") {
/* Values cannot be resolved using type deduction. */
all_template_args_in_function_signature = false;
}
else if (type_str == "int" || type_str == "uint" || type_str == "char" || type_str == "uchar" ||
type_str == "short" || type_str == "ushort")
{
/* Values cannot be resolved using type deduction. */
all_template_args_in_function_signature = false;
}
else {
error_handler.report(type, "Invalid template argument type");
}
}
void SourceProcessor::lower_template_instantiation(
SourceProcessor::Parser &parser,
/* If method, the end token of the template inside the struct. */
const Token method_end,
const Token &inst_start,
const Scope &inst_args,
const metadata::TemplateDefinition template_def,
const Token &symbol_name,
const vector<string> &arg_list,
const string &fn_decl,
const bool all_template_args_in_function_signature)
{
/* Note that we do not use the full path as file identifier. So all names are unique. */
string instance_filename = filepath_.substr(filepath_.find_last_of('/') + 1);
string template_filename = template_def.filepath.substr(template_def.filepath.find_last_of('/') +
1);
/* Avoid adding noise in the source file if instance is inside the same file as declaration. */
if (instance_filename == template_filename) {
instance_filename = "";
template_filename = "";
}
/* Parse template values. */
vector<pair<string, string>> arg_name_value_pairs;
{
int arg_count = 0;
inst_args.foreach_scope(ScopeType::TemplateArg, [&](const Scope &arg) {
if (arg_count < arg_list.size()) {
arg_name_value_pairs.emplace_back(arg_list[arg_count], arg.str());
}
arg_count++;
});
if (arg_count != arg_list.size()) {
report_error(inst_args.front(), "Invalid amount of argument in template instantiation.");
return;
}
}
/* Specialize template content. */
string instance_content;
{
SourceProcessor::Parser instance_parser(fn_decl, error_handler);
/* Inject namespace around definition for symbols namespaces resolution. */
if (template_def.name_space.empty()) {
instance_parser.insert_before(instance_parser.front(), "\n");
instance_parser.insert_after(instance_parser.back(), "\n");
}
else {
/* Remove suffix "::". */
string ns_name(template_def.name_space.substr(0, template_def.name_space.size() - 2));
if (template_def.is_method) {
size_t split = ns_name.rfind("::");
string struct_name;
if (split == string::npos) {
struct_name = ns_name;
ns_name = "";
}
else {
struct_name = ns_name.substr(split + 2);
ns_name = ns_name.substr(0, split);
}
if (!ns_name.empty()) {
instance_parser.insert_before(instance_parser.front(), "namespace " + ns_name + " {");
}
instance_parser.insert_before(instance_parser.front(), "struct " + struct_name + " {\n");
instance_parser.insert_after(instance_parser.back(), "\n};");
if (!ns_name.empty()) {
instance_parser.insert_after(instance_parser.back(), "}");
}
instance_parser.insert_after(instance_parser.back(), "\n");
}
else {
instance_parser.insert_before(instance_parser.front(), "namespace " + ns_name + " {\n");
instance_parser.insert_after(instance_parser.back(), "\n}\n");
}
}
/* Insert line directive. Important for symbol namespace resolution and error logging.
* Not using insert_line_number because it uses insert_after. */
string line_str = "\n#line " + std::to_string(template_def.definition_line);
if (!template_filename.empty()) {
line_str += " \"" + template_filename + '\"';
}
instance_parser.insert_before(instance_parser.front(), line_str + "\n");
instance_parser().foreach_token(Word, [&](const Token &word) {
string_view token_str = word.str();
/* Replace each parameter appearance inside the instance. */
for (const auto &arg_name_value : arg_name_value_pairs) {
if (token_str == arg_name_value.first) {
instance_parser.replace(word, arg_name_value.second, true);
}
}
/* Append template args after unspecified struct typename references.
* `A func(A<T> b) {}` > `A<T> func(A<T> b) {}`. */
if (template_def.is_struct && word.next() != AngleOpen && token_str == symbol_name.str()) {
instance_parser.insert_after(word.str_index_last_no_whitespace(),
SourceProcessor::template_arguments_mangle(inst_args));
}
});
/* Position of symbol name declaration. */
size_t symbol_name_pos = instance_parser.str().find(" " + string(symbol_name.str()));
/* Append template args after function name if needed. This is required by BSL specification.
* `void func() {}` > `void func<a, 1>() {}`. */
if (!template_def.is_struct && !all_template_args_in_function_signature) {
instance_parser.insert_after(symbol_name_pos + symbol_name.str().size(),
SourceProcessor::template_arguments_mangle(inst_args));
}
/* Append namespace to symbol name because the appended mangled arguments (above) make
* namespace resolution impossible. Methods do not need it because they are instanciated inside
* their struct. They will get the correct namespace prefix (if they are static) later on. */
if (!template_def.is_method) {
instance_parser.insert_after(symbol_name_pos, string(template_def.name_space));
}
instance_parser.apply_mutations();
lower_pre_template(instance_parser);
/* Paste template content in place of instantiation. */
instance_content = instance_parser.result_get();
/* Remove added first line from the injected namespace. */
instance_content = instance_content.substr(instance_content.find_first_of('\n') + 1);
if (template_def.is_method) {
/* Remove added last line from the injected struct + namespace. */
instance_content = instance_content.substr(
0, instance_content.find_last_of('\n', instance_content.size() - 2) + 1);
}
}
const Token inst_end = inst_start.find_next(SemiColon);
/* Method are put back in their classes. */
const Token insert_at = template_def.is_method ? method_end : inst_end;
/* Insert instantiation content. Instance line directived was already added. */
parser.insert_after(insert_at, instance_content);
parser.insert_line_number(insert_at, insert_at.line_number(true), instance_filename);
}
void SourceProcessor::lower_template_dependent_names(Parser &parser)
{
parser().foreach_match("tA<..>", [&](const Tokens &toks) {
if (toks[0].prev() == '.' || (toks[0].prev().prev() == '-' && toks[0].prev() == '>')) {
parser.erase(toks[0]);
}
});
}
void SourceProcessor::lower_pre_template(Parser &parser)
{
/* Lower noop attributes after linting them. */
lower_maybe_unused(parser);
/* Lint and remove C++ accessor templates before lowering template. */
lower_srt_accessor_templates(parser);
lower_union_accessor_templates(parser);
/* Lower namespaces. */
lower_using(parser);
lower_namespaces(parser);
lower_scope_resolution_operators(parser);
lower_template_calls(parser);
lower_template_specialization(parser);
}
/* Mangle template parameter into the symbol name. */
void SourceProcessor::lower_template_calls(Parser &parser)
{
/* Process templated function calls first to avoid matching them later. */
parser().foreach_match("A<..>(..)", [&](const vector<Token> &tokens) {
const Scope template_args = tokens[1].scope();
template_args.foreach_match("A<..>", [&parser](const vector<Token> &tokens) {
parser.replace(tokens[1].scope(), template_arguments_mangle(tokens[1].scope()), true);
});
});
/* Likewise, process templated struct method definitions. */
parser().foreach_match("A<..>A<", [&](const vector<Token> &tokens) {
parser.replace(tokens[1].scope(), template_arguments_mangle(tokens[1].scope()), true);
});
parser.apply_mutations();
}
void SourceProcessor::lower_template_specialization(Parser &parser)
{
auto process_specialization = [&](const Token specialization_start, const Scope template_args) {
parser.erase(specialization_start, specialization_start.next().next());
parser.replace(template_args, template_arguments_mangle(template_args), true);
};
parser().foreach_match("t<>AA<", [&](const vector<Token> &tokens) {
process_specialization(tokens[0], tokens[5].scope());
});
parser().foreach_match("t<>sA<..>", [&](const vector<Token> &tokens) {
process_specialization(tokens[0], tokens[5].scope());
});
parser.apply_mutations();
}
void SourceProcessor::process_template_struct(metadata::TemplateDefinition &template_def,
SourceProcessor::Parser &parser)
{
struct DefinitionParser {
SourceProcessor::Parser def_parser;
const Scope template_scope = def_parser[1].scope();
const Token struct_start = template_scope.back().next();
const Token struct_name = struct_start.next();
const Scope struct_body = struct_name.next().scope();
const Token struct_end = struct_body.back().next();
const string struct_decl = def_parser.substr_range_inclusive(struct_start, struct_end);
bool all_template_args_in_function_signature = false;
vector<string> arg_list;
/* Parse template declaration. */
DefinitionParser(metadata::TemplateDefinition &template_def, ErrorHandler report_error)
: def_parser(template_def.definition, report_error)
{
template_scope.foreach_scope(ScopeType::TemplateArg, [&](Scope arg) {
parse_template_definition_args(arg,
arg_list,
Scope(def_parser),
all_template_args_in_function_signature,
report_error);
});
}
};
/* Only parse if there is an instantiation. */
unique_ptr<DefinitionParser> def_parser;
/* Since we already lowered the namespaces in main parser, we need to search for the namespace
* resolved symbol name. */
const string full_specified_name = template_full_specified_name(template_def);
/* Replace instantiations. */
parser().foreach_match("tsA<", [&](const vector<Token> &tokens) {
if (full_specified_name != tokens[2].str()) {
return;
}
if (!def_parser) {
def_parser = make_unique<DefinitionParser>(template_def, error_handler);
}
lower_template_instantiation(parser,
Token::invalid(&parser),
tokens[0],
tokens[3].scope(),
template_def,
def_parser->struct_name,
def_parser->arg_list,
def_parser->struct_decl,
def_parser->all_template_args_in_function_signature);
});
}
void SourceProcessor::process_template_function(
metadata::TemplateDefinition &template_def,
SourceProcessor::Parser &parser,
/* If method, the end token of the template inside the struct. */
const Token method_end)
{
struct DefinitionParser {
SourceProcessor::Parser def_parser;
const Scope template_scope = def_parser[1].scope();
const Token fn_start = template_scope.back().next();
/* Skip attributes */
const Token after_attr = fn_start == SquareOpen ? fn_start.scope().back().next() : fn_start;
const Scope fn_args = after_attr.find_next(ParOpen).scope();
const Token fn_name = fn_args.front().prev();
const Token fn_end = fn_args.back().find_next(BracketOpen).scope().back();
const string fn_decl = def_parser.substr_range_inclusive(fn_start, fn_end);
bool all_template_args_in_function_signature = true;
vector<string> arg_list;
/* Parse template declaration. */
DefinitionParser(metadata::TemplateDefinition &template_def, ErrorHandler report_error)
: def_parser(template_def.definition, report_error)
{
assert(fn_start.is_valid() && fn_name.is_valid() && fn_args.is_valid() &&
template_scope.is_valid() && fn_end.is_valid());
template_scope.foreach_scope(ScopeType::TemplateArg, [&](Scope arg) {
parse_template_definition_args(
arg, arg_list, fn_args, all_template_args_in_function_signature, report_error);
});
}
};
/* Only parse if there is an instantiation. */
unique_ptr<DefinitionParser> def_parser;
/* Since we already lowered the namespaces in main parser, we need to search for the namespace
* resolved symbol name. */
const string full_specified_name = template_full_specified_name(template_def);
/* Replace instantiations. */
parser().foreach_match("tAA<", [&](const vector<Token> &tokens) {
if (full_specified_name != tokens[2].str()) {
return;
}
if (!def_parser) {
def_parser = make_unique<DefinitionParser>(template_def, error_handler);
}
lower_template_instantiation(parser,
method_end,
tokens[0],
tokens[3].scope(),
template_def,
def_parser->fn_name,
def_parser->arg_list,
def_parser->fn_decl,
def_parser->all_template_args_in_function_signature);
});
}
void SourceProcessor::lower_templates(Parser &parser)
{
/* Lint missing template arguments in instantiation and specialization.
* This is required by the BSL spec in order to simplify implementation. */
auto lint_explicit = [&](const Token symbol_name) {
if (symbol_name.next().scope().type() != parser::ScopeType::Template) {
report_error(
symbol_name,
"Template instantiation and specialization require explicit template arguments");
}
};
parser().foreach_match("t<>AA", [&](const vector<Token> &toks) { lint_explicit(toks[4]); });
parser().foreach_match("t<>A<..>A", [&](const vector<Token> &toks) { lint_explicit(toks[8]); });
parser().foreach_match("tAA", [&](const vector<Token> &toks) { lint_explicit(toks[2]); });
parser().foreach_match("tA<..>A", [&](const vector<Token> &toks) { lint_explicit(toks[6]); });
/* Delete templated struct and function definitions (not methods! see later).
* They were already parsed by `SourceProcessor::parse_namespace_symbols`. */
bool error = false;
parser().foreach_match("t<..>", [&](const vector<Token> &toks) {
/* Only process global templates, not methods. */
if (toks[0].scope() != parser()) {
return;
}
/* Default arguments are not supported. */
toks[1].scope().foreach_token(Assign, [&](Token tok) {
report_error(tok, "Default arguments are not supported inside template declaration");
error = true;
});
Token end = toks[0].find_next(BracketOpen).scope().back();
if (toks[4].next() == Struct) {
/* Capture end semicolon. */
end = end.next();
}
/* This can fail as it might try to erase templated method inside templated struct. */
parser.erase_try(toks[0], end);
});
parser.apply_mutations();
/* Deduplicate symbols (can happen because the main file is parsed twice). */
unordered_map<string, TemplateDefinition> unique_symbols;
for (const auto &symbol : metadata_.template_definitions) {
unique_symbols.try_emplace(symbol.name_space + symbol.identifier, symbol);
}
/* Process struct first, so methods can instantiate inside them. */
for (auto [_, template_def] : unique_symbols) {
if (template_def.is_struct) {
process_template_struct(template_def, parser);
}
}
parser.apply_mutations();
/* Then process methods. We can only process methods if their struct exists in the same file.
* This holds true for instanciated struct templates. */
parser().foreach_struct([&](Token, Scope, Token, Scope body) {
body.foreach_match("t<..>", [&](const vector<Token> &toks) {
/* Since this can be an instanciated struct, we need to make sure to instanciate its own
* methods. Hence the need to parse the definition. */
TemplateDefinition template_def = parse_template_definition(
parser, toks[0], true, toks[0].scope(), filepath_);
/* Insertion point of the instantiation. */
Token method_end = toks[0].find_next(BracketOpen).scope().back();
process_template_function(template_def, parser, method_end);
/* Delete definitions. */
parser.erase(toks[0], method_end);
});
});
/* For each template definition, process all instantiation. */
for (auto [_, template_def] : unique_symbols) {
if (!template_def.is_struct && !template_def.is_method) {
process_template_function(template_def, parser, Token::invalid(&parser));
}
}
parser.apply_mutations();
/* Remove template instantiation afterward. */
parser().foreach_token(Template, [&](const Token &tok) {
if (tok.next() == '<') {
report_error(tok, "Invalid template definition");
}
else {
parser.erase(tok, tok.find_next(SemiColon));
}
});
parser.apply_mutations();
/* Process calls to templated types or functions. */
parser().foreach_match("A<..>", [&](const vector<Token> &tokens) {
parser.replace(tokens[1].scope(), template_arguments_mangle(tokens[1].scope()), true);
});
parser.apply_mutations();
}
} // namespace blender::gpu::shader

View File

@@ -0,0 +1,35 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup shader_tools
*
* Lite duplicates of blender utility types and other utility functions.
* They are duplicated to avoid pulling half of blender as a dependency.
*/
#pragma once
#include <chrono>
namespace blender::gpu::shader::parser {
struct TimeIt {
using Duration = std::chrono::microseconds;
Duration &time;
std::chrono::high_resolution_clock::time_point start;
TimeIt(Duration &time) : time(time)
{
start = std::chrono::high_resolution_clock::now();
}
~TimeIt()
{
auto end = std::chrono::high_resolution_clock::now();
time = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
}
};
} // namespace blender::gpu::shader::parser

View File

@@ -0,0 +1,190 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup shader_tool
*
*/
#pragma once
#include "token_stream.hh"
namespace blender::gpu::shader::parser {
struct Scope;
/**
* Semantic token adding access to ParserBase data.
* It is also safer than lexit::Token as invalid token will not result invalid behavior.
*/
struct Token : lexit::Token {
Token(const lexit::Token &tok) : lexit::Token(tok) {}
Token(const ParserBase &data, int64_t index) : lexit::Token(&data, index) {}
/* Create an invalid token. */
Token(const ParserBase &data) : lexit::Token(&data, -1) {}
Token prev(int i = 1) const
{
return static_cast<const lexit::Token *>(this)->prev(i);
}
Token next(int i = 1) const
{
return static_cast<const lexit::Token *>(this)->next(i);
}
Token find_next(TokenType type) const
{
Token tok = this->next();
while (tok.is_valid() && tok != type) {
tok = tok.next();
}
return tok;
}
/* Return start of namespace identifier if the token is part of one. */
Token namespace_start() const
{
if (*this != Word) {
return *this;
}
/* Scan back identifier that could contain namespaces. */
Token tok = *this;
while (tok.is_valid()) {
if (tok.prev() == ':' && tok.prev(2) == ':') {
tok = tok.prev(3);
}
else {
return tok;
}
}
return tok;
}
/* For a word, return the name containing the prefix namespaces if present. */
std::string full_symbol_name() const
{
size_t start = namespace_start().str_index_start();
size_t end = str_index_last_no_whitespace();
return std::string(buf_->str_.substr(start, end - start + 1));
}
/* Returns the scope that contains this token. */
Scope scope() const;
size_t str_index_start() const
{
return buf_->offsets_[index_];
}
size_t str_index_last() const
{
return buf_->offsets_[index_ + 1] - 1;
}
size_t str_index_last_no_whitespace() const
{
return buf_->offsets_end_[index_] - 1;
}
/* Index of the first character of the line this token is. */
size_t line_start() const
{
size_t pos = buf_->str_.rfind('\n', str_index_start());
return (pos == std::string::npos) ? 0 : (pos + 1);
}
/* Index of the last character of the line this token is, excluding `\n`. */
size_t line_end() const
{
size_t pos = buf_->str_.find('\n', str_index_start());
return (pos == std::string::npos) ? (buf_->str_.size() - 1) : (pos - 1);
}
std::string_view str_with_whitespace() const
{
if (is_invalid()) {
return "";
}
return static_cast<const lexit::Token *>(this)->str_with_whitespace();
}
std::string_view str() const
{
if (is_invalid()) {
return "";
}
return static_cast<const lexit::Token *>(this)->str();
}
/* Return the line number this token is found at. Take into account the #line directives.
* If `at_end` is true, return the line number after this token. */
size_t line_number(bool at_end = false) const
{
if (is_invalid()) {
return 0;
}
int index = at_end ? str_index_last() : str_index_last_no_whitespace();
int line_num = parser::line_number(buf_->str_, index);
/* Add the last char (not counted by line_number). */
return line_num + int(at_end && buf_->str_[index] == '\n');
}
/* Return the offset to the start of the line. */
size_t char_number() const
{
if (is_invalid()) {
return 0;
}
return parser::char_number(buf_->str_, str_index_start());
}
/* Return the name of the file containing the token. */
std::string filename() const
{
if (is_invalid()) {
return "";
}
return parser::filename(buf_->str_, str_index_start());
}
/* Return the line the token is at. */
std::string line_str() const
{
return parser::line_str(buf_->str_, str_index_start());
}
TokenType type() const
{
if (is_invalid()) {
return Invalid;
}
return TokenType(buf_->types_[index_]);
}
/* Return the attribute scope before this token if it exists. */
Scope attribute_before() const;
/* Return the attribute scope after this token if it exists. */
Scope attribute_after() const;
bool operator==(TokenType type) const
{
return this->type() == type;
}
bool operator!=(TokenType type) const
{
return !(*this == type);
}
bool operator==(char type) const
{
return *this == TokenType(type);
}
bool operator!=(char type) const
{
return *this != TokenType(type);
}
};
} // namespace blender::gpu::shader::parser

View File

@@ -0,0 +1,147 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup shader_tool
*
*/
#pragma once
#include "enums.hh"
#include "utils.hh"
#include <array>
namespace blender::gpu::shader::parser {
struct Token;
struct Scope;
/**
* Turns string into token.
*/
struct LexerBase : lexit::TokenBuffer {
static const std::array<CharClass, 128> bsl_char_class_table;
static const std::array<CharClass, 128> default_char_class_table;
/** Compact visualization of token_types. */
std::string_view token_types_str() const
{
return std::string_view((const char *)types_.get(), size_);
}
/* Change words into keyword (ex: `if`, `struct`, `template`). */
void identify_keywords();
/* Change angle bracket tokens into template tokens if they match template condition. */
void identify_template_tokens();
/* Undo the changes from identify_template_tokens. */
void reset_template_tokens();
};
/**
* Only support rough tokenization.
*/
struct SimpleLexer {
static void lexical_analysis(LexerBase &lex, std::string_view input)
{
lex.process(input, LexerBase::bsl_char_class_table.data());
}
};
/**
* Identify BSL keywords, and correctly identify float literals.
*/
struct FullLexer {
static void lexical_analysis(LexerBase &lex, std::string_view input)
{
lex.process(input, LexerBase::bsl_char_class_table.data());
lex.merge_complex_literals();
lex.identify_keywords();
}
};
struct ParserBase;
struct ScopeLinks {
/* All in scope indices. */
int parent_ = -1;
int prev_ = -1;
int next_ = -1;
int child_first_ = -1;
int child_last_ = -1;
};
/**
* Create semantic scopes from token stream.
* Also creates mapping table from token to scope to have bi-directional mapping.
*/
struct ParserBase : LexerBase {
/** Compact visualization of scope_types. */
std::string_view scope_types_str;
/* --- Structure of Array style data for scopes. --- */
/** Range of token per scope. */
std::vector<ScopeType> scope_types;
/** Range of token per scope. */
std::vector<IndexRange> scope_ranges;
/** Index of adjacent scopes. */
std::vector<ScopeLinks> scope_links;
/** Index of bottom most scope per token. */
std::vector<int> token_scope;
/* Return the i'th token. */
Token operator[](int i) const;
void build_scope_tree(ErrorHandler &err_handler);
void build_token_to_scope_map();
private:
void update_string_view();
};
;
/* Don't do anything. No access to scopes is allowed. */
struct NullParser {
static void semantic_analysis(ParserBase &parser, ErrorHandler & /*err_handler*/)
{
parser.scope_types = {};
parser.scope_ranges = {};
}
};
/* Do not parse. Creates a single global scope containing all tokens. */
struct DummyParser {
static void semantic_analysis(ParserBase &parser, ErrorHandler & /*err_handler*/)
{
parser.scope_types = {ScopeType::Global};
parser.scope_ranges = {IndexRange(0, parser.size())};
parser.build_token_to_scope_map();
}
};
struct FullParser {
static void semantic_analysis(ParserBase &parser, ErrorHandler &err_handler)
{
parser.build_scope_tree(err_handler);
parser.build_token_to_scope_map();
}
};
template<typename LexerFn, typename ParserFn> struct Parser : ParserBase {
void lexical_analysis(std::string_view input)
{
LexerFn::lexical_analysis(*this, input);
}
void semantic_analysis(ErrorHandler &err_handler)
{
ParserFn::semantic_analysis(*this, err_handler);
}
};
} // namespace blender::gpu::shader::parser

View File

@@ -0,0 +1,461 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup shader_tool
*/
#include <unordered_map>
#include "intermediate.hh"
#include "metadata.hh"
#include "processor.hh"
namespace blender::gpu::shader {
using namespace std;
using namespace shader::parser;
using namespace metadata;
void SourceProcessor::lower_unions(Parser &parser)
{
struct Member {
string type, name;
size_t offset, size;
bool is_enum;
/* Return true for builtin trivial types (e.g. uint, float3). */
bool is_trivial() const
{
return type.empty();
}
};
auto is_struct_size_known = [&](Scope attributes) -> bool {
if (attributes.is_invalid()) {
return false;
}
bool is_shared = false;
attributes.foreach_attribute([&](Token attr, Scope) {
if (attr.str() == "host_shared") {
is_shared = true;
}
});
if (!is_shared) {
return false;
}
return true;
};
/* Description of each union types. */
unordered_map<string, vector<Member>> union_members;
/* First, lower anonymous unions into separate struct. */
parser().foreach_struct([&](Token struct_tok, Scope attrs, Token struct_name, Scope body) {
int union_index = 0;
body.foreach_match("o{..};", [&](const Tokens &t) {
Scope union_body = t[1].scope();
string union_name = "union" + to_string(union_index);
string union_type = string(struct_name.str()) + "_" + union_name;
/* Parse members of the union for later use. */
vector<Member> members;
union_body.foreach_declaration(
[&](Scope, Token, Token type, Scope, Token name, Scope array, Token) {
if (array.is_valid()) {
report_error(name, "Arrays are not supported inside unions.");
}
members.emplace_back(
Member{string(type.str()), string(name.str()), 0, 0, type.prev() == Enum});
});
if (members.empty()) {
report_error(t[0], "Empty union");
return;
}
union_members.emplace(union_type, members);
string union_member = union_type + " " + union_name + ";";
if (attrs.contains("host_shared")) {
union_member = "struct " + union_member;
}
parser.insert_before(t.front(), union_member);
parser.erase(t.front(), t.back());
string type_decl = "struct [[host_shared]] " + union_type + " {\n";
/* Temporary storage (use first member, still valid since all members should have the same
* size). The real storage can only be set once we know the size of the union, which we can
* only know after lowering them as outside types. */
type_decl += " " + members.front().type + " " + members.front().name + ";\n";
type_decl += "};\n";
parser.insert_line_number(struct_tok.str_index_start() - 1, t[0].line_number());
parser.insert_before(struct_tok, type_decl);
parser.insert_line_number(struct_tok.str_index_start() - 1, struct_tok.line_number());
union_index++;
});
});
parser.apply_mutations();
/* Map structure name to structure members. */
unordered_map<string, vector<Member>> struct_members = {
{"float", {{"", "", 0, 4}}},
{"float2", {{"", "", 0, 8}}},
{"float4", {{"", "", 0, 16}}},
{"bool32_t", {{"", "", 0, 4}}},
{"int", {{"", "", 0, 4}}},
{"int2", {{"", "", 0, 8}}},
{"int4", {{"", "", 0, 16}}},
{"uint", {{"", "", 0, 4}}},
{"uint2", {{"", "", 0, 8}}},
{"uint4", {{"", "", 0, 16}}},
{"string_t", {{"", "", 0, 4}}},
{"packed_float3", {{"", "", 0, 12}}},
{"packed_int3", {{"", "", 0, 12}}},
{"packed_uint3", {{"", "", 0, 12}}},
{"float2x4", {{"float4", "[0]", 0, 16}, {"float4", "[1]", 16, 16}}},
{"float3x4",
{{"float4", "[0]", 0, 16}, {"float4", "[1]", 16, 16}, {"float4", "[2]", 32, 16}}},
{"float4x4",
{{"float4", "[0]", 0, 16},
{"float4", "[1]", 16, 16},
{"float4", "[2]", 32, 16},
{"float4", "[3]", 48, 16}}},
};
auto type_size_get = [&](Token type) -> size_t {
auto value = struct_members.find(string(type.str()));
if (value == struct_members.end()) {
return 0;
}
int total_size = 0;
for (const Member &member : value->second) {
total_size += member.size;
}
return total_size;
};
/* Then populate struct members. */
parser().foreach_struct([&](Token, Scope attributes, Token struct_name, Scope body) {
if (!is_struct_size_known(attributes)) {
return;
}
vector<Member> members;
size_t offset = 0;
body.foreach_declaration([&](Scope, Token, Token type, Scope, Token name, Scope array, Token) {
size_t size = 4;
size_t array_size = 0;
if (array.is_valid()) {
/* Assume size to be zero by default. It will create invalid size error later on. */
array_size = static_array_size(array, 0);
}
else {
array_size = 1;
}
for (int i = 0; i < array_size; i++) {
string name_str(name.str());
if (array.is_valid()) {
name_str += "[" + to_string(i) + "]";
}
if (type.prev() != Enum) {
size = type_size_get(type);
if (size != 0) {
members.emplace_back(Member{string(type.str()), "." + name_str, offset, size});
}
}
else {
members.emplace_back(Member{string(type.str()), "." + name_str, offset, size, true});
}
offset += size;
}
});
struct_members.emplace(struct_name.str(), members);
});
/* Replace placeholder struct with a generic one. */
auto replace_placeholder_member = [&](Scope body) {
/* Replace placeholder struct with float members. */
size_t size = type_size_get(body.front().next());
if (size == 0) {
report_error(body.front().next(),
"Can't infer size of member. Type must be defined in this file and have "
"the [[host_shared]] attribute.");
}
for (int i = 0; i < size; i += 16) {
size_t member_size = size - i;
const char *data_type = "float4";
if (member_size == 4) {
data_type = "float";
}
else if (member_size == 8) {
data_type = "float2";
}
else if (member_size == 12) {
data_type = "float3";
}
parser.insert_after(body.front().str_index_last_no_whitespace(),
"\n " + string(data_type) + " data" + to_string(i / 16) + ";");
}
parser.erase(body.front().next(), body.back().prev());
};
auto member_from_float =
[&](const Member &union_member, const Member &struct_member, const string &access) {
/* Account for trivial types. */
const string &type = struct_member.is_trivial() ? union_member.type : struct_member.type;
bool is_enum = struct_member.is_trivial() ? union_member.is_enum : struct_member.is_enum;
if (is_enum) {
return struct_member.type + "(floatBitsToUint(" + access + "))";
}
if (type.starts_with("uint")) {
return "floatBitsToUint(" + access + ")";
}
if (type.starts_with("int")) {
return "floatBitsToInt(" + access + ")";
}
if (type == "bool") {
return "floatBitsToInt(" + access + ") != 0";
}
return access;
};
auto member_to_float =
[&](const Member &union_member, const Member &struct_member, const string &access) {
/* Account for trivial types. */
const string &type = struct_member.is_trivial() ? union_member.type : struct_member.type;
bool is_enum = struct_member.is_trivial() ? union_member.is_enum : struct_member.is_enum;
if (is_enum) {
return "uintBitsToFloat(uint(" + access + "))";
}
if (type.starts_with("uint")) {
return "uintBitsToFloat(" + access + ")";
}
if (type.starts_with("int")) {
return "intBitsToFloat(" + access + ")";
}
if (type == "bool") {
return "intBitsToFloat(int(" + access + "))";
}
return access;
};
auto union_data_access = [&](const Member &struct_member, size_t union_size) {
const size_t offset = struct_member.offset;
string access = ".data" + to_string(offset / 16);
if (struct_member.size == 12) {
access += ".xyz";
}
else if (struct_member.size == 8) {
access += ((offset % 16) == 0) ? ".xy" : ".zw";
}
else if (struct_member.size == 4) {
switch (offset % 16) {
case 0:
/* Special case if last member is a scalar. */
access += ((union_size - offset) == 4) ? "" : ".x";
break;
case 4:
access += ".y";
break;
case 8:
access += ".z";
break;
case 12:
access += ".w";
break;
}
}
return access;
};
auto member_data_access = [&](const Member &struct_member) -> string {
return struct_member.is_trivial() ? string() : struct_member.name;
};
auto create_getter = [&](/* Tokens of the union declaration inside the struct. */
const Token &union_type_tok,
const Token &union_var_tok,
/* Union member we are creating the accessor for. */
const Member &union_member,
/* Definition of the type of the accessed member. */
const vector<Member> &struct_members) -> string {
const size_t union_size = type_size_get(union_type_tok);
if (union_size == 0) {
report_error(union_type_tok,
"Can't infer size of member. Type must be defined in this file and have "
"the [[host_shared]] attribute.");
return "";
}
const Member &last_member = struct_members.back();
if (last_member.offset + last_member.size != union_size) {
report_error(union_type_tok, "union has members of different sizes");
return "";
}
string fn_body = "{\n";
/* Declare return variable of the same type as the accessed member. */
fn_body += " " + union_member.type + " val;\n";
for (const auto &member : struct_members) {
string to_var = "val" + member_data_access(member);
string access = "this_." + string(union_var_tok.str()) +
union_data_access(member, union_size);
fn_body += " " + to_var + " = " + member_from_float(union_member, member, access) + ";\n";
}
fn_body += " return val;\n";
fn_body += "}\n";
return "\n" + union_member.type + " " + union_member.name + "() const " + fn_body;
};
auto create_setter = [&](/* Tokens of the union declaration inside the struct. */
const Token &union_type_tok,
const Token &union_var_tok,
/* Union member we are creating the accessor for. */
const Member &union_member,
/* Definition of the type of the accessed member. */
const vector<Member> &struct_members) -> string {
const size_t union_size = type_size_get(union_type_tok);
if (union_size == 0) {
report_error(union_type_tok,
"Can't infer size of member. Type must be defined in this file and have "
"the [[host_shared]] attribute.");
return "";
}
const Member &last_member = struct_members.back();
if (last_member.offset + last_member.size != union_size) {
report_error(union_type_tok, "union has members of different sizes");
return "";
}
string fn_body = "{\n";
for (const auto &member : struct_members) {
string to_var = "this->" + string(union_var_tok.str()) +
union_data_access(member, union_size);
string access = "value" + member_data_access(member);
fn_body += " " + to_var + " = " + member_to_float(union_member, member, access) + ";\n";
}
fn_body += "}\n";
return "\nvoid " + union_member.name + "_set_(" + union_member.type + " value) " + fn_body;
};
auto flatten_members = [&](Token type, vector<Member> &members) {
vector<Member> dst;
dst.reserve(members.size());
bool expanded = false;
for (const auto &member : members) {
if (member.is_trivial() || member.is_enum) {
dst.emplace_back(member);
continue;
}
if (!struct_members.contains(member.type)) {
report_error(
type,
"Unknown type encountered while unwrapping union. Contained types must be defined "
"in this file and decorated with [[host_shared]] attribute.");
continue;
}
vector<Member> nested_structure = struct_members.find(member.type)->second;
for (Member nested_member : nested_structure) {
if (nested_member.is_trivial() || nested_member.is_enum) {
dst.emplace_back(member);
}
else {
expanded = true;
nested_member.name = member.name + nested_member.name;
nested_member.offset = member.offset + nested_member.offset;
dst.emplace_back(nested_member);
}
}
}
members = dst;
return expanded;
};
parser().foreach_struct([&](Token, Scope, Token struct_name, Scope body) {
if (union_members.contains(string(struct_name.str()))) {
replace_placeholder_member(body);
return;
}
body.foreach_declaration([&](Scope, Token, Token type, Scope, Token name, Scope, Token) {
if (!union_members.contains(string(type.str()))) {
return;
}
const vector<Member> &members = union_members.find(string(type.str()))->second;
for (const auto &member : members) {
if (!struct_members.contains(member.type)) {
report_error(
type,
"Unknown union member type. Type must be defined in this file and decorated "
"with [[host_shared]] attribute.");
return;
}
vector<Member> structure = struct_members.find(member.type)->second;
/* Flatten references to other structures, recursively. */
while (flatten_members(type, structure)) {
}
parser.insert_after(body.back().prev(), create_getter(type, name, member, structure));
parser.insert_after(body.back().prev(), create_setter(type, name, member, structure));
}
});
});
/* Replace assignment pattern.
* Example: `a.b() = c;` > `a.b_set_(c);`
* This pattern is currently only allowed for `union_t`. */
parser().foreach_match("A()=", [&](const Tokens &t) {
parser.insert_before(t[1], "_set_");
parser.erase(t[2], t[3]);
parser.insert_after(t[3].scope().back(), ")");
});
parser.apply_mutations();
}
/**
* For safety reason, union members need to be declared with the union_t template.
* This avoid raw member access which we cannot emulate. Instead this forces the use of the `()`
* operator for accessing the members of the enum.
*
* Need to run before lower_unions.
*/
void SourceProcessor::lower_union_accessor_templates(Parser &parser)
{
parser().foreach_struct([&](Token, Scope, Token, Scope body) {
body.foreach_match("o{..};", [&](const Tokens &t) {
t[1].scope().foreach_declaration(
[&](Scope, Token, Token type, Scope template_scope, Token name, Scope, Token) {
if (type.str() != "union_t") {
report_error(
name,
"All union members must have their type wrapped using the union_t<T> template.");
parser.erase(type, type.find_next(SemiColon));
return;
}
/* Remove the template but not the wrapped type. */
parser.erase(type);
if (template_scope.is_valid()) {
parser.erase(template_scope.front());
parser.erase(template_scope.back());
}
});
});
});
parser.apply_mutations();
}
} // namespace blender::gpu::shader

View File

@@ -0,0 +1,131 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup shader_tools
*
* Lite duplicates of blender utility types and other utility functions.
* They are duplicated to avoid pulling half of blender as a dependency.
*/
#pragma once
#include <cstddef>
#include <cstdint>
#include <optional>
#include <string>
namespace blender::gpu::shader::parser {
struct Token;
struct ErrorHandler {
struct Error {
/* Contain only the message passed to the report function. */
std::string message;
/* Contains filename, token location, source line and cursor. */
std::string full_report;
};
std::string default_filename;
std::optional<Error> err;
void report(Token tok, std::string_view message);
void report(int row, int column, std::string line, std::string_view message);
void reset()
{
err.reset();
}
};
/** Poor man's IndexRange. */
struct IndexRange {
int64_t start;
int64_t size;
IndexRange(int64_t start, int64_t size) : start(start), size(size) {}
bool overlaps(IndexRange other) const
{
if (start == other.start && size == other.size) {
return true;
}
return ((start < other.start) && (other.start < (start + size))) ||
((other.start < start) && (start < (other.start + other.size)));
}
int64_t last() const
{
return start + size - 1;
}
};
/** Poor man's MutableSpan. */
template<typename T> struct MutableSpan {
T *data_;
uint64_t size_;
T &operator[](const int64_t index)
{
return data_[index];
}
const T &operator[](const int64_t index) const
{
return data_[index];
}
T *data()
{
return data_;
}
uint64_t size() const
{
return size_;
}
T back() const
{
return (*this)[size_ - 1];
}
T *begin()
{
return data_;
}
T *end()
{
return data_ + size_;
}
const T *begin() const
{
return data_;
}
const T *end() const
{
return data_ + size_;
}
/**
* Set span size to a smaller size, this invokes undefined behavior when n is negative or bigger
* than the current span.
*/
void shrink(int64_t new_size)
{
size_ = new_size;
}
};
/** Return the line number this token is found at. Take into account the #line directives. */
size_t line_number(const std::string_view &str, size_t pos);
/** Return the offset to the start of the line. */
size_t char_number(const std::string_view &str, size_t pos);
/** Return the filename at this position. Take into account the #line directives. */
std::string filename(const std::string_view &str, size_t pos);
/** Returns a string of the line containing the character at the given position. */
std::string line_str(const std::string_view &str, size_t pos);
} // namespace blender::gpu::shader::parser