blob: 495f6089af3de7f8ac9ec116908f3dfa1f3bdd7f [file] [log] [blame]
// Copyright 2023 The Tint Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "src/tint/ir/to_program.h"
#include <string>
#include <tuple>
#include <utility>
#include "src/tint/ir/access.h"
#include "src/tint/ir/binary.h"
#include "src/tint/ir/block.h"
#include "src/tint/ir/call.h"
#include "src/tint/ir/constant.h"
#include "src/tint/ir/exit_if.h"
#include "src/tint/ir/exit_switch.h"
#include "src/tint/ir/if.h"
#include "src/tint/ir/instruction.h"
#include "src/tint/ir/load.h"
#include "src/tint/ir/module.h"
#include "src/tint/ir/multi_in_block.h"
#include "src/tint/ir/return.h"
#include "src/tint/ir/store.h"
#include "src/tint/ir/switch.h"
#include "src/tint/ir/unary.h"
#include "src/tint/ir/user_call.h"
#include "src/tint/ir/var.h"
#include "src/tint/program_builder.h"
#include "src/tint/switch.h"
#include "src/tint/type/atomic.h"
#include "src/tint/type/depth_multisampled_texture.h"
#include "src/tint/type/depth_texture.h"
#include "src/tint/type/multisampled_texture.h"
#include "src/tint/type/pointer.h"
#include "src/tint/type/reference.h"
#include "src/tint/type/sampler.h"
#include "src/tint/type/texture.h"
#include "src/tint/utils/hashmap.h"
#include "src/tint/utils/predicates.h"
#include "src/tint/utils/reverse.h"
#include "src/tint/utils/scoped_assignment.h"
#include "src/tint/utils/transform.h"
#include "src/tint/utils/vector.h"
// Helper for calling TINT_UNIMPLEMENTED() from a Switch(object_ptr) default case.
#define UNHANDLED_CASE(object_ptr) \
TINT_UNIMPLEMENTED(IR, b.Diagnostics()) \
<< "unhandled case in Switch(): " << (object_ptr ? object_ptr->TypeInfo().name : "<null>")
// Helper for incrementing nesting_depth_ and then decrementing nesting_depth_ at the end
// of the scope that holds the call.
#define SCOPED_NESTING() \
nesting_depth_++; \
TINT_DEFER(nesting_depth_--)
namespace tint::ir {
namespace {
/// Empty struct used as a sentinel value to indicate that an ast::Value has been consumed by its
/// single place of usage. Attempting to use this value a second time should result in an ICE.
struct ConsumedValue {};
class State {
public:
explicit State(Module& m) : mod(m) {}
Program Run() {
// TODO(crbug.com/tint/1902): Emit root block
// TODO(crbug.com/tint/1902): Emit user-declared types
for (auto* fn : mod.functions) {
Fn(fn);
}
return Program{std::move(b)};
}
private:
/// The source IR module
Module& mod;
/// The target ProgramBuilder
ProgramBuilder b;
using ValueBinding = std::variant<Symbol, const ast::Expression*, ConsumedValue>;
/// A hashmap of value to one of:
/// * Symbol - Name of 'let' (non-inlinable value), 'var' or parameter.
/// * ast::Expression* - single use, inlined expression.
/// * ConsumedValue - a special value used to indicate that the value has already been
/// consumed.
utils::Hashmap<Value*, ValueBinding, 32> bindings_;
/// The nesting depth of the currently generated AST
/// 0 is module scope
/// 1 is root-level function scope
/// 2+ is within control flow
uint32_t nesting_depth_ = 0;
using StatementList = utils::Vector<const ast::Statement*,
decltype(ast::BlockStatement::statements)::static_length>;
StatementList* statements_ = nullptr;
/// The current switch case block
ir::Block* current_switch_case_ = nullptr;
const ast::Function* Fn(ir::Function* fn) {
SCOPED_NESTING();
// TODO(crbug.com/tint/1915): Properly implement this when we've fleshed out Function
static constexpr size_t N = decltype(ast::Function::params)::static_length;
auto params = utils::Transform<N>(fn->Params(), [&](FunctionParam* param) {
auto name = BindName(param);
auto ty = Type(param->Type());
return b.Param(name, ty);
});
auto name = BindName(fn);
auto ret_ty = Type(fn->ReturnType());
auto* body = Block(fn->StartTarget());
utils::Vector<const ast::Attribute*, 1> attrs{};
utils::Vector<const ast::Attribute*, 1> ret_attrs{};
return b.Func(name, std::move(params), ret_ty, body, std::move(attrs),
std::move(ret_attrs));
}
const ast::BlockStatement* Block(ir::Block* block) {
// TODO(crbug.com/tint/1902): Handle block arguments.
return b.Block(Statements(block));
}
StatementList Statements(ir::Block* block) {
StatementList stmts;
if (block) {
TINT_SCOPED_ASSIGNMENT(statements_, &stmts);
for (auto* inst : *block) {
Instruction(inst);
}
}
return stmts;
}
void Append(const ast::Statement* inst) { statements_->Push(inst); }
void Instruction(ir::Instruction* inst) {
tint::Switch(
inst, //
[&](ir::Binary* u) { Binary(u); }, //
[&](ir::Call* i) { Call(i); }, //
[&](ir::ExitIf*) {}, //
[&](ir::ExitSwitch* i) { ExitSwitch(i); }, //
[&](ir::If* i) { If(i); }, //
[&](ir::Load* l) { Load(l); }, //
[&](ir::Return* i) { Return(i); }, //
[&](ir::Store* i) { Store(i); }, //
[&](ir::Switch* i) { Switch(i); }, //
[&](ir::Unary* u) { Unary(u); }, //
[&](ir::Var* i) { Var(i); }, //
[&](Default) { UNHANDLED_CASE(inst); });
}
void If(ir::If* if_) {
SCOPED_NESTING();
auto true_stmts = Statements(if_->True());
auto false_stmts = Statements(if_->False());
if (IsShortCircuit(if_, true_stmts, false_stmts)) {
return;
}
auto* cond = Expr(if_->Condition());
auto* true_block = b.Block(std::move(true_stmts));
switch (false_stmts.Length()) {
case 0:
Append(b.If(cond, true_block));
return;
case 1:
if (auto* else_if = false_stmts.Front()->As<ast::IfStatement>()) {
Append(b.If(cond, true_block, b.Else(else_if)));
return;
}
break;
}
auto* false_block = b.Block(std::move(false_stmts));
Append(b.If(cond, true_block, b.Else(false_block)));
}
void Switch(ir::Switch* s) {
SCOPED_NESTING();
auto* cond = Expr(s->Condition());
auto cases = utils::Transform(
s->Cases(), //
[&](ir::Switch::Case c) -> const tint::ast::CaseStatement* {
SCOPED_NESTING();
const ast::BlockStatement* body = nullptr;
{
TINT_SCOPED_ASSIGNMENT(current_switch_case_, c.Block());
body = Block(c.Block());
}
auto selectors = utils::Transform(c.selectors, //
[&](ir::Switch::CaseSelector cs) {
return cs.IsDefault()
? b.DefaultCaseSelector()
: b.CaseSelector(Expr(cs.val));
});
return b.Case(std::move(selectors), body);
});
Append(b.Switch(cond, std::move(cases)));
}
void ExitSwitch(const ir::ExitSwitch* e) {
if (current_switch_case_ && current_switch_case_->Terminator() == e) {
return; // No need to emit
}
Append(b.Break());
}
void Return(ir::Return* ret) {
if (ret->Args().IsEmpty()) {
// Return has no arguments.
// If this block is nested withing some control flow, then we must
// emit a 'return' statement, otherwise we've just naturally reached
// the end of the function where the 'return' is redundant.
if (nesting_depth_ > 1) {
Append(b.Return());
}
return;
}
// Return has arguments - this is the return value.
if (ret->Args().Length() != 1) {
TINT_ICE(IR, b.Diagnostics())
<< "expected 1 value for return, got " << ret->Args().Length();
return;
}
Append(b.Return(Expr(ret->Args().Front())));
}
void Var(ir::Var* var) {
auto* val = var->Result();
Symbol name = BindName(val);
auto* ptr = As<type::Pointer>(val->Type());
auto ty = Type(ptr->StoreType());
const ast::Expression* init = nullptr;
if (var->Initializer()) {
init = Expr(var->Initializer());
}
switch (ptr->AddressSpace()) {
case builtin::AddressSpace::kFunction:
Append(b.Decl(b.Var(name, ty, init)));
return;
case builtin::AddressSpace::kStorage:
Append(b.Decl(b.Var(name, ty, init, ptr->Access(), ptr->AddressSpace())));
return;
default:
Append(b.Decl(b.Var(name, ty, init, ptr->AddressSpace())));
return;
}
}
void Store(ir::Store* store) {
auto* dst = Expr(store->To());
auto* src = Expr(store->From());
Append(b.Assign(dst, src));
}
void Call(ir::Call* call) {
auto args = utils::Transform<2>(call->Args(), [&](ir::Value* arg) { return Expr(arg); });
tint::Switch(
call, //
[&](ir::UserCall* c) {
auto* expr = b.Call(BindName(c->Func()), std::move(args));
if (!call->HasResults() || call->Result()->Usages().IsEmpty()) {
Append(b.CallStmt(expr));
return;
}
Bind(c->Result(), expr);
},
[&](Default) { UNHANDLED_CASE(call); });
}
void Load(ir::Load* l) { Bind(l->Result(), Expr(l->From())); }
void Unary(ir::Unary* u) {
const ast::Expression* expr = nullptr;
switch (u->Kind()) {
case ir::Unary::Kind::kComplement:
expr = b.Complement(Expr(u->Val()));
break;
case ir::Unary::Kind::kNegation:
expr = b.Negation(Expr(u->Val()));
break;
}
Bind(u->Result(), expr);
}
void Binary(ir::Binary* e) {
if (e->Kind() == ir::Binary::Kind::kEqual) {
auto* rhs = e->RHS()->As<ir::Constant>();
if (rhs && rhs->Type()->Is<type::Bool>() && rhs->Value()->ValueAs<bool>() == false) {
// expr == false
Bind(e->Result(), b.Not(Expr(e->LHS())));
return;
}
}
auto* lhs = Expr(e->LHS());
auto* rhs = Expr(e->RHS());
const ast::Expression* expr = nullptr;
switch (e->Kind()) {
case ir::Binary::Kind::kAdd:
expr = b.Add(lhs, rhs);
break;
case ir::Binary::Kind::kSubtract:
expr = b.Sub(lhs, rhs);
break;
case ir::Binary::Kind::kMultiply:
expr = b.Mul(lhs, rhs);
break;
case ir::Binary::Kind::kDivide:
expr = b.Div(lhs, rhs);
break;
case ir::Binary::Kind::kModulo:
expr = b.Mod(lhs, rhs);
break;
case ir::Binary::Kind::kAnd:
expr = b.And(lhs, rhs);
break;
case ir::Binary::Kind::kOr:
expr = b.Or(lhs, rhs);
break;
case ir::Binary::Kind::kXor:
expr = b.Xor(lhs, rhs);
break;
case ir::Binary::Kind::kEqual:
expr = b.Equal(lhs, rhs);
break;
case ir::Binary::Kind::kNotEqual:
expr = b.NotEqual(lhs, rhs);
break;
case ir::Binary::Kind::kLessThan:
expr = b.LessThan(lhs, rhs);
break;
case ir::Binary::Kind::kGreaterThan:
expr = b.GreaterThan(lhs, rhs);
break;
case ir::Binary::Kind::kLessThanEqual:
expr = b.LessThanEqual(lhs, rhs);
break;
case ir::Binary::Kind::kGreaterThanEqual:
expr = b.GreaterThanEqual(lhs, rhs);
break;
case ir::Binary::Kind::kShiftLeft:
expr = b.Shl(lhs, rhs);
break;
case ir::Binary::Kind::kShiftRight:
expr = b.Shr(lhs, rhs);
break;
}
Bind(e->Result(), expr);
}
TINT_BEGIN_DISABLE_WARNING(UNREACHABLE_CODE);
const ast::Expression* Expr(ir::Value* value) {
return tint::Switch(
value, //
[&](ir::Constant* c) { return Constant(c); }, //
[&](Default) -> const ast::Expression* {
auto lookup = bindings_.Find(value);
if (TINT_UNLIKELY(!lookup)) {
TINT_ICE(IR, b.Diagnostics())
<< "Expr(" << (value ? value->TypeInfo().name : "null")
<< ") value has no expression";
return b.Expr("<error>");
}
return std::visit(
[&](auto&& got) -> const ast::Expression* {
using T = std::decay_t<decltype(got)>;
if constexpr (std::is_same_v<T, Symbol>) {
return b.Expr(got); // var, let or parameter.
}
if constexpr (std::is_same_v<T, const ast::Expression*>) {
// Single use (inlined) expression.
// Mark the bindings_ map entry as consumed.
*lookup = ConsumedValue{};
return got;
}
if constexpr (std::is_same_v<T, ConsumedValue>) {
TINT_ICE(IR, b.Diagnostics()) << "Expr(" << value->TypeInfo().name
<< ") called twice on the same value";
} else {
TINT_ICE(IR, b.Diagnostics())
<< "Expr(" << value->TypeInfo().name << ") has unhandled value";
}
return b.Expr("<error>");
},
*lookup);
});
}
TINT_END_DISABLE_WARNING(UNREACHABLE_CODE);
const ast::Expression* Constant(ir::Constant* c) {
return tint::Switch(
c->Type(), //
[&](const type::I32*) { return b.Expr(c->Value()->ValueAs<i32>()); },
[&](const type::U32*) { return b.Expr(c->Value()->ValueAs<u32>()); },
[&](const type::F32*) { return b.Expr(c->Value()->ValueAs<f32>()); },
[&](const type::F16*) { return b.Expr(c->Value()->ValueAs<f16>()); },
[&](const type::Bool*) { return b.Expr(c->Value()->ValueAs<bool>()); },
[&](Default) {
UNHANDLED_CASE(c);
return b.Expr("<error>");
});
}
////////////////////////////////////////////////////////////////////////////////////////////////
// Types
//
// The the case of an error:
// * The types generating methods must return a non-null ast type, which may not be semantically
// legal, but is enough to populate the AST.
// * A diagnostic error must be added to the ast::ProgramBuilder.
// This prevents littering the ToProgram logic with expensive error checking code.
////////////////////////////////////////////////////////////////////////////////////////////////
/// @param ty the type::Type
/// @return an ast::Type from @p ty.
/// @note May be a semantically-invalid placeholder type on error.
ast::Type Type(const type::Type* ty) {
return tint::Switch(
ty, //
[&](const type::Void*) { return ast::Type{}; }, //
[&](const type::I32*) { return b.ty.i32(); }, //
[&](const type::U32*) { return b.ty.u32(); }, //
[&](const type::F16*) { return b.ty.f16(); }, //
[&](const type::F32*) { return b.ty.f32(); }, //
[&](const type::Bool*) { return b.ty.bool_(); },
[&](const type::Matrix* m) {
return b.ty.mat(Type(m->type()), m->columns(), m->rows());
},
[&](const type::Vector* v) {
auto el = Type(v->type());
if (v->Packed()) {
TINT_ASSERT(IR, v->Width() == 3u);
return b.ty(builtin::Builtin::kPackedVec3, el);
} else {
return b.ty.vec(el, v->Width());
}
},
[&](const type::Array* a) {
auto el = Type(a->ElemType());
utils::Vector<const ast::Attribute*, 1> attrs;
if (!a->IsStrideImplicit()) {
attrs.Push(b.Stride(a->Stride()));
}
if (a->Count()->Is<type::RuntimeArrayCount>()) {
return b.ty.array(el, std::move(attrs));
}
auto count = a->ConstantCount();
if (TINT_UNLIKELY(!count)) {
TINT_ICE(IR, b.Diagnostics()) << type::Array::kErrExpectedConstantCount;
return b.ty.array(el, u32(1), std::move(attrs));
}
return b.ty.array(el, u32(count.value()), std::move(attrs));
},
[&](const type::Struct* s) { return b.ty(s->Name().NameView()); },
[&](const type::Atomic* a) { return b.ty.atomic(Type(a->Type())); },
[&](const type::DepthTexture* t) { return b.ty.depth_texture(t->dim()); },
[&](const type::DepthMultisampledTexture* t) {
return b.ty.depth_multisampled_texture(t->dim());
},
[&](const type::ExternalTexture*) { return b.ty.external_texture(); },
[&](const type::MultisampledTexture* t) {
auto el = Type(t->type());
return b.ty.multisampled_texture(t->dim(), el);
},
[&](const type::SampledTexture* t) {
auto el = Type(t->type());
return b.ty.sampled_texture(t->dim(), el);
},
[&](const type::StorageTexture* t) {
return b.ty.storage_texture(t->dim(), t->texel_format(), t->access());
},
[&](const type::Sampler* s) { return b.ty.sampler(s->kind()); },
[&](const type::Pointer* p) {
// Note: type::Pointer always has an inferred access, but WGSL only allows an
// explicit access in the 'storage' address space.
auto el = Type(p->StoreType());
auto address_space = p->AddressSpace();
auto access = address_space == builtin::AddressSpace::kStorage
? p->Access()
: builtin::Access::kUndefined;
return b.ty.ptr(address_space, el, access);
},
[&](const type::Reference*) {
TINT_ICE(IR, b.Diagnostics()) << "reference types should never appear in the IR";
return b.ty.i32();
},
[&](Default) {
UNHANDLED_CASE(ty);
return b.ty.i32();
});
}
////////////////////////////////////////////////////////////////////////////////////////////////
// Bindings
////////////////////////////////////////////////////////////////////////////////////////////////
/// Creates and returns a new, unique name for the given value, or returns the previously
/// created name.
/// @return the value's name
Symbol BindName(Value* value, std::string_view suggested = {}) {
TINT_ASSERT(IR, value);
auto& existing = bindings_.GetOrCreate(value, [&] {
if (!suggested.empty()) {
return b.Symbols().New(suggested);
}
if (auto sym = mod.NameOf(value)) {
return b.Symbols().New(sym.NameView());
}
return b.Symbols().New("v");
});
if (auto* name = std::get_if<Symbol>(&existing); TINT_LIKELY(name)) {
return *name;
}
TINT_ICE(IR, b.Diagnostics()) << "BindName(" << value->TypeInfo().name
<< ") called on value that has non-name binding";
return {};
}
template <typename T>
void Bind(ir::Value* value, const T* expr) {
TINT_ASSERT(IR, value);
if (CanInline(value)) {
// Value will be inlined at its place of usage.
bool added = bindings_.Add(value, expr);
if (TINT_UNLIKELY(!added)) {
TINT_ICE(IR, b.Diagnostics())
<< "Bind(" << value->TypeInfo().name << ") called twice for same node";
}
} else {
Append(b.Decl(b.Let(BindName(value), expr)));
}
}
/// @returns true if the if the value can be inlined into its single place
/// of usage. Currently a value is inlined if it has a single usage and is unnamed.
/// TODO(crbug.com/tint/1902): This logic needs to check that the sequence of side-effecting
/// expressions is not changed by inlining the expression. This needs fixing.
bool CanInline(Value* val) { return val->Usages().Count() == 1 && !mod.NameOf(val).IsValid(); }
////////////////////////////////////////////////////////////////////////////////////////////////
// Helpers
////////////////////////////////////////////////////////////////////////////////////////////////
bool IsShortCircuit(ir::If* i,
const StatementList& true_stmts,
const StatementList& false_stmts) {
if (!i->HasResults()) {
return false;
}
auto* result = i->Result();
if (!result->Type()->Is<type::Bool>()) {
return false; // Wrong result type
}
if (i->Exits().Count() != 2) {
return false; // Doesn't have two exits
}
if (!true_stmts.IsEmpty() || !false_stmts.IsEmpty()) {
return false; // True or False blocks contain statements
}
auto* cond = i->Condition();
auto* true_val = i->True()->Back()->Operands().Front();
auto* false_val = i->False()->Back()->Operands().Front();
if (IsConstant(false_val, false)) {
// %res = if %cond {
// block { # true
// exit_if %true_val;
// }
// block { # false
// exit_if false;
// }
// }
//
// transform into:
//
// res = cond && true_val;
//
auto* lhs = Expr(cond);
auto* rhs = Expr(true_val);
Bind(result, b.LogicalAnd(lhs, rhs));
return true;
}
if (IsConstant(true_val, true)) {
// %res = if %cond {
// block { # true
// exit_if true;
// }
// block { # false
// exit_if %false_val;
// }
// }
//
// transform into:
//
// res = cond || false_val;
//
auto* lhs = Expr(cond);
auto* rhs = Expr(false_val);
Bind(result, b.LogicalOr(lhs, rhs));
return true;
}
return false;
}
bool IsConstant(ir::Value* val, bool value) {
if (auto* c = val->As<ir::Constant>()) {
if (c->Type()->Is<type::Bool>()) {
return c->Value()->ValueAs<bool>() == value;
}
}
return false;
}
};
} // namespace
Program ToProgram(Module& i) {
return State{i}.Run();
}
} // namespace tint::ir