blob: 255f00ed4f1d7eaa8563c9419920a35d1ea7d806 [file] [edit]
/*
* Copyright (c) 2026 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#include "EvaluationOrderRewriter.h"
#include "AST.h"
#include "ASTVisitor.h"
#include "Types.h"
#include "WGSLShaderModule.h"
#include <wtf/HashSet.h>
#include <wtf/text/MakeString.h>
namespace WGSL {
// WGSL evaluates the reference of an assignment's left hand side before it evaluates the value
// being assigned, and evaluates that reference exactly once even for a compound assignment.
// Metal inherits C++17's rules instead, where the right operand of `=` is sequenced *before* the
// left one, and where a compound assignment has to be expanded into `lhs = lhs op rhs`, which
// mentions the left hand side twice. Either difference is observable whenever an index expression
// in the assignment target calls a function that writes to memory.
//
// Both are fixed by pinning every side effecting index in an assignment target to a let declared
// immediately before the statement, which leaves the assignment itself indexing by plain values.
// The same applies to a pointer valued let, whose initializer the pointer rewriter inlines into
// each use: hoisting the indices out first keeps them from being evaluated at those uses instead.
class EvaluationOrderRewriter final : public AST::Visitor {
using Base = AST::Visitor;
using Base::visit;
public:
EvaluationOrderRewriter(ShaderModule& shaderModule)
: m_shaderModule(shaderModule)
{
}
void run();
void visit(AST::CompoundStatement&) override;
void visit(AST::LoopStatement&) override;
private:
void rewrite(AST::Statement::List&);
void hoistAssignmentTarget(AST::Statement&);
void hoistIndices(AST::Expression&);
void hoist(AST::IndexAccessExpression&);
bool mayHaveSideEffects(AST::Expression&);
ShaderModule& m_shaderModule;
HashSet<String> m_userFunctions;
AST::Statement::List m_hoisted;
unsigned m_hoistCount { 0 };
};
// Only a call to a user declared function or to an atomic built-in can write to memory that a
// later part of the same statement could observe. Every other call, including a type constructor
// or a built-in such as min(), is a pure computation whose position does not matter.
class SideEffectFinder final : public AST::Visitor {
using Base = AST::Visitor;
using Base::visit;
public:
SideEffectFinder(const HashSet<String>& userFunctions)
: m_userFunctions(userFunctions)
{
}
bool check(AST::Expression& expression)
{
Base::visit(expression);
return m_found;
}
void visit(AST::CallExpression& call) override
{
if (!call.isConstructor()) {
if (auto* target = dynamicDowncast<AST::IdentifierExpression>(call.target())) {
const auto& name = target->identifier().id();
if (m_userFunctions.contains(name) || name.startsWith("atomic"_s))
m_found = true;
}
}
Base::visit(call);
}
private:
const HashSet<String>& m_userFunctions;
bool m_found { false };
};
void EvaluationOrderRewriter::run()
{
for (auto& declaration : m_shaderModule.declarations()) {
if (auto* function = dynamicDowncast<AST::Function>(declaration))
m_userFunctions.add(function->name().id());
}
Base::visit(m_shaderModule);
}
void EvaluationOrderRewriter::rewrite(AST::Statement::List& statements)
{
Vector<std::pair<unsigned, AST::Statement::List>> insertions;
for (unsigned index = 0; index < statements.size(); ++index) {
hoistAssignmentTarget(statements[index]);
// The hoisted declarations have to be taken before descending into the statement, since a
// nested block hoists into its own list.
if (!m_hoisted.isEmpty())
insertions.append({ index, std::exchange(m_hoisted, AST::Statement::List { }) });
Base::visit(statements[index]);
}
// Inserting from the back leaves the indices of the earlier insertion points untouched.
for (unsigned i = insertions.size(); i--;)
m_shaderModule.insertVector(statements, insertions[i].first, insertions[i].second);
}
void EvaluationOrderRewriter::visit(AST::CompoundStatement& statement)
{
rewrite(statement.statements());
}
void EvaluationOrderRewriter::visit(AST::LoopStatement& statement)
{
rewrite(statement.body());
if (auto& continuing = statement.continuing())
rewrite(continuing->body);
}
void EvaluationOrderRewriter::hoistAssignmentTarget(AST::Statement& statement)
{
switch (statement.kind()) {
case AST::NodeKind::AssignmentStatement:
hoistIndices(uncheckedDowncast<AST::AssignmentStatement>(statement).lhs());
break;
case AST::NodeKind::CompoundAssignmentStatement:
hoistIndices(uncheckedDowncast<AST::CompoundAssignmentStatement>(statement).leftExpression());
break;
case AST::NodeKind::VariableStatement: {
auto* initializer = uncheckedDowncast<AST::VariableStatement>(statement).variable().maybeInitializer();
if (!initializer || !initializer->inferredType())
break;
if (std::holds_alternative<Types::Pointer>(*initializer->inferredType()))
hoistIndices(*initializer);
break;
}
default:
break;
}
}
void EvaluationOrderRewriter::hoistIndices(AST::Expression& expression)
{
switch (expression.kind()) {
case AST::NodeKind::IdentityExpression:
hoistIndices(uncheckedDowncast<AST::IdentityExpression>(expression).expression());
break;
case AST::NodeKind::UnaryExpression:
hoistIndices(uncheckedDowncast<AST::UnaryExpression>(expression).expression());
break;
case AST::NodeKind::PointerDereferenceExpression:
hoistIndices(uncheckedDowncast<AST::PointerDereferenceExpression>(expression).target());
break;
case AST::NodeKind::FieldAccessExpression:
hoistIndices(uncheckedDowncast<AST::FieldAccessExpression>(expression).base());
break;
case AST::NodeKind::IndexAccessExpression: {
auto& access = uncheckedDowncast<AST::IndexAccessExpression>(expression);
// The base sits to the left of the index, so anything hoisted out of it has to come first.
hoistIndices(access.base());
if (mayHaveSideEffects(access.index()))
hoist(access);
break;
}
default:
break;
}
}
bool EvaluationOrderRewriter::mayHaveSideEffects(AST::Expression& expression)
{
// A const expression was already folded, so whatever it called happened at compile time.
if (expression.constantValue())
return false;
auto* type = expression.inferredType();
if (!type)
return false;
// An index is normally a plain integer. A reference or a pointer would need the hoisted let to
// be declared with a type this pass has no way to spell, and cannot be produced by a call, so
// leave those alone.
if (std::holds_alternative<Types::Reference>(*type) || std::holds_alternative<Types::Pointer>(*type))
return false;
return SideEffectFinder(m_userFunctions).check(expression);
}
void EvaluationOrderRewriter::hoist(AST::IndexAccessExpression& access)
{
auto& index = access.index();
auto name = makeString("__evalOrder"_s, m_hoistCount++);
auto& variable = m_shaderModule.astBuilder().construct<AST::Variable>(
SourceSpan::empty(),
AST::VariableFlavor::Let,
AST::Identifier::make(name),
nullptr,
nullptr,
&index,
AST::Attribute::List { }
);
m_hoisted.append(m_shaderModule.astBuilder().construct<AST::VariableStatement>(
SourceSpan::empty(),
variable
));
auto& identifier = m_shaderModule.astBuilder().construct<AST::IdentifierExpression>(
SourceSpan::empty(),
AST::Identifier::make(name)
);
identifier.m_inferredType = index.inferredType();
auto& newAccess = m_shaderModule.astBuilder().construct<AST::IndexAccessExpression>(
access.span(),
access.base(),
identifier
);
newAccess.m_inferredType = access.inferredType();
m_shaderModule.replace(access, newAccess);
}
void rewriteEvaluationOrder(ShaderModule& shaderModule)
{
EvaluationOrderRewriter(shaderModule).run();
}
} // namespace WGSL