[ir] Emit `var` and `let` into the IR

This CL adds a nodes for `var` variables. In order to support
global var a new module level block is added, the `root_block`. It only
exists if there are declarations which are emitted into it. The
initializer for the `var` is assigned through a `store` instruction.

A `let` declaration is just the initializer result.

Bug: tint:1897
Change-Id: Icb949699ef643988151cc52026f9ad6a12cdd93f
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/130761
Reviewed-by: Ben Clayton <bclayton@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
Commit-Queue: Dan Sinclair <dsinclair@chromium.org>
diff --git a/src/tint/BUILD.gn b/src/tint/BUILD.gn
index 56c2031..c6affb1 100644
--- a/src/tint/BUILD.gn
+++ b/src/tint/BUILD.gn
@@ -1173,6 +1173,8 @@
     "ir/user_call.h",
     "ir/value.cc",
     "ir/value.h",
+    "ir/var.cc",
+    "ir/var.h",
   ]
 
   deps = [
diff --git a/src/tint/CMakeLists.txt b/src/tint/CMakeLists.txt
index 2bd0cee..2e1ded8 100644
--- a/src/tint/CMakeLists.txt
+++ b/src/tint/CMakeLists.txt
@@ -750,6 +750,8 @@
     ir/user_call.h
     ir/value.cc
     ir/value.h
+    ir/var.cc
+    ir/var.h
   )
 endif()
 
diff --git a/src/tint/ir/builder.cc b/src/tint/ir/builder.cc
index 7c020df..2c97737 100644
--- a/src/tint/ir/builder.cc
+++ b/src/tint/ir/builder.cc
@@ -24,6 +24,17 @@
 
 Builder::~Builder() = default;
 
+ir::Block* Builder::CreateRootBlockIfNeeded() {
+    if (!ir.root_block) {
+        ir.root_block = CreateBlock();
+
+        // Everything in the module scope must have been const-eval's, so everything will go into a
+        // single block. So, we can create the terminator for the root-block now.
+        ir.root_block->branch.target = CreateTerminator();
+    }
+    return ir.root_block;
+}
+
 Block* Builder::CreateBlock() {
     return ir.flow_nodes.Create<Block>();
 }
@@ -225,4 +236,10 @@
     return ir.instructions.Create<ir::Store>(to, from);
 }
 
+ir::Var* Builder::Declare(const type::Type* type,
+                          builtin::AddressSpace address_space,
+                          builtin::Access access) {
+    return ir.instructions.Create<ir::Var>(next_inst_id(), type, address_space, access);
+}
+
 }  // namespace tint::ir
diff --git a/src/tint/ir/builder.h b/src/tint/ir/builder.h
index 9c847c6..d509c06 100644
--- a/src/tint/ir/builder.h
+++ b/src/tint/ir/builder.h
@@ -35,6 +35,7 @@
 #include "src/tint/ir/unary.h"
 #include "src/tint/ir/user_call.h"
 #include "src/tint/ir/value.h"
+#include "src/tint/ir/var.h"
 #include "src/tint/type/bool.h"
 #include "src/tint/type/f16.h"
 #include "src/tint/type/f32.h"
@@ -358,6 +359,19 @@
     /// @returns the instruction
     ir::Store* Store(Value* to, Value* from);
 
+    /// Creates a new `var` declaration
+    /// @param type the var type
+    /// @param address_space the address space
+    /// @param access the access mode
+    /// @returns the instruction
+    ir::Var* Declare(const type::Type* type,
+                     builtin::AddressSpace address_space,
+                     builtin::Access access);
+
+    /// Retrieves the root block for the module, creating if necessary
+    /// @returns the root block
+    ir::Block* CreateRootBlockIfNeeded();
+
     /// The IR module.
     Module ir;
 
diff --git a/src/tint/ir/builder_impl.cc b/src/tint/ir/builder_impl.cc
index 90f7fb3..19caf6f 100644
--- a/src/tint/ir/builder_impl.cc
+++ b/src/tint/ir/builder_impl.cc
@@ -39,6 +39,7 @@
 #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/let.h"
 #include "src/tint/ast/literal_expression.h"
 #include "src/tint/ast/loop_statement.h"
 #include "src/tint/ast/override.h"
@@ -50,6 +51,7 @@
 #include "src/tint/ast/switch_statement.h"
 #include "src/tint/ast/templated_identifier.h"
 #include "src/tint/ast/unary_op_expression.h"
+#include "src/tint/ast/var.h"
 #include "src/tint/ast/variable_decl_statement.h"
 #include "src/tint/ast/while_statement.h"
 #include "src/tint/ir/function.h"
@@ -69,8 +71,10 @@
 #include "src/tint/sem/value_constructor.h"
 #include "src/tint/sem/value_conversion.h"
 #include "src/tint/sem/value_expression.h"
+#include "src/tint/sem/variable.h"
 #include "src/tint/switch.h"
 #include "src/tint/type/void.h"
+#include "src/tint/utils/scoped_assignment.h"
 
 namespace tint::ir {
 namespace {
@@ -169,10 +173,13 @@
             [&](const ast::Alias*) {
                 // Folded away and doesn't appear in the IR.
             },
-            // [&](const ast::Variable* var) {
-            // TODO(dsinclair): Implement
-            // },
-            [&](const ast::Function* func) { return EmitFunction(func); },
+            [&](const ast::Variable* var) {
+                // Setup the current flow node to be the root block for the module. The builder will
+                // handle creating it if it doesn't exist already.
+                TINT_SCOPED_ASSIGNMENT(current_flow_block, builder.CreateRootBlockIfNeeded());
+                EmitVariable(var);
+            },
+            [&](const ast::Function* func) { EmitFunction(func); },
             // [&](const ast::Enable*) {
             // TODO(dsinclair): Implement? I think these need to be passed along so further stages
             // know what is enabled.
@@ -430,8 +437,8 @@
         BranchToIfNeeded(loop_node->start.target);
     }
 
-    // The loop merge can get disconnected if the loop returns directly, or the continuing target
-    // branches, eventually, to the merge, but nothing branched to the continuing target.
+    // The loop merge can get disconnected if the loop returns directly, or the continuing
+    // target branches, eventually, to the merge, but nothing branched to the continuing target.
     current_flow_block = loop_node->merge.target->As<Block>();
     if (!IsConnected(loop_node->merge.target)) {
         current_flow_block = nullptr;
@@ -613,9 +620,9 @@
 }
 
 // Discard is being treated as an instruction. The semantics in WGSL is demote_to_helper, so the
-// code has to continue as before it just predicates writes. If WGSL grows some kind of terminating
-// discard that would probably make sense as a FlowNode but would then require figuring out the
-// multi-level exit that is triggered.
+// code has to continue as before it just predicates writes. If WGSL grows some kind of
+// terminating discard that would probably make sense as a FlowNode but would then require
+// figuring out the multi-level exit that is triggered.
 void BuilderImpl::EmitDiscard(const ast::DiscardStatement*) {
     auto* inst = builder.Discard();
     current_flow_block->instructions.Push(inst);
@@ -691,14 +698,35 @@
 }
 
 void BuilderImpl::EmitVariable(const ast::Variable* var) {
+    auto* sem = program_->Sem().Get(var);
+
     return tint::Switch(  //
         var,
-        // [&](const ast::Var* var) {
-        // TODO(dsinclair): Implement
-        // },
-        // [&](const ast::Let*) {
-        // TODO(dsinclair): Implement
-        // },
+        [&](const ast::Var* v) {
+            auto* ty = sem->Type()->Clone(clone_ctx_.type_ctx);
+            auto* val = builder.Declare(ty, sem->AddressSpace(), sem->Access());
+            current_flow_block->instructions.Push(val);
+
+            if (v->initializer) {
+                auto init = EmitExpression(v->initializer);
+                if (!init) {
+                    return;
+                }
+
+                auto* store = builder.Store(val, init.Get());
+                current_flow_block->instructions.Push(store);
+            }
+            // TODO(dsinclair): Store the mapping from the var name to the `Declare` value
+        },
+        [&](const ast::Let* l) {
+            // A `let` doesn't exist as a standalone item in the IR, it's just the result of the
+            // initializer.
+            auto init = EmitExpression(l->initializer);
+            if (!init) {
+                return;
+            }
+            // TODO(dsinclair): Store the mapping from the let name to the `init` value
+        },
         [&](const ast::Override*) {
             add_error(var->source,
                       "found an `Override` variable. The SubstituteOverrides "
diff --git a/src/tint/ir/builder_impl_test.cc b/src/tint/ir/builder_impl_test.cc
index 3cd3c60..1e6a26d 100644
--- a/src/tint/ir/builder_impl_test.cc
+++ b/src/tint/ir/builder_impl_test.cc
@@ -1558,6 +1558,72 @@
     EXPECT_EQ(2_u, val->As<constant::Scalar<u32>>()->ValueAs<f32>());
 }
 
+TEST_F(IR_BuilderImplTest, Emit_GlobalVar_NoInit) {
+    GlobalVar("a", ty.u32(), builtin::AddressSpace::kPrivate);
+
+    auto r = Build();
+    ASSERT_TRUE(r) << Error();
+    auto m = r.Move();
+
+    EXPECT_EQ(Disassemble(m), R"(%fn0 = block
+%1(ref<private, u32, read_write>) = var private read_write
+ret
+
+)");
+}
+
+TEST_F(IR_BuilderImplTest, Emit_GlobalVar_Init) {
+    auto* expr = Expr(2_u);
+    GlobalVar("a", ty.u32(), builtin::AddressSpace::kPrivate, expr);
+
+    auto r = Build();
+    ASSERT_TRUE(r) << Error();
+    auto m = r.Move();
+
+    EXPECT_EQ(Disassemble(m), R"(%fn0 = block
+%1(ref<private, u32, read_write>) = var private read_write
+store %1(ref<private, u32, read_write>), 2u
+ret
+
+)");
+}
+
+TEST_F(IR_BuilderImplTest, Emit_Var_NoInit) {
+    auto* a = Var("a", ty.u32(), builtin::AddressSpace::kFunction);
+    WrapInFunction(a);
+
+    auto r = Build();
+    ASSERT_TRUE(r) << Error();
+    auto m = r.Move();
+
+    EXPECT_EQ(Disassemble(m), R"(%fn0 = func test_function
+  %fn1 = block
+  %1(ref<function, u32, read_write>) = var function read_write
+  ret
+func_end
+
+)");
+}
+
+TEST_F(IR_BuilderImplTest, Emit_Var_Init) {
+    auto* expr = Expr(2_u);
+    auto* a = Var("a", ty.u32(), builtin::AddressSpace::kFunction, expr);
+    WrapInFunction(a);
+
+    auto r = Build();
+    ASSERT_TRUE(r) << Error();
+    auto m = r.Move();
+
+    EXPECT_EQ(Disassemble(m), R"(%fn0 = func test_function
+  %fn1 = block
+  %1(ref<function, u32, read_write>) = var function read_write
+  store %1(ref<function, u32, read_write>), 2u
+  ret
+func_end
+
+)");
+}
+
 TEST_F(IR_BuilderImplTest, EmitExpression_Binary_Add) {
     Func("my_func", utils::Empty, ty.u32(), utils::Vector{Return(0_u)});
     auto* expr = Add(Call("my_func"), 4_u);
diff --git a/src/tint/ir/disassembler.cc b/src/tint/ir/disassembler.cc
index 53bf722..2b8de0b 100644
--- a/src/tint/ir/disassembler.cc
+++ b/src/tint/ir/disassembler.cc
@@ -20,6 +20,7 @@
 #include "src/tint/ir/switch.h"
 #include "src/tint/ir/terminator.h"
 #include "src/tint/switch.h"
+#include "src/tint/utils/scoped_assignment.h"
 
 namespace tint::ir {
 namespace {
@@ -89,6 +90,8 @@
     tint::Switch(
         node,
         [&](const ir::Function* f) {
+            TINT_SCOPED_ASSIGNMENT(in_function_, true);
+
             Indent() << "%fn" << GetIdForNode(f) << " = func " << f->name.Name() << std::endl;
 
             {
@@ -241,11 +244,19 @@
                 Walk(l->merge.target);
             }
         },
-        [&](const ir::Terminator*) { Indent() << "func_end" << std::endl
-                                              << std::endl; });
+        [&](const ir::Terminator*) {
+            if (in_function_) {
+                Indent() << "func_end" << std::endl;
+            }
+            out_ << std::endl;
+        });
 }
 
 std::string Disassembler::Disassemble() {
+    if (mod_.root_block) {
+        Walk(mod_.root_block);
+    }
+
     for (const auto* func : mod_.functions) {
         Walk(func);
     }
diff --git a/src/tint/ir/disassembler.h b/src/tint/ir/disassembler.h
index eee6b76..04b3997 100644
--- a/src/tint/ir/disassembler.h
+++ b/src/tint/ir/disassembler.h
@@ -56,6 +56,7 @@
     std::unordered_map<const FlowNode*, size_t> flow_node_to_id_;
     size_t next_node_id_ = 0;
     uint32_t indent_size_ = 0;
+    bool in_function_ = false;
 };
 
 }  // namespace tint::ir
diff --git a/src/tint/ir/module.h b/src/tint/ir/module.h
index a7172ea..ebf9209 100644
--- a/src/tint/ir/module.h
+++ b/src/tint/ir/module.h
@@ -65,6 +65,9 @@
     /// List of indexes into the functions list for the entry points
     utils::Vector<Function*, 8> entry_points;
 
+    /// The block containing module level declarations, if any exist.
+    Block* root_block = nullptr;
+
     /// The type manager for the module
     type::Manager types;
 
diff --git a/src/tint/ir/var.cc b/src/tint/ir/var.cc
new file mode 100644
index 0000000..740a35e
--- /dev/null
+++ b/src/tint/ir/var.cc
@@ -0,0 +1,35 @@
+// 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/var.h"
+#include "src/tint/debug.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ir::Var);
+
+namespace tint::ir {
+
+Var::Var(uint32_t id,
+         const type::Type* ty,
+         builtin::AddressSpace address_space,
+         builtin::Access access)
+    : Base(id, ty), address_space_(address_space), access_(access) {}
+
+Var::~Var() = default;
+
+utils::StringStream& Var::ToInstruction(utils::StringStream& out) const {
+    ToValue(out) << " = var " << address_space_ << " " << access_;
+    return out;
+}
+
+}  // namespace tint::ir
diff --git a/src/tint/ir/var.h b/src/tint/ir/var.h
new file mode 100644
index 0000000..456e7d3
--- /dev/null
+++ b/src/tint/ir/var.h
@@ -0,0 +1,63 @@
+// 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_VAR_H_
+#define SRC_TINT_IR_VAR_H_
+
+#include "src/tint/builtin/access.h"
+#include "src/tint/builtin/address_space.h"
+#include "src/tint/ir/instruction.h"
+#include "src/tint/utils/castable.h"
+#include "src/tint/utils/string_stream.h"
+
+namespace tint::ir {
+
+/// An instruction in the IR.
+class Var : public utils::Castable<Var, Instruction> {
+  public:
+    /// Constructor
+    /// @param id the instruction id
+    /// @param type the type
+    /// @param address_space the address space of the var
+    /// @param access the access mode of the var
+    Var(uint32_t id,
+        const type::Type* type,
+        builtin::AddressSpace address_space,
+        builtin::Access access);
+    Var(const Var& inst) = delete;
+    Var(Var&& inst) = delete;
+    ~Var() override;
+
+    Var& operator=(const Var& inst) = delete;
+    Var& operator=(Var&& inst) = delete;
+
+    /// @returns the address space
+    builtin::AddressSpace AddressSpace() const { return address_space_; }
+
+    /// @returns the access mode
+    builtin::Access Access() const { return access_; }
+
+    /// Write the instruction to the given stream
+    /// @param out the stream to write to
+    /// @returns the stream
+    utils::StringStream& ToInstruction(utils::StringStream& out) const override;
+
+  private:
+    builtin::AddressSpace address_space_;
+    builtin::Access access_;
+};
+
+}  // namespace tint::ir
+
+#endif  // SRC_TINT_IR_VAR_H_