[ir] Add user functions, value constructors and value converters

This CL adds user function calls, value constructors and value
converters into the IR.

Bug: tint:1718
Change-Id: Iab59aa7d01b9d7831299d785f6e45e9fba12f7b5
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/122600
Reviewed-by: Ben Clayton <bclayton@google.com>
Commit-Queue: Dan Sinclair <dsinclair@chromium.org>
Kokoro: Kokoro <noreply+kokoro@google.com>
diff --git a/src/tint/CMakeLists.txt b/src/tint/CMakeLists.txt
index 38fe7c1..4ea36bb 100644
--- a/src/tint/CMakeLists.txt
+++ b/src/tint/CMakeLists.txt
@@ -699,8 +699,14 @@
     ir/builder.h
     ir/builder_impl.cc
     ir/builder_impl.h
+    ir/call.cc
+    ir/call.h
     ir/constant.cc
     ir/constant.h
+    ir/construct.cc
+    ir/construct.h
+    ir/convert.cc
+    ir/convert.h
     ir/debug.cc
     ir/debug.h
     ir/disassembler.cc
@@ -723,6 +729,8 @@
     ir/temp.h
     ir/terminator.cc
     ir/terminator.h
+    ir/user_call.cc
+    ir/user_call.h
     ir/value.cc
     ir/value.h
   )
diff --git a/src/tint/ir/builder.cc b/src/tint/ir/builder.cc
index 1452dc4..96578c5 100644
--- a/src/tint/ir/builder.cc
+++ b/src/tint/ir/builder.cc
@@ -177,4 +177,20 @@
     return ir.instructions.Create<ir::Bitcast>(Temp(type), val);
 }
 
+ir::UserCall* Builder::UserCall(const type::Type* type,
+                                Symbol name,
+                                utils::VectorRef<Value*> args) {
+    return ir.instructions.Create<ir::UserCall>(Temp(type), name, std::move(args));
+}
+
+ir::Convert* Builder::Convert(const type::Type* to,
+                              const type::Type* from,
+                              utils::VectorRef<Value*> args) {
+    return ir.instructions.Create<ir::Convert>(Temp(to), from, std::move(args));
+}
+
+ir::Construct* Builder::Construct(const type::Type* to, utils::VectorRef<Value*> args) {
+    return ir.instructions.Create<ir::Construct>(Temp(to), std::move(args));
+}
+
 }  // namespace tint::ir
diff --git a/src/tint/ir/builder.h b/src/tint/ir/builder.h
index 147e4c4..e7d64bb 100644
--- a/src/tint/ir/builder.h
+++ b/src/tint/ir/builder.h
@@ -21,6 +21,8 @@
 #include "src/tint/ir/binary.h"
 #include "src/tint/ir/bitcast.h"
 #include "src/tint/ir/constant.h"
+#include "src/tint/ir/construct.h"
+#include "src/tint/ir/convert.h"
 #include "src/tint/ir/function.h"
 #include "src/tint/ir/if.h"
 #include "src/tint/ir/loop.h"
@@ -28,6 +30,7 @@
 #include "src/tint/ir/switch.h"
 #include "src/tint/ir/temp.h"
 #include "src/tint/ir/terminator.h"
+#include "src/tint/ir/user_call.h"
 #include "src/tint/ir/value.h"
 #include "src/tint/type/bool.h"
 #include "src/tint/type/f16.h"
@@ -279,6 +282,28 @@
     /// @returns the instruction
     ir::Bitcast* Bitcast(const type::Type* type, Value* val);
 
+    /// Creates a user function call instruction
+    /// @param type the return type of the call
+    /// @param name the name of the function being called
+    /// @param args the call arguments
+    /// @returns the instruction
+    ir::UserCall* UserCall(const type::Type* type, Symbol name, utils::VectorRef<Value*> args);
+
+    /// Creates a value conversion instruction
+    /// @param to the type converted to
+    /// @param from the type converted from
+    /// @param args the arguments to be converted
+    /// @returns the instruction
+    ir::Convert* Convert(const type::Type* to,
+                         const type::Type* from,
+                         utils::VectorRef<Value*> args);
+
+    /// Creates a value constructor instruction
+    /// @param to the type being converted
+    /// @param args the arguments to be converted
+    /// @returns the instruction
+    ir::Construct* Construct(const type::Type* to, utils::VectorRef<Value*> args);
+
     /// @returns a unique temp id
     Temp::Id AllocateTempId();
 
diff --git a/src/tint/ir/builder_impl.cc b/src/tint/ir/builder_impl.cc
index d3f8d53..e773966 100644
--- a/src/tint/ir/builder_impl.cc
+++ b/src/tint/ir/builder_impl.cc
@@ -14,6 +14,8 @@
 
 #include "src/tint/ir/builder_impl.h"
 
+#include <iostream>
+
 #include "src/tint/ast/alias.h"
 #include "src/tint/ast/binary_expression.h"
 #include "src/tint/ast/bitcast_expression.h"
@@ -21,6 +23,8 @@
 #include "src/tint/ast/bool_literal_expression.h"
 #include "src/tint/ast/break_if_statement.h"
 #include "src/tint/ast/break_statement.h"
+#include "src/tint/ast/call_expression.h"
+#include "src/tint/ast/call_statement.h"
 #include "src/tint/ast/const_assert.h"
 #include "src/tint/ast/continue_statement.h"
 #include "src/tint/ast/float_literal_expression.h"
@@ -28,6 +32,7 @@
 #include "src/tint/ast/function.h"
 #include "src/tint/ast/id_attribute.h"
 #include "src/tint/ast/identifier.h"
+#include "src/tint/ast/identifier_expression.h"
 #include "src/tint/ast/if_statement.h"
 #include "src/tint/ast/int_literal_expression.h"
 #include "src/tint/ast/literal_expression.h"
@@ -39,6 +44,7 @@
 #include "src/tint/ast/struct_member_align_attribute.h"
 #include "src/tint/ast/struct_member_size_attribute.h"
 #include "src/tint/ast/switch_statement.h"
+#include "src/tint/ast/templated_identifier.h"
 #include "src/tint/ast/variable_decl_statement.h"
 #include "src/tint/ast/while_statement.h"
 #include "src/tint/ir/function.h"
@@ -48,8 +54,12 @@
 #include "src/tint/ir/switch.h"
 #include "src/tint/ir/terminator.h"
 #include "src/tint/program.h"
+#include "src/tint/sem/builtin.h"
+#include "src/tint/sem/call.h"
 #include "src/tint/sem/module.h"
 #include "src/tint/sem/switch_statement.h"
+#include "src/tint/sem/value_constructor.h"
+#include "src/tint/sem/value_conversion.h"
 #include "src/tint/sem/value_expression.h"
 #include "src/tint/type/void.h"
 
@@ -237,9 +247,7 @@
         [&](const ast::BlockStatement* b) { return EmitBlock(b); },
         [&](const ast::BreakStatement* b) { return EmitBreak(b); },
         [&](const ast::BreakIfStatement* b) { return EmitBreakIf(b); },
-        // [&](const ast::CallStatement* c) {
-        // TODO(dsinclair): Implement
-        // },
+        [&](const ast::CallStatement* c) { return EmitCall(c); },
         // [&](const ast::CompoundAssignmentStatement* c) {
         // TODO(dsinclair): Implement
         // },
@@ -593,9 +601,7 @@
         // },
         [&](const ast::BinaryExpression* b) { return EmitBinary(b); },
         [&](const ast::BitcastExpression* b) { return EmitBitcast(b); },
-        // [&](const ast::CallExpression* c) {
-        // TODO(dsinclair): Implement
-        // },
+        [&](const ast::CallExpression* c) { return EmitCall(c); },
         // [&](const ast::IdentifierExpression* i) {
         // TODO(dsinclair): Implement
         // },
@@ -743,6 +749,63 @@
     return instr->Result();
 }
 
+utils::Result<Value*> BuilderImpl::EmitCall(const ast::CallStatement* stmt) {
+    return EmitCall(stmt->expr);
+}
+
+utils::Result<Value*> BuilderImpl::EmitCall(const ast::CallExpression* expr) {
+    utils::Vector<Value*, 8> args;
+    args.Reserve(expr->args.Length());
+
+    // Emit the arguments
+    for (const auto* arg : expr->args) {
+        auto value = EmitExpression(arg);
+        if (!value) {
+            diagnostics_.add_error(tint::diag::System::IR, "Failed to convert arguments",
+                                   arg->source);
+            return utils::Failure;
+        }
+        args.Push(value.Get());
+    }
+
+    auto* sem = program_->Sem().Get<sem::Call>(expr);
+    if (!sem) {
+        diagnostics_.add_error(
+            tint::diag::System::IR,
+            "Failed to get semantic information for call " + std::string(expr->TypeInfo().name),
+            expr->source);
+        return utils::Failure;
+    }
+
+    auto* ty = sem->Target()->ReturnType()->Clone(clone_ctx_.type_ctx);
+
+    Instruction* instr = nullptr;
+
+    // If this is a builtin function, emit the specific builtin value
+    if (sem->Target()->As<sem::Builtin>()) {
+        // TODO(dsinclair): .. something ...
+        diagnostics_.add_error(tint::diag::System::IR, "Missing builtin function support",
+                               expr->source);
+    } else if (sem->Target()->As<sem::ValueConstructor>()) {
+        instr = builder.Construct(ty, std::move(args));
+    } else if (auto* conv = sem->Target()->As<sem::ValueConversion>()) {
+        auto* from = conv->Source()->Clone(clone_ctx_.type_ctx);
+        instr = builder.Convert(ty, from, std::move(args));
+    } else if (expr->target->identifier->Is<ast::TemplatedIdentifier>()) {
+        TINT_UNIMPLEMENTED(IR, diagnostics_) << "Missing templated ident support";
+        return utils::Failure;
+    } else {
+        // Not a builtin and not a templated call, so this is a user function.
+        auto name = CloneSymbol(expr->target->identifier->symbol);
+        instr = builder.UserCall(ty, name, std::move(args));
+    }
+    if (instr == nullptr) {
+        return utils::Failure;
+    }
+    current_flow_block->instructions.Push(instr);
+    return instr->Result();
+}
+
 utils::Result<Value*> BuilderImpl::EmitLiteral(const ast::LiteralExpression* lit) {
     auto* sem = program_->Sem().Get(lit);
     if (!sem) {
diff --git a/src/tint/ir/builder_impl.h b/src/tint/ir/builder_impl.h
index 034ab65..e531aed 100644
--- a/src/tint/ir/builder_impl.h
+++ b/src/tint/ir/builder_impl.h
@@ -39,6 +39,8 @@
 class BlockStatement;
 class BreakIfStatement;
 class BreakStatement;
+class CallExpression;
+class CallStatement;
 class ContinueStatement;
 class Expression;
 class ForLoopStatement;
@@ -61,6 +63,9 @@
 class Switch;
 class Terminator;
 }  // namespace tint::ir
+namespace tint::sem {
+class Builtin;
+}  // namespace tint::sem
 
 namespace tint::ir {
 
@@ -165,6 +170,16 @@
     /// @returns the value storing the result if successful, utils::Failure otherwise
     utils::Result<Value*> EmitBitcast(const ast::BitcastExpression* expr);
 
+    /// Emits a call expression
+    /// @param stmt the call statement
+    /// @returns the value storing the result if successful, utils::Failure otherwise
+    utils::Result<Value*> EmitCall(const ast::CallStatement* stmt);
+
+    /// Emits a call expression
+    /// @param expr the call expression
+    /// @returns the value storing the result if successful, utils::Failure otherwise
+    utils::Result<Value*> EmitCall(const ast::CallExpression* expr);
+
     /// Emits a literal expression
     /// @param lit the literal to emit
     /// @returns true if successful, false otherwise
diff --git a/src/tint/ir/builder_impl_test.cc b/src/tint/ir/builder_impl_test.cc
index 9af481b..be94e65 100644
--- a/src/tint/ir/builder_impl_test.cc
+++ b/src/tint/ir/builder_impl_test.cc
@@ -1858,5 +1858,70 @@
 )");
 }
 
+TEST_F(IR_BuilderImplTest, EmitStatement_UserFunction) {
+    Func("my_func", utils::Vector{Param("p", ty.f32())}, ty.void_(), utils::Empty);
+
+    auto* stmt = CallStmt(Call("my_func", Mul(2_f, 3_f)));
+    WrapInFunction(stmt);
+
+    auto& b = CreateBuilder();
+    InjectFlowBlock();
+    auto r = b.EmitStatement(stmt);
+    ASSERT_TRUE(r) << b.error();
+
+    Disassembler d(b.builder.ir);
+    d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
+    EXPECT_EQ(d.AsString(), R"(%1 (f32) = 2.0 * 3.0
+%2 (void) = call(my_func, %1 (f32))
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitExpression_ConstructEmpty) {
+    auto* expr = vec3(ty.f32());
+    WrapInFunction(expr);
+
+    auto& b = CreateBuilder();
+    InjectFlowBlock();
+    auto r = b.EmitExpression(expr);
+    ASSERT_TRUE(r) << b.error();
+
+    Disassembler d(b.builder.ir);
+    d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
+    EXPECT_EQ(d.AsString(), R"(%1 (vec3<f32>) = construct(vec3<f32>)
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Construct) {
+    auto i = GlobalVar("i", builtin::AddressSpace::kPrivate, Expr(1_f));
+    auto* expr = vec3(ty.f32(), 2_f, 3_f, i);
+    WrapInFunction(expr);
+
+    auto& b = CreateBuilder();
+    InjectFlowBlock();
+    auto r = b.EmitExpression(expr);
+    ASSERT_TRUE(r) << b.error();
+
+    Disassembler d(b.builder.ir);
+    d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
+    EXPECT_EQ(d.AsString(), R"(%2 (vec3<f32>) = construct(vec3<f32>, 2.0, 3.0, %1 (void))
+)");
+}
+
+TEST_F(IR_BuilderImplTest, EmitExpression_Convert) {
+    auto i = GlobalVar("i", builtin::AddressSpace::kPrivate, Expr(1_i));
+    auto* expr = Call(ty.f32(), i);
+    WrapInFunction(expr);
+
+    auto& b = CreateBuilder();
+    InjectFlowBlock();
+    auto r = b.EmitExpression(expr);
+    ASSERT_TRUE(r) << b.error();
+
+    Disassembler d(b.builder.ir);
+    d.EmitBlockInstructions(b.current_flow_block->As<ir::Block>());
+    EXPECT_EQ(d.AsString(), R"(%2 (f32) = convert(f32, i32, %1 (void))
+)");
+}
+
 }  // namespace
 }  // namespace tint::ir
diff --git a/src/tint/ir/call.cc b/src/tint/ir/call.cc
new file mode 100644
index 0000000..9dda1f9
--- /dev/null
+++ b/src/tint/ir/call.cc
@@ -0,0 +1,40 @@
+// 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/call.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ir::Call);
+
+namespace tint::ir {
+
+Call::Call(Value* result, utils::VectorRef<Value*> args) : Base(result), args_(args) {
+    for (auto* arg : args) {
+        arg->AddUsage(this);
+    }
+}
+
+Call::~Call() = default;
+
+void Call::EmitArgs(utils::StringStream& out, const SymbolTable& st) const {
+    bool first = true;
+    for (const auto* arg : args_) {
+        if (!first) {
+            out << ", ";
+        }
+        first = false;
+        arg->ToString(out, st);
+    }
+}
+
+}  // namespace tint::ir
diff --git a/src/tint/ir/call.h b/src/tint/ir/call.h
new file mode 100644
index 0000000..d6ced3f
--- /dev/null
+++ b/src/tint/ir/call.h
@@ -0,0 +1,54 @@
+// 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.
+
+#ifndef SRC_TINT_IR_CALL_H_
+#define SRC_TINT_IR_CALL_H_
+
+#include "src/tint/castable.h"
+#include "src/tint/ir/instruction.h"
+#include "src/tint/symbol_table.h"
+#include "src/tint/type/type.h"
+#include "src/tint/utils/string_stream.h"
+
+namespace tint::ir {
+
+/// A Call instruction in the IR.
+class Call : public Castable<Call, Instruction> {
+  public:
+    /// Constructor
+    /// @param result the result value
+    /// @param args the constructor arguments
+    Call(Value* result, utils::VectorRef<Value*> args);
+    Call(const Call& instr) = delete;
+    Call(Call&& instr) = delete;
+    ~Call() override;
+
+    Call& operator=(const Call& instr) = delete;
+    Call& operator=(Call&& instr) = delete;
+
+    /// @returns the constructor arguments
+    utils::VectorRef<Value*> Args() const { return args_; }
+
+    /// Writes the call arguments to the given stream.
+    /// @param out the output stream
+    /// @param st the symbol table
+    void EmitArgs(utils::StringStream& out, const SymbolTable& st) const;
+
+  private:
+    utils::Vector<Value*, 1> args_;
+};
+
+}  // namespace tint::ir
+
+#endif  // SRC_TINT_IR_CALL_H_
diff --git a/src/tint/ir/construct.cc b/src/tint/ir/construct.cc
new file mode 100644
index 0000000..27bb084
--- /dev/null
+++ b/src/tint/ir/construct.cc
@@ -0,0 +1,37 @@
+// 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/construct.h"
+#include "src/tint/debug.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ir::Construct);
+
+namespace tint::ir {
+
+Construct::Construct(Value* result, utils::VectorRef<Value*> args) : Base(result, args) {}
+
+Construct::~Construct() = default;
+
+utils::StringStream& Construct::ToString(utils::StringStream& out, const SymbolTable& st) const {
+    Result()->ToString(out, st);
+    out << " = construct(" << Result()->Type()->FriendlyName(st);
+    if (!Args().IsEmpty()) {
+        out << ", ";
+        EmitArgs(out, st);
+    }
+    out << ")";
+    return out;
+}
+
+}  // namespace tint::ir
diff --git a/src/tint/ir/construct.h b/src/tint/ir/construct.h
new file mode 100644
index 0000000..6f620d3
--- /dev/null
+++ b/src/tint/ir/construct.h
@@ -0,0 +1,49 @@
+// 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.
+
+#ifndef SRC_TINT_IR_CONSTRUCT_H_
+#define SRC_TINT_IR_CONSTRUCT_H_
+
+#include "src/tint/castable.h"
+#include "src/tint/ir/call.h"
+#include "src/tint/symbol_table.h"
+#include "src/tint/type/type.h"
+#include "src/tint/utils/string_stream.h"
+
+namespace tint::ir {
+
+/// A constructor instruction in the IR.
+class Construct : public Castable<Construct, Call> {
+  public:
+    /// Constructor
+    /// @param result the result value
+    /// @param args the constructor arguments
+    Construct(Value* result, utils::VectorRef<Value*> args);
+    Construct(const Construct& instr) = delete;
+    Construct(Construct&& instr) = delete;
+    ~Construct() override;
+
+    Construct& operator=(const Construct& instr) = delete;
+    Construct& operator=(Construct&& instr) = delete;
+
+    /// Write the instruction to the given stream
+    /// @param out the stream to write to
+    /// @param st the symbol table
+    /// @returns the stream
+    utils::StringStream& ToString(utils::StringStream& out, const SymbolTable& st) const override;
+};
+
+}  // namespace tint::ir
+
+#endif  // SRC_TINT_IR_CONSTRUCT_H_
diff --git a/src/tint/ir/convert.cc b/src/tint/ir/convert.cc
new file mode 100644
index 0000000..e845adb
--- /dev/null
+++ b/src/tint/ir/convert.cc
@@ -0,0 +1,36 @@
+// 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/convert.h"
+#include "src/tint/debug.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ir::Convert);
+
+namespace tint::ir {
+
+Convert::Convert(Value* result, const type::Type* from, utils::VectorRef<Value*> args)
+    : Base(result, args), from_(from) {}
+
+Convert::~Convert() = default;
+
+utils::StringStream& Convert::ToString(utils::StringStream& out, const SymbolTable& st) const {
+    Result()->ToString(out, st);
+    out << " = convert(" << Result()->Type()->FriendlyName(st) << ", " << from_->FriendlyName(st)
+        << ", ";
+    EmitArgs(out, st);
+    out << ")";
+    return out;
+}
+
+}  // namespace tint::ir
diff --git a/src/tint/ir/convert.h b/src/tint/ir/convert.h
new file mode 100644
index 0000000..5132248
--- /dev/null
+++ b/src/tint/ir/convert.h
@@ -0,0 +1,58 @@
+// 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.
+
+#ifndef SRC_TINT_IR_CONVERT_H_
+#define SRC_TINT_IR_CONVERT_H_
+
+#include "src/tint/castable.h"
+#include "src/tint/ir/call.h"
+#include "src/tint/symbol_table.h"
+#include "src/tint/type/type.h"
+#include "src/tint/utils/string_stream.h"
+
+namespace tint::ir {
+
+/// A value conversion instruction in the IR.
+class Convert : public Castable<Convert, Call> {
+  public:
+    /// Constructor
+    /// @param result the result value
+    /// @param from the type being converted from
+    /// @param args the conversion arguments
+    Convert(Value* result, const type::Type* from, utils::VectorRef<Value*> args);
+    Convert(const Convert& instr) = delete;
+    Convert(Convert&& instr) = delete;
+    ~Convert() override;
+
+    Convert& operator=(const Convert& instr) = delete;
+    Convert& operator=(Convert&& instr) = delete;
+
+    /// @returns the from type
+    const type::Type* From() const { return from_; }
+    /// @returns the to type
+    const type::Type* To() const { return Result()->Type(); }
+
+    /// Write the instruction to the given stream
+    /// @param out the stream to write to
+    /// @param st the symbol table
+    /// @returns the stream
+    utils::StringStream& ToString(utils::StringStream& out, const SymbolTable& st) const override;
+
+  private:
+    const type::Type* from_ = nullptr;
+};
+
+}  // namespace tint::ir
+
+#endif  // SRC_TINT_IR_CONVERT_H_
diff --git a/src/tint/ir/test_helper.h b/src/tint/ir/test_helper.h
index 9fb54cc..6e36ed3 100644
--- a/src/tint/ir/test_helper.h
+++ b/src/tint/ir/test_helper.h
@@ -40,6 +40,8 @@
     /// return the same builder without rebuilding.
     /// @return the builder
     BuilderImpl& CreateBuilder() {
+        SetResolveOnBuild(true);
+
         if (gen_) {
             return *gen_;
         }
diff --git a/src/tint/ir/user_call.cc b/src/tint/ir/user_call.cc
new file mode 100644
index 0000000..cf672cf
--- /dev/null
+++ b/src/tint/ir/user_call.cc
@@ -0,0 +1,36 @@
+// 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/user_call.h"
+#include "src/tint/debug.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ir::UserCall);
+
+namespace tint::ir {
+
+UserCall::UserCall(Value* result, Symbol name, utils::VectorRef<Value*> args)
+    : Base(result, args), name_(name) {}
+
+UserCall::~UserCall() = default;
+
+utils::StringStream& UserCall::ToString(utils::StringStream& out, const SymbolTable& st) const {
+    Result()->ToString(out, st);
+    out << " = call(";
+    out << st.NameFor(name_) << ", ";
+    EmitArgs(out, st);
+    out << ")";
+    return out;
+}
+
+}  // namespace tint::ir
diff --git a/src/tint/ir/user_call.h b/src/tint/ir/user_call.h
new file mode 100644
index 0000000..2edeecb
--- /dev/null
+++ b/src/tint/ir/user_call.h
@@ -0,0 +1,56 @@
+// 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.
+
+#ifndef SRC_TINT_IR_USER_CALL_H_
+#define SRC_TINT_IR_USER_CALL_H_
+
+#include "src/tint/castable.h"
+#include "src/tint/ir/call.h"
+#include "src/tint/symbol_table.h"
+#include "src/tint/type/type.h"
+#include "src/tint/utils/string_stream.h"
+
+namespace tint::ir {
+
+/// A user call instruction in the IR.
+class UserCall : public Castable<UserCall, Call> {
+  public:
+    /// Constructor
+    /// @param result the result value
+    /// @param name the function name
+    /// @param args the function arguments
+    UserCall(Value* result, Symbol name, utils::VectorRef<Value*> args);
+    UserCall(const UserCall& instr) = delete;
+    UserCall(UserCall&& instr) = delete;
+    ~UserCall() override;
+
+    UserCall& operator=(const UserCall& instr) = delete;
+    UserCall& operator=(UserCall&& instr) = delete;
+
+    /// @returns the function name
+    Symbol Name() const { return name_; }
+
+    /// Write the instruction to the given stream
+    /// @param out the stream to write to
+    /// @param st the symbol table
+    /// @returns the stream
+    utils::StringStream& ToString(utils::StringStream& out, const SymbolTable& st) const override;
+
+  private:
+    Symbol name_{};
+};
+
+}  // namespace tint::ir
+
+#endif  // SRC_TINT_IR_USER_CALL_H_