mirror of
https://github.com/encounter/dawn-cmake.git
synced 2025-06-06 22:53:35 +00:00
Resolver: Check that every AST node is reached once
AST nodes must not be shared. Diamonds in the AST will cause all sorts of exciting, non trivial bugs. All AST nodes must be reached by the Resolver. There are two common reasons why they may not be: (a) They were constructed and not attached to the AST. Several transforms scan the full list of constructed AST nodes to find nodes of a given type. Having detached nodes will likely cause bugs in these transforms. Detached nodes is also just a waste of memory. (b) They are attached to the AST, but the resolver did not traverse them. Having the resolver skip over parts of the AST will fail to catch validation issues, and will leave semantic gaps, likely breaking downstream logic. Bug: tint:469 Change-Id: I143b84fd830699f874d2936146f0e93197db610c Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/47778 Kokoro: Kokoro <noreply+kokoro@google.com> Commit-Queue: Ben Clayton <bclayton@chromium.org> Reviewed-by: Antonio Maiorano <amaiorano@google.com>
This commit is contained in:
parent
b8ea59149e
commit
6fcefe4c25
@ -420,6 +420,20 @@ INSTANTIATE_TEST_SUITE_P(
|
|||||||
Params{ty_mat3x3<f32>, (default_mat3x3.align - 1) * 7, false},
|
Params{ty_mat3x3<f32>, (default_mat3x3.align - 1) * 7, false},
|
||||||
Params{ty_mat4x4<f32>, (default_mat4x4.align - 1) * 7, false}));
|
Params{ty_mat4x4<f32>, (default_mat4x4.align - 1) * 7, false}));
|
||||||
|
|
||||||
|
TEST_F(ArrayStrideTest, MultipleDecorations) {
|
||||||
|
auto* arr = create<type::Array>(ty.i32(), 4,
|
||||||
|
ast::DecorationList{
|
||||||
|
create<ast::StrideDecoration>(4),
|
||||||
|
create<ast::StrideDecoration>(4),
|
||||||
|
});
|
||||||
|
|
||||||
|
Global(Source{{12, 34}}, "myarray", arr, ast::StorageClass::kInput);
|
||||||
|
|
||||||
|
EXPECT_FALSE(r()->Resolve());
|
||||||
|
EXPECT_EQ(r()->error(),
|
||||||
|
"12:34 error: array must have at most one [[stride]] decoration");
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
} // namespace ArrayStrideTests
|
} // namespace ArrayStrideTests
|
||||||
} // namespace resolver
|
} // namespace resolver
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
|
#include "src/ast/access_decoration.h"
|
||||||
#include "src/ast/assignment_statement.h"
|
#include "src/ast/assignment_statement.h"
|
||||||
#include "src/ast/bitcast_expression.h"
|
#include "src/ast/bitcast_expression.h"
|
||||||
#include "src/ast/break_statement.h"
|
#include "src/ast/break_statement.h"
|
||||||
@ -178,20 +179,22 @@ bool Resolver::IsValidAssignment(type::Type* lhs, type::Type* rhs) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool Resolver::ResolveInternal() {
|
bool Resolver::ResolveInternal() {
|
||||||
|
Mark(&builder_->AST());
|
||||||
|
|
||||||
// Process everything else in the order they appear in the module. This is
|
// Process everything else in the order they appear in the module. This is
|
||||||
// necessary for validation of use-before-declaration.
|
// necessary for validation of use-before-declaration.
|
||||||
for (auto* decl : builder_->AST().GlobalDeclarations()) {
|
for (auto* decl : builder_->AST().GlobalDeclarations()) {
|
||||||
if (decl->Is<type::Type>()) {
|
if (auto* ty = decl->As<type::Type>()) {
|
||||||
if (auto* str = decl->As<type::Struct>()) {
|
if (!Type(ty)) {
|
||||||
if (!Structure(str)) {
|
return false;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else if (auto* func = decl->As<ast::Function>()) {
|
} else if (auto* func = decl->As<ast::Function>()) {
|
||||||
|
Mark(func);
|
||||||
if (!Function(func)) {
|
if (!Function(func)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else if (auto* var = decl->As<ast::Variable>()) {
|
} else if (auto* var = decl->As<ast::Variable>()) {
|
||||||
|
Mark(var);
|
||||||
if (!GlobalVariable(var)) {
|
if (!GlobalVariable(var)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -202,6 +205,34 @@ bool Resolver::ResolveInternal() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (auto* node : builder_->ASTNodes().Objects()) {
|
||||||
|
if (marked_.count(node) == 0) {
|
||||||
|
if (node->Is<ast::AccessDecoration>()) {
|
||||||
|
// These are generated by the WGSL parser, used to build
|
||||||
|
// type::AccessControls and then leaked.
|
||||||
|
// Once we introduce AST types, this should be fixed.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
TINT_ICE(diagnostics_) << "AST node '" << node->TypeInfo().name
|
||||||
|
<< "' was not reached by the resolver\n"
|
||||||
|
<< "At: " << node->source();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Resolver::Type(type::Type* ty) {
|
||||||
|
ty = ty->UnwrapAliasIfNeeded();
|
||||||
|
if (auto* str = ty->As<type::Struct>()) {
|
||||||
|
if (!Structure(str)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if (auto* arr = ty->As<type::Array>()) {
|
||||||
|
if (!Array(arr, Source{})) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -275,6 +306,7 @@ bool Resolver::GlobalVariable(ast::Variable* var) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (auto* deco : var->decorations()) {
|
for (auto* deco : var->decorations()) {
|
||||||
|
Mark(deco);
|
||||||
if (!(deco->Is<ast::BindingDecoration>() ||
|
if (!(deco->Is<ast::BindingDecoration>() ||
|
||||||
deco->Is<ast::BuiltinDecoration>() ||
|
deco->Is<ast::BuiltinDecoration>() ||
|
||||||
deco->Is<ast::ConstantIdDecoration>() ||
|
deco->Is<ast::ConstantIdDecoration>() ||
|
||||||
@ -287,6 +319,7 @@ bool Resolver::GlobalVariable(ast::Variable* var) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (var->has_constructor()) {
|
if (var->has_constructor()) {
|
||||||
|
Mark(var->constructor());
|
||||||
if (!Expression(var->constructor())) {
|
if (!Expression(var->constructor())) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -570,10 +603,17 @@ bool Resolver::Function(ast::Function* func) {
|
|||||||
|
|
||||||
variable_stack_.push_scope();
|
variable_stack_.push_scope();
|
||||||
for (auto* param : func->params()) {
|
for (auto* param : func->params()) {
|
||||||
|
Mark(param);
|
||||||
auto* param_info = Variable(param);
|
auto* param_info = Variable(param);
|
||||||
if (!param_info) {
|
if (!param_info) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(amaiorano): Validate parameter decorations
|
||||||
|
for (auto* deco : param->decorations()) {
|
||||||
|
Mark(deco);
|
||||||
|
}
|
||||||
|
|
||||||
variable_stack_.set(param->symbol(), param_info);
|
variable_stack_.set(param->symbol(), param_info);
|
||||||
func_info->parameters.emplace_back(param_info);
|
func_info->parameters.emplace_back(param_info);
|
||||||
|
|
||||||
@ -642,12 +682,20 @@ bool Resolver::Function(ast::Function* func) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (func->body()) {
|
if (func->body()) {
|
||||||
|
Mark(func->body());
|
||||||
if (!BlockStatement(func->body())) {
|
if (!BlockStatement(func->body())) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
variable_stack_.pop_scope();
|
variable_stack_.pop_scope();
|
||||||
|
|
||||||
|
for (auto* deco : func->decorations()) {
|
||||||
|
Mark(deco);
|
||||||
|
}
|
||||||
|
for (auto* deco : func->return_type_decorations()) {
|
||||||
|
Mark(deco);
|
||||||
|
}
|
||||||
|
|
||||||
if (!ValidateFunction(func)) {
|
if (!ValidateFunction(func)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -668,6 +716,7 @@ bool Resolver::BlockStatement(const ast::BlockStatement* stmt) {
|
|||||||
|
|
||||||
bool Resolver::Statements(const ast::StatementList& stmts) {
|
bool Resolver::Statements(const ast::StatementList& stmts) {
|
||||||
for (auto* stmt : stmts) {
|
for (auto* stmt : stmts) {
|
||||||
|
Mark(stmt);
|
||||||
if (!Statement(stmt)) {
|
if (!Statement(stmt)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -698,6 +747,7 @@ bool Resolver::Statement(ast::Statement* stmt) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (auto* c = stmt->As<ast::CallStatement>()) {
|
if (auto* c = stmt->As<ast::CallStatement>()) {
|
||||||
|
Mark(c->expr());
|
||||||
return Expression(c->expr());
|
return Expression(c->expr());
|
||||||
}
|
}
|
||||||
if (auto* c = stmt->As<ast::CaseStatement>()) {
|
if (auto* c = stmt->As<ast::CaseStatement>()) {
|
||||||
@ -732,11 +782,15 @@ bool Resolver::Statement(ast::Statement* stmt) {
|
|||||||
// these would make their BlockInfo siblings as in the AST, but we want the
|
// these would make their BlockInfo siblings as in the AST, but we want the
|
||||||
// body BlockInfo to parent the continuing BlockInfo for semantics and
|
// body BlockInfo to parent the continuing BlockInfo for semantics and
|
||||||
// validation. Also, we need to set their types differently.
|
// validation. Also, we need to set their types differently.
|
||||||
|
Mark(l->body());
|
||||||
return BlockScope(l->body(), BlockInfo::Type::kLoop, [&] {
|
return BlockScope(l->body(), BlockInfo::Type::kLoop, [&] {
|
||||||
if (!Statements(l->body()->list())) {
|
if (!Statements(l->body()->list())) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (l->continuing()) { // has_continuing() also checks for empty()
|
||||||
|
Mark(l->continuing());
|
||||||
|
}
|
||||||
if (l->has_continuing()) {
|
if (l->has_continuing()) {
|
||||||
if (!BlockScope(l->continuing(), BlockInfo::Type::kLoopContinuing,
|
if (!BlockScope(l->continuing(), BlockInfo::Type::kLoopContinuing,
|
||||||
[&] { return Statements(l->continuing()->list()); })) {
|
[&] { return Statements(l->continuing()->list()); })) {
|
||||||
@ -764,11 +818,16 @@ bool Resolver::Statement(ast::Statement* stmt) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool Resolver::CaseStatement(ast::CaseStatement* stmt) {
|
bool Resolver::CaseStatement(ast::CaseStatement* stmt) {
|
||||||
|
Mark(stmt->body());
|
||||||
|
for (auto* sel : stmt->selectors()) {
|
||||||
|
Mark(sel);
|
||||||
|
}
|
||||||
return BlockScope(stmt->body(), BlockInfo::Type::kSwitchCase,
|
return BlockScope(stmt->body(), BlockInfo::Type::kSwitchCase,
|
||||||
[&] { return Statements(stmt->body()->list()); });
|
[&] { return Statements(stmt->body()->list()); });
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Resolver::IfStatement(ast::IfStatement* stmt) {
|
bool Resolver::IfStatement(ast::IfStatement* stmt) {
|
||||||
|
Mark(stmt->condition());
|
||||||
if (!Expression(stmt->condition())) {
|
if (!Expression(stmt->condition())) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -781,11 +840,13 @@ bool Resolver::IfStatement(ast::IfStatement* stmt) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Mark(stmt->body());
|
||||||
if (!BlockStatement(stmt->body())) {
|
if (!BlockStatement(stmt->body())) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (auto* else_stmt : stmt->else_statements()) {
|
for (auto* else_stmt : stmt->else_statements()) {
|
||||||
|
Mark(else_stmt);
|
||||||
// Else statements are a bit unusual - they're owned by the if-statement,
|
// Else statements are a bit unusual - they're owned by the if-statement,
|
||||||
// not a BlockStatement.
|
// not a BlockStatement.
|
||||||
constexpr ast::BlockStatement* no_block_statement = nullptr;
|
constexpr ast::BlockStatement* no_block_statement = nullptr;
|
||||||
@ -793,9 +854,13 @@ bool Resolver::IfStatement(ast::IfStatement* stmt) {
|
|||||||
builder_->create<sem::Statement>(else_stmt, no_block_statement);
|
builder_->create<sem::Statement>(else_stmt, no_block_statement);
|
||||||
builder_->Sem().Add(else_stmt, sem_else_stmt);
|
builder_->Sem().Add(else_stmt, sem_else_stmt);
|
||||||
ScopedAssignment<sem::Statement*> sa(current_statement_, sem_else_stmt);
|
ScopedAssignment<sem::Statement*> sa(current_statement_, sem_else_stmt);
|
||||||
if (!Expression(else_stmt->condition())) {
|
if (auto* cond = else_stmt->condition()) {
|
||||||
return false;
|
Mark(cond);
|
||||||
|
if (!Expression(cond)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Mark(else_stmt->body());
|
||||||
if (!BlockStatement(else_stmt->body())) {
|
if (!BlockStatement(else_stmt->body())) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -805,6 +870,7 @@ bool Resolver::IfStatement(ast::IfStatement* stmt) {
|
|||||||
|
|
||||||
bool Resolver::Expressions(const ast::ExpressionList& list) {
|
bool Resolver::Expressions(const ast::ExpressionList& list) {
|
||||||
for (auto* expr : list) {
|
for (auto* expr : list) {
|
||||||
|
Mark(expr);
|
||||||
if (!Expression(expr)) {
|
if (!Expression(expr)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -813,11 +879,6 @@ bool Resolver::Expressions(const ast::ExpressionList& list) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool Resolver::Expression(ast::Expression* expr) {
|
bool Resolver::Expression(ast::Expression* expr) {
|
||||||
// This is blindly called above, so in some cases the expression won't exist.
|
|
||||||
if (!expr) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (TypeOf(expr)) {
|
if (TypeOf(expr)) {
|
||||||
return true; // Already resolved
|
return true; // Already resolved
|
||||||
}
|
}
|
||||||
@ -853,9 +914,11 @@ bool Resolver::Expression(ast::Expression* expr) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool Resolver::ArrayAccessor(ast::ArrayAccessorExpression* expr) {
|
bool Resolver::ArrayAccessor(ast::ArrayAccessorExpression* expr) {
|
||||||
|
Mark(expr->array());
|
||||||
if (!Expression(expr->array())) {
|
if (!Expression(expr->array())) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
Mark(expr->idx_expr());
|
||||||
if (!Expression(expr->idx_expr())) {
|
if (!Expression(expr->idx_expr())) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -892,6 +955,7 @@ bool Resolver::ArrayAccessor(ast::ArrayAccessorExpression* expr) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool Resolver::Bitcast(ast::BitcastExpression* expr) {
|
bool Resolver::Bitcast(ast::BitcastExpression* expr) {
|
||||||
|
Mark(expr->expr());
|
||||||
if (!Expression(expr->expr())) {
|
if (!Expression(expr->expr())) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -907,6 +971,7 @@ bool Resolver::Call(ast::CallExpression* call) {
|
|||||||
// The expression has to be an identifier as you can't store function pointers
|
// The expression has to be an identifier as you can't store function pointers
|
||||||
// but, if it isn't we'll just use the normal result determination to be on
|
// but, if it isn't we'll just use the normal result determination to be on
|
||||||
// the safe side.
|
// the safe side.
|
||||||
|
Mark(call->func());
|
||||||
auto* ident = call->func()->As<ast::IdentifierExpression>();
|
auto* ident = call->func()->As<ast::IdentifierExpression>();
|
||||||
if (!ident) {
|
if (!ident) {
|
||||||
diagnostics_.add_error("call target is not an identifier", call->source());
|
diagnostics_.add_error("call target is not an identifier", call->source());
|
||||||
@ -993,6 +1058,7 @@ bool Resolver::IntrinsicCall(ast::CallExpression* call,
|
|||||||
bool Resolver::Constructor(ast::ConstructorExpression* expr) {
|
bool Resolver::Constructor(ast::ConstructorExpression* expr) {
|
||||||
if (auto* type_ctor = expr->As<ast::TypeConstructorExpression>()) {
|
if (auto* type_ctor = expr->As<ast::TypeConstructorExpression>()) {
|
||||||
for (auto* value : type_ctor->values()) {
|
for (auto* value : type_ctor->values()) {
|
||||||
|
Mark(value);
|
||||||
if (!Expression(value)) {
|
if (!Expression(value)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -1003,13 +1069,14 @@ bool Resolver::Constructor(ast::ConstructorExpression* expr) {
|
|||||||
// obey the constructor type rules laid out in
|
// obey the constructor type rules laid out in
|
||||||
// https://gpuweb.github.io/gpuweb/wgsl.html#type-constructor-expr.
|
// https://gpuweb.github.io/gpuweb/wgsl.html#type-constructor-expr.
|
||||||
if (auto* vec_type = type_ctor->type()->As<type::Vector>()) {
|
if (auto* vec_type = type_ctor->type()->As<type::Vector>()) {
|
||||||
return VectorConstructor(vec_type, type_ctor->values());
|
return ValidateVectorConstructor(vec_type, type_ctor->values());
|
||||||
}
|
}
|
||||||
if (auto* mat_type = type_ctor->type()->As<type::Matrix>()) {
|
if (auto* mat_type = type_ctor->type()->As<type::Matrix>()) {
|
||||||
return MatrixConstructor(mat_type, type_ctor->values());
|
return ValidateMatrixConstructor(mat_type, type_ctor->values());
|
||||||
}
|
}
|
||||||
// TODO(crbug.com/tint/634): Validate array constructor
|
// TODO(crbug.com/tint/634): Validate array constructor
|
||||||
} else if (auto* scalar_ctor = expr->As<ast::ScalarConstructorExpression>()) {
|
} else if (auto* scalar_ctor = expr->As<ast::ScalarConstructorExpression>()) {
|
||||||
|
Mark(scalar_ctor->literal());
|
||||||
SetType(expr, scalar_ctor->literal()->type());
|
SetType(expr, scalar_ctor->literal()->type());
|
||||||
} else {
|
} else {
|
||||||
TINT_ICE(diagnostics_) << "unexpected constructor expression type";
|
TINT_ICE(diagnostics_) << "unexpected constructor expression type";
|
||||||
@ -1017,8 +1084,8 @@ bool Resolver::Constructor(ast::ConstructorExpression* expr) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Resolver::VectorConstructor(const type::Vector* vec_type,
|
bool Resolver::ValidateVectorConstructor(const type::Vector* vec_type,
|
||||||
const ast::ExpressionList& values) {
|
const ast::ExpressionList& values) {
|
||||||
type::Type* elem_type = vec_type->type()->UnwrapAll();
|
type::Type* elem_type = vec_type->type()->UnwrapAll();
|
||||||
size_t value_cardinality_sum = 0;
|
size_t value_cardinality_sum = 0;
|
||||||
for (auto* value : values) {
|
for (auto* value : values) {
|
||||||
@ -1085,8 +1152,8 @@ bool Resolver::VectorConstructor(const type::Vector* vec_type,
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Resolver::MatrixConstructor(const type::Matrix* matrix_type,
|
bool Resolver::ValidateMatrixConstructor(const type::Matrix* matrix_type,
|
||||||
const ast::ExpressionList& values) {
|
const ast::ExpressionList& values) {
|
||||||
// Zero Value expression
|
// Zero Value expression
|
||||||
if (values.empty()) {
|
if (values.empty()) {
|
||||||
return true;
|
return true;
|
||||||
@ -1198,6 +1265,7 @@ bool Resolver::Identifier(ast::IdentifierExpression* expr) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool Resolver::MemberAccessor(ast::MemberAccessorExpression* expr) {
|
bool Resolver::MemberAccessor(ast::MemberAccessorExpression* expr) {
|
||||||
|
Mark(expr->structure());
|
||||||
if (!Expression(expr->structure())) {
|
if (!Expression(expr->structure())) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -1209,8 +1277,9 @@ bool Resolver::MemberAccessor(ast::MemberAccessorExpression* expr) {
|
|||||||
std::vector<uint32_t> swizzle;
|
std::vector<uint32_t> swizzle;
|
||||||
|
|
||||||
if (auto* ty = data_type->As<type::Struct>()) {
|
if (auto* ty = data_type->As<type::Struct>()) {
|
||||||
auto* str = Structure(ty);
|
Mark(expr->member());
|
||||||
auto symbol = expr->member()->symbol();
|
auto symbol = expr->member()->symbol();
|
||||||
|
auto* str = Structure(ty);
|
||||||
|
|
||||||
const sem::StructMember* member = nullptr;
|
const sem::StructMember* member = nullptr;
|
||||||
for (auto* m : str->members) {
|
for (auto* m : str->members) {
|
||||||
@ -1236,6 +1305,7 @@ bool Resolver::MemberAccessor(ast::MemberAccessorExpression* expr) {
|
|||||||
builder_->Sem().Add(expr, builder_->create<sem::StructMemberAccess>(
|
builder_->Sem().Add(expr, builder_->create<sem::StructMemberAccess>(
|
||||||
expr, ret, current_statement_, member));
|
expr, ret, current_statement_, member));
|
||||||
} else if (auto* vec = data_type->As<type::Vector>()) {
|
} else if (auto* vec = data_type->As<type::Vector>()) {
|
||||||
|
Mark(expr->member());
|
||||||
std::string str = builder_->Symbols().NameFor(expr->member()->symbol());
|
std::string str = builder_->Symbols().NameFor(expr->member()->symbol());
|
||||||
auto size = str.size();
|
auto size = str.size();
|
||||||
swizzle.reserve(str.size());
|
swizzle.reserve(str.size());
|
||||||
@ -1481,6 +1551,8 @@ bool Resolver::ValidateBinary(ast::BinaryExpression* expr) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool Resolver::Binary(ast::BinaryExpression* expr) {
|
bool Resolver::Binary(ast::BinaryExpression* expr) {
|
||||||
|
Mark(expr->lhs());
|
||||||
|
Mark(expr->rhs());
|
||||||
if (!Expression(expr->lhs()) || !Expression(expr->rhs())) {
|
if (!Expression(expr->lhs()) || !Expression(expr->rhs())) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -1557,6 +1629,8 @@ bool Resolver::Binary(ast::BinaryExpression* expr) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool Resolver::UnaryOp(ast::UnaryOpExpression* expr) {
|
bool Resolver::UnaryOp(ast::UnaryOpExpression* expr) {
|
||||||
|
Mark(expr->expr());
|
||||||
|
|
||||||
// Result type matches the parameter type.
|
// Result type matches the parameter type.
|
||||||
if (!Expression(expr->expr())) {
|
if (!Expression(expr->expr())) {
|
||||||
return false;
|
return false;
|
||||||
@ -1569,6 +1643,8 @@ bool Resolver::UnaryOp(ast::UnaryOpExpression* expr) {
|
|||||||
|
|
||||||
bool Resolver::VariableDeclStatement(const ast::VariableDeclStatement* stmt) {
|
bool Resolver::VariableDeclStatement(const ast::VariableDeclStatement* stmt) {
|
||||||
ast::Variable* var = stmt->variable();
|
ast::Variable* var = stmt->variable();
|
||||||
|
Mark(var);
|
||||||
|
|
||||||
type::Type* type = var->declared_type();
|
type::Type* type = var->declared_type();
|
||||||
|
|
||||||
bool is_global = false;
|
bool is_global = false;
|
||||||
@ -1582,6 +1658,7 @@ bool Resolver::VariableDeclStatement(const ast::VariableDeclStatement* stmt) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (auto* ctor = stmt->variable()->constructor()) {
|
if (auto* ctor = stmt->variable()->constructor()) {
|
||||||
|
Mark(ctor);
|
||||||
if (!Expression(ctor)) {
|
if (!Expression(ctor)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -1602,11 +1679,16 @@ bool Resolver::VariableDeclStatement(const ast::VariableDeclStatement* stmt) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (auto* deco : var->decorations()) {
|
||||||
|
// TODO(bclayton): Validate decorations
|
||||||
|
Mark(deco);
|
||||||
|
}
|
||||||
|
|
||||||
auto* info = Variable(var, type);
|
auto* info = Variable(var, type);
|
||||||
if (!info) {
|
if (!info) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// TODO(amaiorano): Remove this and fix tests. We're overriding the semantic
|
// TODO(bclayton): Remove this and fix tests. We're overriding the semantic
|
||||||
// type stored in info->type here with a possibly non-canonicalized type.
|
// type stored in info->type here with a possibly non-canonicalized type.
|
||||||
info->type = type;
|
info->type = type;
|
||||||
variable_stack_.set(var->symbol(), info);
|
variable_stack_.set(var->symbol(), info);
|
||||||
@ -1860,9 +1942,16 @@ const sem::Array* Resolver::Array(type::Array* arr, const Source& source) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Look for explicit stride via [[stride(n)]] decoration
|
// Look for explicit stride via [[stride(n)]] decoration
|
||||||
|
uint32_t explicit_stride = 0;
|
||||||
for (auto* deco : arr->decorations()) {
|
for (auto* deco : arr->decorations()) {
|
||||||
|
Mark(deco);
|
||||||
if (auto* stride = deco->As<ast::StrideDecoration>()) {
|
if (auto* stride = deco->As<ast::StrideDecoration>()) {
|
||||||
auto explicit_stride = stride->stride();
|
if (explicit_stride) {
|
||||||
|
diagnostics_.add_error(
|
||||||
|
"array must have at most one [[stride]] decoration", source);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
explicit_stride = stride->stride();
|
||||||
bool is_valid_stride = (explicit_stride >= el_size) &&
|
bool is_valid_stride = (explicit_stride >= el_size) &&
|
||||||
(explicit_stride >= el_align) &&
|
(explicit_stride >= el_align) &&
|
||||||
(explicit_stride % el_align == 0);
|
(explicit_stride % el_align == 0);
|
||||||
@ -1878,10 +1967,11 @@ const sem::Array* Resolver::Array(type::Array* arr, const Source& source) {
|
|||||||
source);
|
source);
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
return create_semantic(explicit_stride);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (explicit_stride) {
|
||||||
|
return create_semantic(explicit_stride);
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate implicit stride
|
// Calculate implicit stride
|
||||||
auto implicit_stride = utils::RoundUp(el_align, el_size);
|
auto implicit_stride = utils::RoundUp(el_align, el_size);
|
||||||
@ -1950,6 +2040,11 @@ Resolver::StructInfo* Resolver::Structure(type::Struct* str) {
|
|||||||
return info_it->second;
|
return info_it->second;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Mark(str->impl());
|
||||||
|
for (auto* deco : str->impl()->decorations()) {
|
||||||
|
Mark(deco);
|
||||||
|
}
|
||||||
|
|
||||||
if (!ValidateStructure(str)) {
|
if (!ValidateStructure(str)) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
@ -1972,6 +2067,8 @@ Resolver::StructInfo* Resolver::Structure(type::Struct* str) {
|
|||||||
uint32_t struct_align = 1;
|
uint32_t struct_align = 1;
|
||||||
|
|
||||||
for (auto* member : str->impl()->members()) {
|
for (auto* member : str->impl()->members()) {
|
||||||
|
Mark(member);
|
||||||
|
|
||||||
// First check the member type is legal
|
// First check the member type is legal
|
||||||
if (!IsStorable(member->type())) {
|
if (!IsStorable(member->type())) {
|
||||||
builder_->Diagnostics().add_error(
|
builder_->Diagnostics().add_error(
|
||||||
@ -1991,6 +2088,7 @@ Resolver::StructInfo* Resolver::Structure(type::Struct* str) {
|
|||||||
bool has_align_deco = false;
|
bool has_align_deco = false;
|
||||||
bool has_size_deco = false;
|
bool has_size_deco = false;
|
||||||
for (auto* deco : member->decorations()) {
|
for (auto* deco : member->decorations()) {
|
||||||
|
Mark(deco);
|
||||||
if (auto* o = deco->As<ast::StructMemberOffsetDecoration>()) {
|
if (auto* o = deco->As<ast::StructMemberOffsetDecoration>()) {
|
||||||
// Offset decorations are not part of the WGSL spec, but are emitted by
|
// Offset decorations are not part of the WGSL spec, but are emitted by
|
||||||
// the SPIR-V reader.
|
// the SPIR-V reader.
|
||||||
@ -2077,11 +2175,15 @@ bool Resolver::ValidateReturn(const ast::ReturnStatement* ret) {
|
|||||||
bool Resolver::Return(ast::ReturnStatement* ret) {
|
bool Resolver::Return(ast::ReturnStatement* ret) {
|
||||||
current_function_->return_statements.push_back(ret);
|
current_function_->return_statements.push_back(ret);
|
||||||
|
|
||||||
auto result = Expression(ret->value());
|
if (auto* value = ret->value()) {
|
||||||
|
Mark(value);
|
||||||
|
|
||||||
// Validate after processing the return value expression so that its type is
|
// Validate after processing the return value expression so that its type is
|
||||||
// available for validation
|
// available for validation
|
||||||
return result && ValidateReturn(ret);
|
return Expression(value) && ValidateReturn(ret);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Resolver::ValidateSwitch(const ast::SwitchStatement* s) {
|
bool Resolver::ValidateSwitch(const ast::SwitchStatement* s) {
|
||||||
@ -2155,10 +2257,12 @@ bool Resolver::ValidateSwitch(const ast::SwitchStatement* s) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool Resolver::Switch(ast::SwitchStatement* s) {
|
bool Resolver::Switch(ast::SwitchStatement* s) {
|
||||||
|
Mark(s->condition());
|
||||||
if (!Expression(s->condition())) {
|
if (!Expression(s->condition())) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
for (auto* case_stmt : s->body()) {
|
for (auto* case_stmt : s->body()) {
|
||||||
|
Mark(case_stmt);
|
||||||
if (!CaseStatement(case_stmt)) {
|
if (!CaseStatement(case_stmt)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -2231,6 +2335,9 @@ bool Resolver::ValidateAssignment(const ast::AssignmentStatement* a) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool Resolver::Assignment(ast::AssignmentStatement* a) {
|
bool Resolver::Assignment(ast::AssignmentStatement* a) {
|
||||||
|
Mark(a->lhs());
|
||||||
|
Mark(a->rhs());
|
||||||
|
|
||||||
if (!Expression(a->lhs()) || !Expression(a->rhs())) {
|
if (!Expression(a->lhs()) || !Expression(a->rhs())) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -2330,6 +2437,19 @@ type::Type* Resolver::Canonical(type::Type* type) {
|
|||||||
[&] { return make_canonical(type); });
|
[&] { return make_canonical(type); });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Resolver::Mark(ast::Node* node) {
|
||||||
|
if (node == nullptr) {
|
||||||
|
TINT_ICE(diagnostics_) << "Resolver::Mark() called with nullptr";
|
||||||
|
}
|
||||||
|
if (marked_.emplace(node).second) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
TINT_ICE(diagnostics_)
|
||||||
|
<< "AST node '" << node->TypeInfo().name
|
||||||
|
<< "' was encountered twice in the same AST of a Program\n"
|
||||||
|
<< "At: " << node->source();
|
||||||
|
}
|
||||||
|
|
||||||
Resolver::VariableInfo::VariableInfo(ast::Variable* decl, type::Type* ctype)
|
Resolver::VariableInfo::VariableInfo(ast::Variable* decl, type::Type* ctype)
|
||||||
: declaration(decl),
|
: declaration(decl),
|
||||||
type(ctype),
|
type(ctype),
|
||||||
|
@ -210,43 +210,45 @@ class Resolver {
|
|||||||
// AST and Type traversal methods
|
// AST and Type traversal methods
|
||||||
// Each return true on success, false on failure.
|
// Each return true on success, false on failure.
|
||||||
bool ArrayAccessor(ast::ArrayAccessorExpression*);
|
bool ArrayAccessor(ast::ArrayAccessorExpression*);
|
||||||
|
bool Assignment(ast::AssignmentStatement* a);
|
||||||
bool Binary(ast::BinaryExpression*);
|
bool Binary(ast::BinaryExpression*);
|
||||||
bool Bitcast(ast::BitcastExpression*);
|
bool Bitcast(ast::BitcastExpression*);
|
||||||
bool BlockStatement(const ast::BlockStatement*);
|
bool BlockStatement(const ast::BlockStatement*);
|
||||||
bool Call(ast::CallExpression*);
|
bool Call(ast::CallExpression*);
|
||||||
bool CaseStatement(ast::CaseStatement*);
|
bool CaseStatement(ast::CaseStatement*);
|
||||||
bool Constructor(ast::ConstructorExpression*);
|
bool Constructor(ast::ConstructorExpression*);
|
||||||
bool VectorConstructor(const type::Vector* vec_type,
|
|
||||||
const ast::ExpressionList& values);
|
|
||||||
bool MatrixConstructor(const type::Matrix* matrix_type,
|
|
||||||
const ast::ExpressionList& values);
|
|
||||||
bool Expression(ast::Expression*);
|
bool Expression(ast::Expression*);
|
||||||
bool Expressions(const ast::ExpressionList&);
|
bool Expressions(const ast::ExpressionList&);
|
||||||
bool Function(ast::Function*);
|
bool Function(ast::Function*);
|
||||||
|
bool GlobalVariable(ast::Variable* var);
|
||||||
bool Identifier(ast::IdentifierExpression*);
|
bool Identifier(ast::IdentifierExpression*);
|
||||||
bool IfStatement(ast::IfStatement*);
|
bool IfStatement(ast::IfStatement*);
|
||||||
bool IntrinsicCall(ast::CallExpression*, sem::IntrinsicType);
|
bool IntrinsicCall(ast::CallExpression*, sem::IntrinsicType);
|
||||||
bool MemberAccessor(ast::MemberAccessorExpression*);
|
bool MemberAccessor(ast::MemberAccessorExpression*);
|
||||||
|
bool Parameter(ast::Variable* param);
|
||||||
|
bool Return(ast::ReturnStatement* ret);
|
||||||
bool Statement(ast::Statement*);
|
bool Statement(ast::Statement*);
|
||||||
bool Statements(const ast::StatementList&);
|
bool Statements(const ast::StatementList&);
|
||||||
|
bool Switch(ast::SwitchStatement* s);
|
||||||
|
bool Type(type::Type* ty);
|
||||||
bool UnaryOp(ast::UnaryOpExpression*);
|
bool UnaryOp(ast::UnaryOpExpression*);
|
||||||
bool VariableDeclStatement(const ast::VariableDeclStatement*);
|
bool VariableDeclStatement(const ast::VariableDeclStatement*);
|
||||||
bool Return(ast::ReturnStatement* ret);
|
|
||||||
bool Switch(ast::SwitchStatement* s);
|
|
||||||
bool Assignment(ast::AssignmentStatement* a);
|
|
||||||
bool GlobalVariable(ast::Variable* var);
|
|
||||||
|
|
||||||
// AST and Type validation methods
|
// AST and Type validation methods
|
||||||
// Each return true on success, false on failure.
|
// Each return true on success, false on failure.
|
||||||
bool ValidateBinary(ast::BinaryExpression* expr);
|
|
||||||
bool ValidateVariable(const ast::Variable* param);
|
|
||||||
bool ValidateParameter(const ast::Variable* param);
|
|
||||||
bool ValidateFunction(const ast::Function* func);
|
|
||||||
bool ValidateEntryPoint(const ast::Function* func);
|
|
||||||
bool ValidateStructure(const type::Struct* st);
|
|
||||||
bool ValidateReturn(const ast::ReturnStatement* ret);
|
|
||||||
bool ValidateSwitch(const ast::SwitchStatement* s);
|
|
||||||
bool ValidateAssignment(const ast::AssignmentStatement* a);
|
bool ValidateAssignment(const ast::AssignmentStatement* a);
|
||||||
|
bool ValidateBinary(ast::BinaryExpression* expr);
|
||||||
|
bool ValidateEntryPoint(const ast::Function* func);
|
||||||
|
bool ValidateFunction(const ast::Function* func);
|
||||||
|
bool ValidateMatrixConstructor(const type::Matrix* matrix_type,
|
||||||
|
const ast::ExpressionList& values);
|
||||||
|
bool ValidateParameter(const ast::Variable* param);
|
||||||
|
bool ValidateReturn(const ast::ReturnStatement* ret);
|
||||||
|
bool ValidateStructure(const type::Struct* st);
|
||||||
|
bool ValidateSwitch(const ast::SwitchStatement* s);
|
||||||
|
bool ValidateVariable(const ast::Variable* param);
|
||||||
|
bool ValidateVectorConstructor(const type::Vector* vec_type,
|
||||||
|
const ast::ExpressionList& values);
|
||||||
|
|
||||||
/// @returns the semantic information for the array `arr`, building it if it
|
/// @returns the semantic information for the array `arr`, building it if it
|
||||||
/// hasn't been constructed already. If an error is raised, nullptr is
|
/// hasn't been constructed already. If an error is raised, nullptr is
|
||||||
@ -312,6 +314,11 @@ class Resolver {
|
|||||||
/// @return pretty string representation
|
/// @return pretty string representation
|
||||||
std::string VectorPretty(uint32_t size, type::Type* element_type);
|
std::string VectorPretty(uint32_t size, type::Type* element_type);
|
||||||
|
|
||||||
|
/// Mark records that the given AST node has been visited, and asserts that
|
||||||
|
/// the given node has not already been seen. Diamonds in the AST are illegal.
|
||||||
|
/// @param node the AST node.
|
||||||
|
void Mark(ast::Node* node);
|
||||||
|
|
||||||
ProgramBuilder* const builder_;
|
ProgramBuilder* const builder_;
|
||||||
std::unique_ptr<IntrinsicTable> const intrinsic_table_;
|
std::unique_ptr<IntrinsicTable> const intrinsic_table_;
|
||||||
diag::List diagnostics_;
|
diag::List diagnostics_;
|
||||||
@ -324,6 +331,7 @@ class Resolver {
|
|||||||
std::unordered_map<ast::Expression*, ExpressionInfo> expr_info_;
|
std::unordered_map<ast::Expression*, ExpressionInfo> expr_info_;
|
||||||
std::unordered_map<type::Struct*, StructInfo*> struct_info_;
|
std::unordered_map<type::Struct*, StructInfo*> struct_info_;
|
||||||
std::unordered_map<type::Type*, type::Type*> type_to_canonical_;
|
std::unordered_map<type::Type*, type::Type*> type_to_canonical_;
|
||||||
|
std::unordered_set<ast::Node*> marked_;
|
||||||
FunctionInfo* current_function_ = nullptr;
|
FunctionInfo* current_function_ = nullptr;
|
||||||
sem::Statement* current_statement_ = nullptr;
|
sem::Statement* current_statement_ = nullptr;
|
||||||
BlockAllocator<VariableInfo> variable_infos_;
|
BlockAllocator<VariableInfo> variable_infos_;
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
#include <tuple>
|
#include <tuple>
|
||||||
|
|
||||||
#include "gmock/gmock.h"
|
#include "gmock/gmock.h"
|
||||||
|
#include "gtest/gtest-spi.h"
|
||||||
#include "src/ast/assignment_statement.h"
|
#include "src/ast/assignment_statement.h"
|
||||||
#include "src/ast/bitcast_expression.h"
|
#include "src/ast/bitcast_expression.h"
|
||||||
#include "src/ast/break_statement.h"
|
#include "src/ast/break_statement.h"
|
||||||
@ -1619,6 +1620,31 @@ TEST_F(ResolverTest, Function_EntryPoints_LinearTime) {
|
|||||||
ASSERT_TRUE(r()->Resolve()) << r()->error();
|
ASSERT_TRUE(r()->Resolve()) << r()->error();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_F(ResolverTest, ASTNodeNotReached) {
|
||||||
|
EXPECT_FATAL_FAILURE(
|
||||||
|
{
|
||||||
|
ProgramBuilder builder;
|
||||||
|
builder.Expr("1");
|
||||||
|
Resolver(&builder).Resolve();
|
||||||
|
},
|
||||||
|
"internal compiler error: AST node 'tint::ast::IdentifierExpression' was "
|
||||||
|
"not reached by the resolver");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ResolverTest, ASTNodeReachedTwice) {
|
||||||
|
EXPECT_FATAL_FAILURE(
|
||||||
|
{
|
||||||
|
ProgramBuilder builder;
|
||||||
|
auto* expr = builder.Expr("1");
|
||||||
|
auto* usesExprTwice = builder.Add(expr, expr);
|
||||||
|
builder.Global("g", builder.ty.i32(), ast::StorageClass::kPrivate,
|
||||||
|
usesExprTwice);
|
||||||
|
Resolver(&builder).Resolve();
|
||||||
|
},
|
||||||
|
"internal compiler error: AST node 'tint::ast::IdentifierExpression' was "
|
||||||
|
"encountered twice in the same AST of a Program");
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
} // namespace resolver
|
} // namespace resolver
|
||||||
} // namespace tint
|
} // namespace tint
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
#include "src/source.h"
|
#include "src/source.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
@ -37,4 +38,57 @@ Source::FileContent::~FileContent() = default;
|
|||||||
|
|
||||||
Source::File::~File() = default;
|
Source::File::~File() = default;
|
||||||
|
|
||||||
|
std::ostream& operator<<(std::ostream& out, const Source& source) {
|
||||||
|
auto rng = source.range;
|
||||||
|
|
||||||
|
if (!source.file_path.empty()) {
|
||||||
|
out << source.file_path << ":";
|
||||||
|
}
|
||||||
|
if (rng.begin.line) {
|
||||||
|
out << rng.begin.line << ":";
|
||||||
|
if (rng.begin.column) {
|
||||||
|
out << rng.begin.column;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (source.file_content) {
|
||||||
|
out << std::endl << std::endl;
|
||||||
|
|
||||||
|
auto repeat = [&](char c, size_t n) {
|
||||||
|
while (n--) {
|
||||||
|
out << c;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (size_t line = rng.begin.line; line <= rng.end.line; line++) {
|
||||||
|
if (line < source.file_content->lines.size() + 1) {
|
||||||
|
auto len = source.file_content->lines[line - 1].size();
|
||||||
|
|
||||||
|
out << source.file_content->lines[line - 1];
|
||||||
|
|
||||||
|
out << std::endl;
|
||||||
|
|
||||||
|
if (line == rng.begin.line && line == rng.end.line) {
|
||||||
|
// Single line
|
||||||
|
repeat(' ', rng.begin.column - 1);
|
||||||
|
repeat('^', std::max<size_t>(rng.end.column - rng.begin.column, 1));
|
||||||
|
} else if (line == rng.begin.line) {
|
||||||
|
// Start of multi-line
|
||||||
|
repeat(' ', rng.begin.column - 1);
|
||||||
|
repeat('^', len - (rng.begin.column - 1));
|
||||||
|
} else if (line == rng.end.line) {
|
||||||
|
// End of multi-line
|
||||||
|
repeat('^', rng.end.column - 1);
|
||||||
|
} else {
|
||||||
|
// Middle of multi-line
|
||||||
|
repeat('^', len);
|
||||||
|
}
|
||||||
|
|
||||||
|
out << std::endl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace tint
|
} // namespace tint
|
||||||
|
@ -147,6 +147,12 @@ class Source {
|
|||||||
const FileContent* file_content = nullptr;
|
const FileContent* file_content = nullptr;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Writes the Source to the std::ostream.
|
||||||
|
/// @param out the std::ostream to write to
|
||||||
|
/// @param source the source to write
|
||||||
|
/// @returns out so calls can be chained
|
||||||
|
std::ostream& operator<<(std::ostream& out, const Source& source);
|
||||||
|
|
||||||
/// Writes the Source::FileContent to the std::ostream.
|
/// Writes the Source::FileContent to the std::ostream.
|
||||||
/// @param out the std::ostream to write to
|
/// @param out the std::ostream to write to
|
||||||
/// @param content the file content to write
|
/// @param content the file content to write
|
||||||
|
Loading…
x
Reference in New Issue
Block a user