Introduce semantic::Info

Will hold the mutable fields that currently reside in the otherwise immutable-AST.

Change the AST string methods to accept a `const semantic::Info&`. This is required as some nodes include type-resolved information in their output strings.

Bug: tint:390
Change-Id: Iba494a9c5645ce2096da0a8cfe63a4309a9d9c3c
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/39003
Commit-Queue: Ben Clayton <bclayton@google.com>
Reviewed-by: dan sinclair <dsinclair@chromium.org>
diff --git a/src/ast/access_decoration.cc b/src/ast/access_decoration.cc
index c535160..976ffe2 100644
--- a/src/ast/access_decoration.cc
+++ b/src/ast/access_decoration.cc
@@ -27,7 +27,9 @@
 
 AccessDecoration::~AccessDecoration() = default;
 
-void AccessDecoration::to_str(std::ostream& out, size_t indent) const {
+void AccessDecoration::to_str(const semantic::Info&,
+                              std::ostream& out,
+                              size_t indent) const {
   make_indent(out, indent);
   out << "AccessDecoration{" << value_ << "}" << std::endl;
 }
diff --git a/src/ast/access_decoration.h b/src/ast/access_decoration.h
index e58c13a..2a50e2c 100644
--- a/src/ast/access_decoration.h
+++ b/src/ast/access_decoration.h
@@ -36,9 +36,12 @@
   AccessControl value() const { return value_; }
 
   /// Outputs the decoration to the given stream
+  /// @param sem the semantic info for the program
   /// @param out the stream to write to
   /// @param indent number of spaces to indent the node when writing
-  void to_str(std::ostream& out, size_t indent) const override;
+  void to_str(const semantic::Info& sem,
+              std::ostream& out,
+              size_t indent) const override;
 
   /// Clones this node and all transitive child nodes using the `CloneContext`
   /// `ctx`.
diff --git a/src/ast/access_decoration_test.cc b/src/ast/access_decoration_test.cc
index 55295fc..e48fe71 100644
--- a/src/ast/access_decoration_test.cc
+++ b/src/ast/access_decoration_test.cc
@@ -37,7 +37,7 @@
 TEST_F(AccessDecorationTest, ToStr) {
   auto* d = create<AccessDecoration>(ast::AccessControl::kReadOnly);
   std::ostringstream out;
-  d->to_str(out, 0);
+  d->to_str(Sem(), out, 0);
   EXPECT_EQ(out.str(), R"(AccessDecoration{read_only}
 )");
 }
diff --git a/src/ast/array_accessor_expression.cc b/src/ast/array_accessor_expression.cc
index be763db..b8855b0 100644
--- a/src/ast/array_accessor_expression.cc
+++ b/src/ast/array_accessor_expression.cc
@@ -47,11 +47,13 @@
   return true;
 }
 
-void ArrayAccessorExpression::to_str(std::ostream& out, size_t indent) const {
+void ArrayAccessorExpression::to_str(const semantic::Info& sem,
+                                     std::ostream& out,
+                                     size_t indent) const {
   make_indent(out, indent);
-  out << "ArrayAccessor[" << result_type_str() << "]{" << std::endl;
-  array_->to_str(out, indent + 2);
-  idx_expr_->to_str(out, indent + 2);
+  out << "ArrayAccessor[" << result_type_str(sem) << "]{" << std::endl;
+  array_->to_str(sem, out, indent + 2);
+  idx_expr_->to_str(sem, out, indent + 2);
   make_indent(out, indent);
   out << "}" << std::endl;
 }
diff --git a/src/ast/array_accessor_expression.h b/src/ast/array_accessor_expression.h
index ebcf9c6..32911b2 100644
--- a/src/ast/array_accessor_expression.h
+++ b/src/ast/array_accessor_expression.h
@@ -57,9 +57,12 @@
   bool IsValid() const override;
 
   /// Writes a representation of the node to the output stream
+  /// @param sem the semantic info for the program
   /// @param out the stream to write to
   /// @param indent number of spaces to indent the node when writing
-  void to_str(std::ostream& out, size_t indent) const override;
+  void to_str(const semantic::Info& sem,
+              std::ostream& out,
+              size_t indent) const override;
 
  private:
   ArrayAccessorExpression(const ArrayAccessorExpression&) = delete;
diff --git a/src/ast/array_accessor_expression_test.cc b/src/ast/array_accessor_expression_test.cc
index be29266..b86266b 100644
--- a/src/ast/array_accessor_expression_test.cc
+++ b/src/ast/array_accessor_expression_test.cc
@@ -93,7 +93,7 @@
 
   auto* exp = create<ArrayAccessorExpression>(ary, idx);
   std::ostringstream out;
-  exp->to_str(out, 2);
+  exp->to_str(Sem(), out, 2);
 
   EXPECT_EQ(demangle(out.str()), R"(  ArrayAccessor[not set]{
     Identifier[not set]{ary}
diff --git a/src/ast/assignment_statement.cc b/src/ast/assignment_statement.cc
index a92c6e3..aec1d64 100644
--- a/src/ast/assignment_statement.cc
+++ b/src/ast/assignment_statement.cc
@@ -45,11 +45,13 @@
   return true;
 }
 
-void AssignmentStatement::to_str(std::ostream& out, size_t indent) const {
+void AssignmentStatement::to_str(const semantic::Info& sem,
+                                 std::ostream& out,
+                                 size_t indent) const {
   make_indent(out, indent);
   out << "Assignment{" << std::endl;
-  lhs_->to_str(out, indent + 2);
-  rhs_->to_str(out, indent + 2);
+  lhs_->to_str(sem, out, indent + 2);
+  rhs_->to_str(sem, out, indent + 2);
   make_indent(out, indent);
   out << "}" << std::endl;
 }
diff --git a/src/ast/assignment_statement.h b/src/ast/assignment_statement.h
index 01b5ac5..294b8e1 100644
--- a/src/ast/assignment_statement.h
+++ b/src/ast/assignment_statement.h
@@ -54,9 +54,12 @@
   bool IsValid() const override;
 
   /// Writes a representation of the node to the output stream
+  /// @param sem the semantic info for the program
   /// @param out the stream to write to
   /// @param indent number of spaces to indent the node when writing
-  void to_str(std::ostream& out, size_t indent) const override;
+  void to_str(const semantic::Info& sem,
+              std::ostream& out,
+              size_t indent) const override;
 
  private:
   AssignmentStatement(const AssignmentStatement&) = delete;
diff --git a/src/ast/assignment_statement_test.cc b/src/ast/assignment_statement_test.cc
index 2d7055d..cbf1813 100644
--- a/src/ast/assignment_statement_test.cc
+++ b/src/ast/assignment_statement_test.cc
@@ -93,7 +93,7 @@
 
   auto* stmt = create<AssignmentStatement>(lhs, rhs);
   std::ostringstream out;
-  stmt->to_str(out, 2);
+  stmt->to_str(Sem(), out, 2);
 
   EXPECT_EQ(demangle(out.str()), R"(  Assignment{
     Identifier[not set]{lhs}
diff --git a/src/ast/binary_expression.cc b/src/ast/binary_expression.cc
index 5c59cbd..c1fc0d6 100644
--- a/src/ast/binary_expression.cc
+++ b/src/ast/binary_expression.cc
@@ -47,15 +47,17 @@
   return op_ != BinaryOp::kNone;
 }
 
-void BinaryExpression::to_str(std::ostream& out, size_t indent) const {
+void BinaryExpression::to_str(const semantic::Info& sem,
+                              std::ostream& out,
+                              size_t indent) const {
   make_indent(out, indent);
-  out << "Binary[" << result_type_str() << "]{" << std::endl;
-  lhs_->to_str(out, indent + 2);
+  out << "Binary[" << result_type_str(sem) << "]{" << std::endl;
+  lhs_->to_str(sem, out, indent + 2);
 
   make_indent(out, indent + 2);
   out << op_ << std::endl;
 
-  rhs_->to_str(out, indent + 2);
+  rhs_->to_str(sem, out, indent + 2);
   make_indent(out, indent);
   out << "}" << std::endl;
 }
diff --git a/src/ast/binary_expression.h b/src/ast/binary_expression.h
index 9bce548..71a1859 100644
--- a/src/ast/binary_expression.h
+++ b/src/ast/binary_expression.h
@@ -120,9 +120,12 @@
   bool IsValid() const override;
 
   /// Writes a representation of the node to the output stream
+  /// @param sem the semantic info for the program
   /// @param out the stream to write to
   /// @param indent number of spaces to indent the node when writing
-  void to_str(std::ostream& out, size_t indent) const override;
+  void to_str(const semantic::Info& sem,
+              std::ostream& out,
+              size_t indent) const override;
 
  private:
   BinaryExpression(const BinaryExpression&) = delete;
diff --git a/src/ast/binary_expression_test.cc b/src/ast/binary_expression_test.cc
index 432bc45..5a8f7bf 100644
--- a/src/ast/binary_expression_test.cc
+++ b/src/ast/binary_expression_test.cc
@@ -106,7 +106,7 @@
 
   auto* r = create<BinaryExpression>(BinaryOp::kEqual, lhs, rhs);
   std::ostringstream out;
-  r->to_str(out, 2);
+  r->to_str(Sem(), out, 2);
   EXPECT_EQ(demangle(out.str()), R"(  Binary[not set]{
     Identifier[not set]{lhs}
     equal
diff --git a/src/ast/binding_decoration.cc b/src/ast/binding_decoration.cc
index 10a6bff..a8714dc 100644
--- a/src/ast/binding_decoration.cc
+++ b/src/ast/binding_decoration.cc
@@ -27,7 +27,9 @@
 
 BindingDecoration::~BindingDecoration() = default;
 
-void BindingDecoration::to_str(std::ostream& out, size_t indent) const {
+void BindingDecoration::to_str(const semantic::Info&,
+                               std::ostream& out,
+                               size_t indent) const {
   make_indent(out, indent);
   out << "BindingDecoration{" << value_ << "}" << std::endl;
 }
diff --git a/src/ast/binding_decoration.h b/src/ast/binding_decoration.h
index e4d73e3..6f9f2bb 100644
--- a/src/ast/binding_decoration.h
+++ b/src/ast/binding_decoration.h
@@ -36,9 +36,12 @@
   uint32_t value() const { return value_; }
 
   /// Outputs the decoration to the given stream
+  /// @param sem the semantic info for the program
   /// @param out the stream to write to
   /// @param indent number of spaces to indent the node when writing
-  void to_str(std::ostream& out, size_t indent) const override;
+  void to_str(const semantic::Info& sem,
+              std::ostream& out,
+              size_t indent) const override;
 
   /// Clones this node and all transitive child nodes using the `CloneContext`
   /// `ctx`.
diff --git a/src/ast/binding_decoration_test.cc b/src/ast/binding_decoration_test.cc
index ddeff33..a50a048 100644
--- a/src/ast/binding_decoration_test.cc
+++ b/src/ast/binding_decoration_test.cc
@@ -40,7 +40,7 @@
 TEST_F(BindingDecorationTest, ToStr) {
   auto* d = create<BindingDecoration>(2);
   std::ostringstream out;
-  d->to_str(out, 0);
+  d->to_str(Sem(), out, 0);
   EXPECT_EQ(out.str(), R"(BindingDecoration{2}
 )");
 }
diff --git a/src/ast/bitcast_expression.cc b/src/ast/bitcast_expression.cc
index 26e5230..df28119 100644
--- a/src/ast/bitcast_expression.cc
+++ b/src/ast/bitcast_expression.cc
@@ -41,11 +41,13 @@
   return type_ != nullptr;
 }
 
-void BitcastExpression::to_str(std::ostream& out, size_t indent) const {
+void BitcastExpression::to_str(const semantic::Info& sem,
+                               std::ostream& out,
+                               size_t indent) const {
   make_indent(out, indent);
-  out << "Bitcast[" << result_type_str() << "]<" << type_->type_name() << ">{"
-      << std::endl;
-  expr_->to_str(out, indent + 2);
+  out << "Bitcast[" << result_type_str(sem) << "]<" << type_->type_name()
+      << ">{" << std::endl;
+  expr_->to_str(sem, out, indent + 2);
   make_indent(out, indent);
   out << "}" << std::endl;
 }
diff --git a/src/ast/bitcast_expression.h b/src/ast/bitcast_expression.h
index d1376f2..734be10 100644
--- a/src/ast/bitcast_expression.h
+++ b/src/ast/bitcast_expression.h
@@ -54,9 +54,12 @@
   bool IsValid() const override;
 
   /// Writes a representation of the node to the output stream
+  /// @param sem the semantic info for the program
   /// @param out the stream to write to
   /// @param indent number of spaces to indent the node when writing
-  void to_str(std::ostream& out, size_t indent) const override;
+  void to_str(const semantic::Info& sem,
+              std::ostream& out,
+              size_t indent) const override;
 
  private:
   BitcastExpression(const BitcastExpression&) = delete;
diff --git a/src/ast/bitcast_expression_test.cc b/src/ast/bitcast_expression_test.cc
index 56e2568..2dce4b9 100644
--- a/src/ast/bitcast_expression_test.cc
+++ b/src/ast/bitcast_expression_test.cc
@@ -79,7 +79,7 @@
 
   auto* exp = create<BitcastExpression>(ty.f32(), expr);
   std::ostringstream out;
-  exp->to_str(out, 2);
+  exp->to_str(Sem(), out, 2);
 
   EXPECT_EQ(demangle(out.str()), R"(  Bitcast[not set]<__f32>{
     Identifier[not set]{expr}
diff --git a/src/ast/block_statement.cc b/src/ast/block_statement.cc
index 07ff30f..6e4b3f2 100644
--- a/src/ast/block_statement.cc
+++ b/src/ast/block_statement.cc
@@ -44,12 +44,14 @@
   return true;
 }
 
-void BlockStatement::to_str(std::ostream& out, size_t indent) const {
+void BlockStatement::to_str(const semantic::Info& sem,
+                            std::ostream& out,
+                            size_t indent) const {
   make_indent(out, indent);
   out << "Block{" << std::endl;
 
   for (auto* stmt : *this) {
-    stmt->to_str(out, indent + 2);
+    stmt->to_str(sem, out, indent + 2);
   }
 
   make_indent(out, indent);
diff --git a/src/ast/block_statement.h b/src/ast/block_statement.h
index 08e4b06..2af2cf6 100644
--- a/src/ast/block_statement.h
+++ b/src/ast/block_statement.h
@@ -76,9 +76,12 @@
   bool IsValid() const override;
 
   /// Writes a representation of the node to the output stream
+  /// @param sem the semantic info for the program
   /// @param out the stream to write to
   /// @param indent number of spaces to indent the node when writing
-  void to_str(std::ostream& out, size_t indent) const override;
+  void to_str(const semantic::Info& sem,
+              std::ostream& out,
+              size_t indent) const override;
 
  private:
   BlockStatement(const BlockStatement&) = delete;
diff --git a/src/ast/block_statement_test.cc b/src/ast/block_statement_test.cc
index 02c536e..5290be8 100644
--- a/src/ast/block_statement_test.cc
+++ b/src/ast/block_statement_test.cc
@@ -87,7 +87,7 @@
   });
 
   std::ostringstream out;
-  b->to_str(out, 2);
+  b->to_str(Sem(), out, 2);
   EXPECT_EQ(out.str(), R"(  Block{
     Discard{}
   }
diff --git a/src/ast/bool_literal.cc b/src/ast/bool_literal.cc
index e162d1c..9a9f180 100644
--- a/src/ast/bool_literal.cc
+++ b/src/ast/bool_literal.cc
@@ -27,7 +27,7 @@
 
 BoolLiteral::~BoolLiteral() = default;
 
-std::string BoolLiteral::to_str() const {
+std::string BoolLiteral::to_str(const semantic::Info&) const {
   return value_ ? "true" : "false";
 }
 
diff --git a/src/ast/bool_literal.h b/src/ast/bool_literal.h
index 2896913..05eb137 100644
--- a/src/ast/bool_literal.h
+++ b/src/ast/bool_literal.h
@@ -40,8 +40,9 @@
   /// @returns the name for this literal. This name is unique to this value.
   std::string name() const override;
 
+  /// @param sem the semantic info for the program
   /// @returns the literal as a string
-  std::string to_str() const override;
+  std::string to_str(const semantic::Info& sem) const override;
 
   /// Clones this node and all transitive child nodes using the `CloneContext`
   /// `ctx`.
diff --git a/src/ast/bool_literal_test.cc b/src/ast/bool_literal_test.cc
index e07e66e..01c6eac 100644
--- a/src/ast/bool_literal_test.cc
+++ b/src/ast/bool_literal_test.cc
@@ -59,8 +59,8 @@
   auto* t = create<BoolLiteral>(&bool_type, true);
   auto* f = create<BoolLiteral>(&bool_type, false);
 
-  EXPECT_EQ(t->to_str(), "true");
-  EXPECT_EQ(f->to_str(), "false");
+  EXPECT_EQ(t->to_str(Sem()), "true");
+  EXPECT_EQ(f->to_str(Sem()), "false");
 }
 
 }  // namespace
diff --git a/src/ast/break_statement.cc b/src/ast/break_statement.cc
index 6bf034c..15a8e1c 100644
--- a/src/ast/break_statement.cc
+++ b/src/ast/break_statement.cc
@@ -36,7 +36,9 @@
   return true;
 }
 
-void BreakStatement::to_str(std::ostream& out, size_t indent) const {
+void BreakStatement::to_str(const semantic::Info&,
+                            std::ostream& out,
+                            size_t indent) const {
   make_indent(out, indent);
   out << "Break{}" << std::endl;
 }
diff --git a/src/ast/break_statement.h b/src/ast/break_statement.h
index 7c9bd91..23f50e5 100644
--- a/src/ast/break_statement.h
+++ b/src/ast/break_statement.h
@@ -42,9 +42,12 @@
   bool IsValid() const override;
 
   /// Writes a representation of the node to the output stream
+  /// @param sem the semantic info for the program
   /// @param out the stream to write to
   /// @param indent number of spaces to indent the node when writing
-  void to_str(std::ostream& out, size_t indent) const override;
+  void to_str(const semantic::Info& sem,
+              std::ostream& out,
+              size_t indent) const override;
 
  private:
   BreakStatement(const BreakStatement&) = delete;
diff --git a/src/ast/break_statement_test.cc b/src/ast/break_statement_test.cc
index 10063a4..ffad482 100644
--- a/src/ast/break_statement_test.cc
+++ b/src/ast/break_statement_test.cc
@@ -42,7 +42,7 @@
 TEST_F(BreakStatementTest, ToStr) {
   auto* stmt = create<BreakStatement>();
   std::ostringstream out;
-  stmt->to_str(out, 2);
+  stmt->to_str(Sem(), out, 2);
   EXPECT_EQ(out.str(), R"(  Break{}
 )");
 }
diff --git a/src/ast/builtin_decoration.cc b/src/ast/builtin_decoration.cc
index 1c33150..e275752 100644
--- a/src/ast/builtin_decoration.cc
+++ b/src/ast/builtin_decoration.cc
@@ -27,7 +27,9 @@
 
 BuiltinDecoration::~BuiltinDecoration() = default;
 
-void BuiltinDecoration::to_str(std::ostream& out, size_t indent) const {
+void BuiltinDecoration::to_str(const semantic::Info&,
+                               std::ostream& out,
+                               size_t indent) const {
   make_indent(out, indent);
   out << "BuiltinDecoration{" << builtin_ << "}" << std::endl;
 }
diff --git a/src/ast/builtin_decoration.h b/src/ast/builtin_decoration.h
index 7c437d5..cbe700e 100644
--- a/src/ast/builtin_decoration.h
+++ b/src/ast/builtin_decoration.h
@@ -35,9 +35,12 @@
   Builtin value() const { return builtin_; }
 
   /// Outputs the decoration to the given stream
+  /// @param sem the semantic info for the program
   /// @param out the stream to write to
   /// @param indent number of spaces to indent the node when writing
-  void to_str(std::ostream& out, size_t indent) const override;
+  void to_str(const semantic::Info& sem,
+              std::ostream& out,
+              size_t indent) const override;
 
   /// Clones this node and all transitive child nodes using the `CloneContext`
   /// `ctx`.
diff --git a/src/ast/builtin_decoration_test.cc b/src/ast/builtin_decoration_test.cc
index df10ec7..b115425 100644
--- a/src/ast/builtin_decoration_test.cc
+++ b/src/ast/builtin_decoration_test.cc
@@ -40,7 +40,7 @@
 TEST_F(BuiltinDecorationTest, ToStr) {
   auto* d = create<BuiltinDecoration>(Builtin::kFragDepth);
   std::ostringstream out;
-  d->to_str(out, 0);
+  d->to_str(Sem(), out, 0);
   EXPECT_EQ(out.str(), R"(BuiltinDecoration{frag_depth}
 )");
 }
diff --git a/src/ast/call_expression.cc b/src/ast/call_expression.cc
index 674350a..a54f9a7 100644
--- a/src/ast/call_expression.cc
+++ b/src/ast/call_expression.cc
@@ -48,15 +48,17 @@
   return true;
 }
 
-void CallExpression::to_str(std::ostream& out, size_t indent) const {
+void CallExpression::to_str(const semantic::Info& sem,
+                            std::ostream& out,
+                            size_t indent) const {
   make_indent(out, indent);
-  out << "Call[" << result_type_str() << "]{" << std::endl;
-  func_->to_str(out, indent + 2);
+  out << "Call[" << result_type_str(sem) << "]{" << std::endl;
+  func_->to_str(sem, out, indent + 2);
 
   make_indent(out, indent + 2);
   out << "(" << std::endl;
   for (auto* param : params_)
-    param->to_str(out, indent + 4);
+    param->to_str(sem, out, indent + 4);
 
   make_indent(out, indent + 2);
   out << ")" << std::endl;
diff --git a/src/ast/call_expression.h b/src/ast/call_expression.h
index d52ab4e..d84f074 100644
--- a/src/ast/call_expression.h
+++ b/src/ast/call_expression.h
@@ -53,9 +53,12 @@
   bool IsValid() const override;
 
   /// Writes a representation of the node to the output stream
+  /// @param sem the semantic info for the program
   /// @param out the stream to write to
   /// @param indent number of spaces to indent the node when writing
-  void to_str(std::ostream& out, size_t indent) const override;
+  void to_str(const semantic::Info& sem,
+              std::ostream& out,
+              size_t indent) const override;
 
  private:
   CallExpression(const CallExpression&) = delete;
diff --git a/src/ast/call_expression_test.cc b/src/ast/call_expression_test.cc
index 8769a52..f8f3ee7 100644
--- a/src/ast/call_expression_test.cc
+++ b/src/ast/call_expression_test.cc
@@ -97,7 +97,7 @@
   auto* func = Expr("func");
   auto* stmt = create<CallExpression>(func, ExpressionList{});
   std::ostringstream out;
-  stmt->to_str(out, 2);
+  stmt->to_str(Sem(), out, 2);
   EXPECT_EQ(demangle(out.str()), R"(  Call[not set]{
     Identifier[not set]{func}
     (
@@ -114,7 +114,7 @@
 
   auto* stmt = create<CallExpression>(func, params);
   std::ostringstream out;
-  stmt->to_str(out, 2);
+  stmt->to_str(Sem(), out, 2);
   EXPECT_EQ(demangle(out.str()), R"(  Call[not set]{
     Identifier[not set]{func}
     (
diff --git a/src/ast/call_statement.cc b/src/ast/call_statement.cc
index 66b94e5..1e48538 100644
--- a/src/ast/call_statement.cc
+++ b/src/ast/call_statement.cc
@@ -39,8 +39,10 @@
   return call_ != nullptr && call_->IsValid();
 }
 
-void CallStatement::to_str(std::ostream& out, size_t indent) const {
-  call_->to_str(out, indent);
+void CallStatement::to_str(const semantic::Info& sem,
+                           std::ostream& out,
+                           size_t indent) const {
+  call_->to_str(sem, out, indent);
 }
 
 }  // namespace ast
diff --git a/src/ast/call_statement.h b/src/ast/call_statement.h
index c8aa837..dfbb06b 100644
--- a/src/ast/call_statement.h
+++ b/src/ast/call_statement.h
@@ -50,9 +50,12 @@
   bool IsValid() const override;
 
   /// Writes a representation of the node to the output stream
+  /// @param sem the semantic info for the program
   /// @param out the stream to write to
   /// @param indent number of spaces to indent the node when writing
-  void to_str(std::ostream& out, size_t indent) const override;
+  void to_str(const semantic::Info& sem,
+              std::ostream& out,
+              size_t indent) const override;
 
  private:
   CallStatement(const CallStatement&) = delete;
diff --git a/src/ast/call_statement_test.cc b/src/ast/call_statement_test.cc
index a9545e2..844d1cb 100644
--- a/src/ast/call_statement_test.cc
+++ b/src/ast/call_statement_test.cc
@@ -58,7 +58,7 @@
       create<CallExpression>(Expr("func"), ExpressionList{}));
 
   std::ostringstream out;
-  c->to_str(out, 2);
+  c->to_str(Sem(), out, 2);
   EXPECT_EQ(demangle(out.str()), R"(  Call[not set]{
     Identifier[not set]{func}
     (
diff --git a/src/ast/case_statement.cc b/src/ast/case_statement.cc
index ac1a267..5f5934d 100644
--- a/src/ast/case_statement.cc
+++ b/src/ast/case_statement.cc
@@ -40,7 +40,9 @@
   return body_ != nullptr && body_->IsValid();
 }
 
-void CaseStatement::to_str(std::ostream& out, size_t indent) const {
+void CaseStatement::to_str(const semantic::Info& sem,
+                           std::ostream& out,
+                           size_t indent) const {
   make_indent(out, indent);
 
   if (IsDefault()) {
@@ -53,14 +55,14 @@
         out << ", ";
 
       first = false;
-      out << selector->to_str();
+      out << selector->to_str(sem);
     }
     out << "{" << std::endl;
   }
 
   if (body_ != nullptr) {
     for (auto* stmt : *body_) {
-      stmt->to_str(out, indent + 2);
+      stmt->to_str(sem, out, indent + 2);
     }
   }
 
diff --git a/src/ast/case_statement.h b/src/ast/case_statement.h
index b793342..5b5d664 100644
--- a/src/ast/case_statement.h
+++ b/src/ast/case_statement.h
@@ -66,9 +66,12 @@
   bool IsValid() const override;
 
   /// Writes a representation of the node to the output stream
+  /// @param sem the semantic info for the program
   /// @param out the stream to write to
   /// @param indent number of spaces to indent the node when writing
-  void to_str(std::ostream& out, size_t indent) const override;
+  void to_str(const semantic::Info& sem,
+              std::ostream& out,
+              size_t indent) const override;
 
  private:
   CaseStatement(const CaseStatement&) = delete;
diff --git a/src/ast/case_statement_test.cc b/src/ast/case_statement_test.cc
index ad54986..2a9779b 100644
--- a/src/ast/case_statement_test.cc
+++ b/src/ast/case_statement_test.cc
@@ -135,7 +135,7 @@
   auto* c = create<CaseStatement>(CaseSelectorList{b}, body);
 
   std::ostringstream out;
-  c->to_str(out, 2);
+  c->to_str(Sem(), out, 2);
   EXPECT_EQ(out.str(), R"(  Case -2{
     Discard{}
   }
@@ -152,7 +152,7 @@
   auto* c = create<CaseStatement>(CaseSelectorList{b}, body);
 
   std::ostringstream out;
-  c->to_str(out, 2);
+  c->to_str(Sem(), out, 2);
   EXPECT_EQ(out.str(), R"(  Case 2{
     Discard{}
   }
@@ -170,7 +170,7 @@
   auto* c = create<CaseStatement>(b, body);
 
   std::ostringstream out;
-  c->to_str(out, 2);
+  c->to_str(Sem(), out, 2);
   EXPECT_EQ(out.str(), R"(  Case 1, 2{
     Discard{}
   }
@@ -184,7 +184,7 @@
   auto* c = create<CaseStatement>(CaseSelectorList{}, body);
 
   std::ostringstream out;
-  c->to_str(out, 2);
+  c->to_str(Sem(), out, 2);
   EXPECT_EQ(out.str(), R"(  Default{
     Discard{}
   }
diff --git a/src/ast/constant_id_decoration.cc b/src/ast/constant_id_decoration.cc
index 463e5b3..fec8fe8 100644
--- a/src/ast/constant_id_decoration.cc
+++ b/src/ast/constant_id_decoration.cc
@@ -27,7 +27,9 @@
 
 ConstantIdDecoration::~ConstantIdDecoration() = default;
 
-void ConstantIdDecoration::to_str(std::ostream& out, size_t indent) const {
+void ConstantIdDecoration::to_str(const semantic::Info&,
+                                  std::ostream& out,
+                                  size_t indent) const {
   make_indent(out, indent);
   out << "ConstantIdDecoration{" << value_ << "}" << std::endl;
 }
diff --git a/src/ast/constant_id_decoration.h b/src/ast/constant_id_decoration.h
index 0d2b407..11acca6 100644
--- a/src/ast/constant_id_decoration.h
+++ b/src/ast/constant_id_decoration.h
@@ -35,9 +35,12 @@
   uint32_t value() const { return value_; }
 
   /// Outputs the decoration to the given stream
+  /// @param sem the semantic info for the program
   /// @param out the stream to write to
   /// @param indent number of spaces to indent the node when writing
-  void to_str(std::ostream& out, size_t indent) const override;
+  void to_str(const semantic::Info& sem,
+              std::ostream& out,
+              size_t indent) const override;
 
   /// Clones this node and all transitive child nodes using the `CloneContext`
   /// `ctx`.
diff --git a/src/ast/constant_id_decoration_test.cc b/src/ast/constant_id_decoration_test.cc
index ea38af3..b536ee6 100644
--- a/src/ast/constant_id_decoration_test.cc
+++ b/src/ast/constant_id_decoration_test.cc
@@ -39,7 +39,7 @@
 TEST_F(ConstantIdDecorationTest, ToStr) {
   auto* d = create<ConstantIdDecoration>(1200);
   std::ostringstream out;
-  d->to_str(out, 0);
+  d->to_str(Sem(), out, 0);
   EXPECT_EQ(out.str(), R"(ConstantIdDecoration{1200}
 )");
 }
diff --git a/src/ast/continue_statement.cc b/src/ast/continue_statement.cc
index 58edfba..3b066a4 100644
--- a/src/ast/continue_statement.cc
+++ b/src/ast/continue_statement.cc
@@ -36,7 +36,9 @@
   return true;
 }
 
-void ContinueStatement::to_str(std::ostream& out, size_t indent) const {
+void ContinueStatement::to_str(const semantic::Info&,
+                               std::ostream& out,
+                               size_t indent) const {
   make_indent(out, indent);
   out << "Continue{}" << std::endl;
 }
diff --git a/src/ast/continue_statement.h b/src/ast/continue_statement.h
index 1d4de24..ab2e870 100644
--- a/src/ast/continue_statement.h
+++ b/src/ast/continue_statement.h
@@ -45,9 +45,12 @@
   bool IsValid() const override;
 
   /// Writes a representation of the node to the output stream
+  /// @param sem the semantic info for the program
   /// @param out the stream to write to
   /// @param indent number of spaces to indent the node when writing
-  void to_str(std::ostream& out, size_t indent) const override;
+  void to_str(const semantic::Info& sem,
+              std::ostream& out,
+              size_t indent) const override;
 
  private:
   ContinueStatement(const ContinueStatement&) = delete;
diff --git a/src/ast/continue_statement_test.cc b/src/ast/continue_statement_test.cc
index ab725b1..31c859a 100644
--- a/src/ast/continue_statement_test.cc
+++ b/src/ast/continue_statement_test.cc
@@ -42,7 +42,7 @@
 TEST_F(ContinueStatementTest, ToStr) {
   auto* stmt = create<ContinueStatement>();
   std::ostringstream out;
-  stmt->to_str(out, 2);
+  stmt->to_str(Sem(), out, 2);
   EXPECT_EQ(out.str(), R"(  Continue{}
 )");
 }
diff --git a/src/ast/discard_statement.cc b/src/ast/discard_statement.cc
index 3d4ae66..ec2bf65 100644
--- a/src/ast/discard_statement.cc
+++ b/src/ast/discard_statement.cc
@@ -36,7 +36,9 @@
   return true;
 }
 
-void DiscardStatement::to_str(std::ostream& out, size_t indent) const {
+void DiscardStatement::to_str(const semantic::Info&,
+                              std::ostream& out,
+                              size_t indent) const {
   make_indent(out, indent);
   out << "Discard{}" << std::endl;
 }
diff --git a/src/ast/discard_statement.h b/src/ast/discard_statement.h
index 8f17fd7..bb6e95a 100644
--- a/src/ast/discard_statement.h
+++ b/src/ast/discard_statement.h
@@ -42,9 +42,12 @@
   bool IsValid() const override;
 
   /// Writes a representation of the node to the output stream
+  /// @param sem the semantic info for the program
   /// @param out the stream to write to
   /// @param indent number of spaces to indent the node when writing
-  void to_str(std::ostream& out, size_t indent) const override;
+  void to_str(const semantic::Info& sem,
+              std::ostream& out,
+              size_t indent) const override;
 
  private:
   DiscardStatement(const DiscardStatement&) = delete;
diff --git a/src/ast/discard_statement_test.cc b/src/ast/discard_statement_test.cc
index 545d732..43e0771 100644
--- a/src/ast/discard_statement_test.cc
+++ b/src/ast/discard_statement_test.cc
@@ -54,7 +54,7 @@
 TEST_F(DiscardStatementTest, ToStr) {
   auto* stmt = create<DiscardStatement>();
   std::ostringstream out;
-  stmt->to_str(out, 2);
+  stmt->to_str(Sem(), out, 2);
   EXPECT_EQ(out.str(), R"(  Discard{}
 )");
 }
diff --git a/src/ast/else_statement.cc b/src/ast/else_statement.cc
index 63201a3..30e18a6 100644
--- a/src/ast/else_statement.cc
+++ b/src/ast/else_statement.cc
@@ -43,14 +43,16 @@
   return condition_ == nullptr || condition_->IsValid();
 }
 
-void ElseStatement::to_str(std::ostream& out, size_t indent) const {
+void ElseStatement::to_str(const semantic::Info& sem,
+                           std::ostream& out,
+                           size_t indent) const {
   make_indent(out, indent);
   out << "Else{" << std::endl;
   if (condition_ != nullptr) {
     make_indent(out, indent + 2);
     out << "(" << std::endl;
 
-    condition_->to_str(out, indent + 4);
+    condition_->to_str(sem, out, indent + 4);
 
     make_indent(out, indent + 2);
     out << ")" << std::endl;
@@ -61,7 +63,7 @@
 
   if (body_ != nullptr) {
     for (auto* stmt : *body_) {
-      stmt->to_str(out, indent + 4);
+      stmt->to_str(sem, out, indent + 4);
     }
   }
 
diff --git a/src/ast/else_statement.h b/src/ast/else_statement.h
index 7b08fa5..a432c49 100644
--- a/src/ast/else_statement.h
+++ b/src/ast/else_statement.h
@@ -62,9 +62,12 @@
   bool IsValid() const override;
 
   /// Writes a representation of the node to the output stream
+  /// @param sem the semantic info for the program
   /// @param out the stream to write to
   /// @param indent number of spaces to indent the node when writing
-  void to_str(std::ostream& out, size_t indent) const override;
+  void to_str(const semantic::Info& sem,
+              std::ostream& out,
+              size_t indent) const override;
 
  private:
   ElseStatement(const ElseStatement&) = delete;
diff --git a/src/ast/else_statement_test.cc b/src/ast/else_statement_test.cc
index 0855fec..f37fe56 100644
--- a/src/ast/else_statement_test.cc
+++ b/src/ast/else_statement_test.cc
@@ -115,7 +115,7 @@
   });
   auto* e = create<ElseStatement>(cond, body);
   std::ostringstream out;
-  e->to_str(out, 2);
+  e->to_str(Sem(), out, 2);
   EXPECT_EQ(out.str(), R"(  Else{
     (
       ScalarConstructor[not set]{true}
@@ -133,7 +133,7 @@
   });
   auto* e = create<ElseStatement>(nullptr, body);
   std::ostringstream out;
-  e->to_str(out, 2);
+  e->to_str(Sem(), out, 2);
   EXPECT_EQ(out.str(), R"(  Else{
     {
       Discard{}
diff --git a/src/ast/expression.h b/src/ast/expression.h
index 853a57c..f46d461 100644
--- a/src/ast/expression.h
+++ b/src/ast/expression.h
@@ -38,7 +38,7 @@
 
   /// @returns a string representation of the result type or 'not set' if no
   /// result type present
-  std::string result_type_str() const {
+  std::string result_type_str(const semantic::Info&) const {
     return result_type_ ? result_type_->type_name() : "not set";
   }
 
diff --git a/src/ast/expression_test.cc b/src/ast/expression_test.cc
deleted file mode 100644
index 9dde377..0000000
--- a/src/ast/expression_test.cc
+++ /dev/null
@@ -1,55 +0,0 @@
-// Copyright 2020 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/ast/expression.h"
-
-#include "src/ast/test_helper.h"
-#include "src/type/alias_type.h"
-#include "src/type/i32_type.h"
-
-namespace tint {
-namespace ast {
-namespace {
-
-class FakeExpr : public Expression {
- public:
-  FakeExpr() : Expression(Source{}) {}
-
-  FakeExpr* Clone(CloneContext*) const override { return nullptr; }
-  bool IsValid() const override { return true; }
-  void to_str(std::ostream&, size_t) const override {}
-};
-
-using ExpressionTest = TestHelper;
-
-TEST_F(ExpressionTest, set_result_type) {
-  FakeExpr e;
-  e.set_result_type(ty.i32());
-  ASSERT_NE(e.result_type(), nullptr);
-  EXPECT_TRUE(e.result_type()->Is<type::I32>());
-}
-
-TEST_F(ExpressionTest, set_result_type_alias) {
-  auto* a = ty.alias("a", ty.i32());
-  auto* b = ty.alias("b", a);
-
-  FakeExpr e;
-  e.set_result_type(b);
-  ASSERT_NE(e.result_type(), nullptr);
-  EXPECT_TRUE(e.result_type()->Is<type::I32>());
-}
-
-}  // namespace
-}  // namespace ast
-}  // namespace tint
diff --git a/src/ast/fallthrough_statement.cc b/src/ast/fallthrough_statement.cc
index ede2ca6..dcdc26a 100644
--- a/src/ast/fallthrough_statement.cc
+++ b/src/ast/fallthrough_statement.cc
@@ -37,7 +37,9 @@
   return true;
 }
 
-void FallthroughStatement::to_str(std::ostream& out, size_t indent) const {
+void FallthroughStatement::to_str(const semantic::Info&,
+                                  std::ostream& out,
+                                  size_t indent) const {
   make_indent(out, indent);
   out << "Fallthrough{}" << std::endl;
 }
diff --git a/src/ast/fallthrough_statement.h b/src/ast/fallthrough_statement.h
index 25d5870..2db9546 100644
--- a/src/ast/fallthrough_statement.h
+++ b/src/ast/fallthrough_statement.h
@@ -42,9 +42,12 @@
   bool IsValid() const override;
 
   /// Writes a representation of the node to the output stream
+  /// @param sem the semantic info for the program
   /// @param out the stream to write to
   /// @param indent number of spaces to indent the node when writing
-  void to_str(std::ostream& out, size_t indent) const override;
+  void to_str(const semantic::Info& sem,
+              std::ostream& out,
+              size_t indent) const override;
 
  private:
   FallthroughStatement(const FallthroughStatement&) = delete;
diff --git a/src/ast/fallthrough_statement_test.cc b/src/ast/fallthrough_statement_test.cc
index a17131a..4021bba 100644
--- a/src/ast/fallthrough_statement_test.cc
+++ b/src/ast/fallthrough_statement_test.cc
@@ -50,7 +50,7 @@
 TEST_F(FallthroughStatementTest, ToStr) {
   auto* stmt = create<FallthroughStatement>();
   std::ostringstream out;
-  stmt->to_str(out, 2);
+  stmt->to_str(Sem(), out, 2);
   EXPECT_EQ(out.str(), R"(  Fallthrough{}
 )");
 }
diff --git a/src/ast/float_literal.cc b/src/ast/float_literal.cc
index 6e48199..f7b54f4 100644
--- a/src/ast/float_literal.cc
+++ b/src/ast/float_literal.cc
@@ -30,7 +30,7 @@
 
 FloatLiteral::~FloatLiteral() = default;
 
-std::string FloatLiteral::to_str() const {
+std::string FloatLiteral::to_str(const semantic::Info&) const {
   return std::to_string(value_);
 }
 
diff --git a/src/ast/float_literal.h b/src/ast/float_literal.h
index bbf58f8..b3deb9c 100644
--- a/src/ast/float_literal.h
+++ b/src/ast/float_literal.h
@@ -38,8 +38,9 @@
   /// @returns the name for this literal. This name is unique to this value.
   std::string name() const override;
 
+  /// @param sem the semantic info for the program
   /// @returns the literal as a string
-  std::string to_str() const override;
+  std::string to_str(const semantic::Info& sem) const override;
 
   /// Clones this node and all transitive child nodes using the `CloneContext`
   /// `ctx`.
diff --git a/src/ast/float_literal_test.cc b/src/ast/float_literal_test.cc
index fd4dfe7..b329035 100644
--- a/src/ast/float_literal_test.cc
+++ b/src/ast/float_literal_test.cc
@@ -46,7 +46,7 @@
 TEST_F(FloatLiteralTest, ToStr) {
   auto* f = create<FloatLiteral>(ty.f32(), 42.1f);
 
-  EXPECT_EQ(f->to_str(), "42.099998");
+  EXPECT_EQ(f->to_str(Sem()), "42.099998");
 }
 
 TEST_F(FloatLiteralTest, ToName) {
diff --git a/src/ast/function.cc b/src/ast/function.cc
index ceb2d3d..f17421a 100644
--- a/src/ast/function.cc
+++ b/src/ast/function.cc
@@ -247,13 +247,15 @@
   return true;
 }
 
-void Function::to_str(std::ostream& out, size_t indent) const {
+void Function::to_str(const semantic::Info& sem,
+                      std::ostream& out,
+                      size_t indent) const {
   make_indent(out, indent);
   out << "Function " << symbol_.to_str() << " -> " << return_type_->type_name()
       << std::endl;
 
   for (auto* deco : decorations()) {
-    deco->to_str(out, indent);
+    deco->to_str(sem, out, indent);
   }
 
   make_indent(out, indent);
@@ -263,7 +265,7 @@
     out << std::endl;
 
     for (auto* param : params_)
-      param->to_str(out, indent + 2);
+      param->to_str(sem, out, indent + 2);
 
     make_indent(out, indent);
   }
@@ -274,7 +276,7 @@
 
   if (body_ != nullptr) {
     for (auto* stmt : *body_) {
-      stmt->to_str(out, indent + 2);
+      stmt->to_str(sem, out, indent + 2);
     }
   }
 
diff --git a/src/ast/function.h b/src/ast/function.h
index 0f635c1..ff72b14 100644
--- a/src/ast/function.h
+++ b/src/ast/function.h
@@ -184,9 +184,12 @@
   bool IsValid() const override;
 
   /// Writes a representation of the node to the output stream
+  /// @param sem the semantic info for the program
   /// @param out the stream to write to
   /// @param indent number of spaces to indent the node when writing
-  void to_str(std::ostream& out, size_t indent) const override;
+  void to_str(const semantic::Info& sem,
+              std::ostream& out,
+              size_t indent) const override;
 
   /// @returns the type name for this function
   std::string type_name() const;
diff --git a/src/ast/function_test.cc b/src/ast/function_test.cc
index 1181efa..39b6c62 100644
--- a/src/ast/function_test.cc
+++ b/src/ast/function_test.cc
@@ -245,7 +245,7 @@
                  FunctionDecorationList{});
 
   std::ostringstream out;
-  f->to_str(out, 2);
+  f->to_str(Sem(), out, 2);
   EXPECT_EQ(demangle(out.str()), R"(  Function func -> __void
   ()
   {
@@ -262,7 +262,7 @@
                  FunctionDecorationList{create<WorkgroupDecoration>(2, 4, 6)});
 
   std::ostringstream out;
-  f->to_str(out, 2);
+  f->to_str(Sem(), out, 2);
   EXPECT_EQ(demangle(out.str()), R"(  Function func -> __void
   WorkgroupDecoration{2 4 6}
   ()
@@ -283,7 +283,7 @@
                  FunctionDecorationList{});
 
   std::ostringstream out;
-  f->to_str(out, 2);
+  f->to_str(Sem(), out, 2);
   EXPECT_EQ(demangle(out.str()), R"(  Function func -> __void
   (
     Variable{
diff --git a/src/ast/group_decoration.cc b/src/ast/group_decoration.cc
index 019cc36..5c8171d 100644
--- a/src/ast/group_decoration.cc
+++ b/src/ast/group_decoration.cc
@@ -27,7 +27,9 @@
 
 GroupDecoration::~GroupDecoration() = default;
 
-void GroupDecoration::to_str(std::ostream& out, size_t indent) const {
+void GroupDecoration::to_str(const semantic::Info&,
+                             std::ostream& out,
+                             size_t indent) const {
   make_indent(out, indent);
   out << "GroupDecoration{" << value_ << "}" << std::endl;
 }
diff --git a/src/ast/group_decoration.h b/src/ast/group_decoration.h
index 323012d..1ff24be 100644
--- a/src/ast/group_decoration.h
+++ b/src/ast/group_decoration.h
@@ -35,9 +35,12 @@
   uint32_t value() const { return value_; }
 
   /// Outputs the decoration to the given stream
+  /// @param sem the semantic info for the program
   /// @param out the stream to write to
   /// @param indent number of spaces to indent the node when writing
-  void to_str(std::ostream& out, size_t indent) const override;
+  void to_str(const semantic::Info& sem,
+              std::ostream& out,
+              size_t indent) const override;
 
   /// Clones this node and all transitive child nodes using the `CloneContext`
   /// `ctx`.
diff --git a/src/ast/group_decoration_test.cc b/src/ast/group_decoration_test.cc
index 95625d3..5052c85 100644
--- a/src/ast/group_decoration_test.cc
+++ b/src/ast/group_decoration_test.cc
@@ -40,7 +40,7 @@
 TEST_F(GroupDecorationTest, ToStr) {
   auto* d = create<GroupDecoration>(2);
   std::ostringstream out;
-  d->to_str(out, 0);
+  d->to_str(Sem(), out, 0);
   EXPECT_EQ(out.str(), R"(GroupDecoration{2}
 )");
 }
diff --git a/src/ast/identifier_expression.cc b/src/ast/identifier_expression.cc
index c6c0779..ba05e64 100644
--- a/src/ast/identifier_expression.cc
+++ b/src/ast/identifier_expression.cc
@@ -38,9 +38,11 @@
   return sym_.IsValid();
 }
 
-void IdentifierExpression::to_str(std::ostream& out, size_t indent) const {
+void IdentifierExpression::to_str(const semantic::Info& sem,
+                                  std::ostream& out,
+                                  size_t indent) const {
   make_indent(out, indent);
-  out << "Identifier[" << result_type_str() << "]{" << sym_.to_str() << "}"
+  out << "Identifier[" << result_type_str(sem) << "]{" << sym_.to_str() << "}"
       << std::endl;
 }
 
diff --git a/src/ast/identifier_expression.h b/src/ast/identifier_expression.h
index d1a2b77..1021bdd 100644
--- a/src/ast/identifier_expression.h
+++ b/src/ast/identifier_expression.h
@@ -76,9 +76,12 @@
   bool IsValid() const override;
 
   /// Writes a representation of the node to the output stream
+  /// @param sem the semantic info for the program
   /// @param out the stream to write to
   /// @param indent number of spaces to indent the node when writing
-  void to_str(std::ostream& out, size_t indent) const override;
+  void to_str(const semantic::Info& sem,
+              std::ostream& out,
+              size_t indent) const override;
 
  private:
   IdentifierExpression(const IdentifierExpression&) = delete;
diff --git a/src/ast/identifier_expression_test.cc b/src/ast/identifier_expression_test.cc
index 1059af2..3d504ca 100644
--- a/src/ast/identifier_expression_test.cc
+++ b/src/ast/identifier_expression_test.cc
@@ -49,7 +49,7 @@
 TEST_F(IdentifierExpressionTest, ToStr) {
   auto* i = Expr("ident");
   std::ostringstream out;
-  i->to_str(out, 2);
+  i->to_str(Sem(), out, 2);
   EXPECT_EQ(demangle(out.str()), R"(  Identifier[not set]{ident}
 )");
 }
diff --git a/src/ast/if_statement.cc b/src/ast/if_statement.cc
index 16a677d..0b018ef 100644
--- a/src/ast/if_statement.cc
+++ b/src/ast/if_statement.cc
@@ -67,7 +67,9 @@
   return true;
 }
 
-void IfStatement::to_str(std::ostream& out, size_t indent) const {
+void IfStatement::to_str(const semantic::Info& sem,
+                         std::ostream& out,
+                         size_t indent) const {
   make_indent(out, indent);
   out << "If{" << std::endl;
 
@@ -75,7 +77,7 @@
   make_indent(out, indent + 2);
   out << "(" << std::endl;
 
-  condition_->to_str(out, indent + 4);
+  condition_->to_str(sem, out, indent + 4);
 
   // Close if conditional
   make_indent(out, indent + 2);
@@ -87,7 +89,7 @@
 
   if (body_ != nullptr) {
     for (auto* stmt : *body_) {
-      stmt->to_str(out, indent + 4);
+      stmt->to_str(sem, out, indent + 4);
     }
   }
 
@@ -100,7 +102,7 @@
   out << "}" << std::endl;
 
   for (auto* e : else_statements_) {
-    e->to_str(out, indent);
+    e->to_str(sem, out, indent);
   }
 }
 
diff --git a/src/ast/if_statement.h b/src/ast/if_statement.h
index 11515ab..10ee4a1 100644
--- a/src/ast/if_statement.h
+++ b/src/ast/if_statement.h
@@ -67,9 +67,12 @@
   bool IsValid() const override;
 
   /// Writes a representation of the node to the output stream
+  /// @param sem the semantic info for the program
   /// @param out the stream to write to
   /// @param indent number of spaces to indent the node when writing
-  void to_str(std::ostream& out, size_t indent) const override;
+  void to_str(const semantic::Info& sem,
+              std::ostream& out,
+              size_t indent) const override;
 
  private:
   IfStatement(const IfStatement&) = delete;
diff --git a/src/ast/if_statement_test.cc b/src/ast/if_statement_test.cc
index 6e4bf62..d071f7f 100644
--- a/src/ast/if_statement_test.cc
+++ b/src/ast/if_statement_test.cc
@@ -168,7 +168,7 @@
   auto* stmt = create<IfStatement>(cond, body, ElseStatementList{});
 
   std::ostringstream out;
-  stmt->to_str(out, 2);
+  stmt->to_str(Sem(), out, 2);
   EXPECT_EQ(demangle(out.str()), R"(  If{
     (
       Identifier[not set]{cond}
@@ -196,7 +196,7 @@
       });
 
   std::ostringstream out;
-  stmt->to_str(out, 2);
+  stmt->to_str(Sem(), out, 2);
   EXPECT_EQ(demangle(out.str()), R"(  If{
     (
       Identifier[not set]{cond}
diff --git a/src/ast/literal.cc b/src/ast/literal.cc
index 05375ad..4c365da 100644
--- a/src/ast/literal.cc
+++ b/src/ast/literal.cc
@@ -28,9 +28,11 @@
   return true;
 }
 
-void Literal::to_str(std::ostream& out, size_t indent) const {
+void Literal::to_str(const semantic::Info& sem,
+                     std::ostream& out,
+                     size_t indent) const {
   make_indent(out, indent);
-  out << to_str();
+  out << to_str(sem);
 }
 
 }  // namespace ast
diff --git a/src/ast/literal.h b/src/ast/literal.h
index 2b08eea..96779d7 100644
--- a/src/ast/literal.h
+++ b/src/ast/literal.h
@@ -35,12 +35,16 @@
   bool IsValid() const override;
 
   /// Writes a representation of the node to the output stream
+  /// @param sem the semantic info for the program
   /// @param out the stream to write to
   /// @param indent number of spaces to indent the node when writing
-  void to_str(std::ostream& out, size_t indent) const override;
+  void to_str(const semantic::Info& sem,
+              std::ostream& out,
+              size_t indent) const override;
 
+  /// @param sem the semantic info for the program
   /// @returns the literal as a string
-  virtual std::string to_str() const = 0;
+  virtual std::string to_str(const semantic::Info& sem) const = 0;
 
   /// @returns the name for this literal. This name is unique to this value.
   virtual std::string name() const = 0;
diff --git a/src/ast/location_decoration.cc b/src/ast/location_decoration.cc
index 1eff8a6..8e25036 100644
--- a/src/ast/location_decoration.cc
+++ b/src/ast/location_decoration.cc
@@ -27,7 +27,9 @@
 
 LocationDecoration::~LocationDecoration() = default;
 
-void LocationDecoration::to_str(std::ostream& out, size_t indent) const {
+void LocationDecoration::to_str(const semantic::Info&,
+                                std::ostream& out,
+                                size_t indent) const {
   make_indent(out, indent);
   out << "LocationDecoration{" << value_ << "}" << std::endl;
 }
diff --git a/src/ast/location_decoration.h b/src/ast/location_decoration.h
index fc24963..3d28c69 100644
--- a/src/ast/location_decoration.h
+++ b/src/ast/location_decoration.h
@@ -36,9 +36,12 @@
   uint32_t value() const { return value_; }
 
   /// Outputs the decoration to the given stream
+  /// @param sem the semantic info for the program
   /// @param out the stream to write to
   /// @param indent number of spaces to indent the node when writing
-  void to_str(std::ostream& out, size_t indent) const override;
+  void to_str(const semantic::Info& sem,
+              std::ostream& out,
+              size_t indent) const override;
 
   /// Clones this node and all transitive child nodes using the `CloneContext`
   /// `ctx`.
diff --git a/src/ast/location_decoration_test.cc b/src/ast/location_decoration_test.cc
index b8084e3..349e730 100644
--- a/src/ast/location_decoration_test.cc
+++ b/src/ast/location_decoration_test.cc
@@ -42,7 +42,7 @@
 TEST_F(LocationDecorationTest, ToStr) {
   auto* d = create<LocationDecoration>(2);
   std::ostringstream out;
-  d->to_str(out, 0);
+  d->to_str(Sem(), out, 0);
   EXPECT_EQ(out.str(), R"(LocationDecoration{2}
 )");
 }
diff --git a/src/ast/loop_statement.cc b/src/ast/loop_statement.cc
index 68730c4..74cb698 100644
--- a/src/ast/loop_statement.cc
+++ b/src/ast/loop_statement.cc
@@ -46,13 +46,15 @@
   return true;
 }
 
-void LoopStatement::to_str(std::ostream& out, size_t indent) const {
+void LoopStatement::to_str(const semantic::Info& sem,
+                           std::ostream& out,
+                           size_t indent) const {
   make_indent(out, indent);
   out << "Loop{" << std::endl;
 
   if (body_ != nullptr) {
     for (auto* stmt : *body_) {
-      stmt->to_str(out, indent + 2);
+      stmt->to_str(sem, out, indent + 2);
     }
   }
 
@@ -61,7 +63,7 @@
     out << "continuing {" << std::endl;
 
     for (auto* stmt : *continuing_) {
-      stmt->to_str(out, indent + 4);
+      stmt->to_str(sem, out, indent + 4);
     }
 
     make_indent(out, indent + 2);
diff --git a/src/ast/loop_statement.h b/src/ast/loop_statement.h
index e80eb94..b4f7f74 100644
--- a/src/ast/loop_statement.h
+++ b/src/ast/loop_statement.h
@@ -64,9 +64,12 @@
   bool IsValid() const override;
 
   /// Writes a representation of the node to the output stream
+  /// @param sem the semantic info for the program
   /// @param out the stream to write to
   /// @param indent number of spaces to indent the node when writing
-  void to_str(std::ostream& out, size_t indent) const override;
+  void to_str(const semantic::Info& sem,
+              std::ostream& out,
+              size_t indent) const override;
 
  private:
   LoopStatement(const LoopStatement&) = delete;
diff --git a/src/ast/loop_statement_test.cc b/src/ast/loop_statement_test.cc
index d3dca64..0f0b4f4 100644
--- a/src/ast/loop_statement_test.cc
+++ b/src/ast/loop_statement_test.cc
@@ -171,7 +171,7 @@
 
   auto* l = create<LoopStatement>(body, nullptr);
   std::ostringstream out;
-  l->to_str(out, 2);
+  l->to_str(Sem(), out, 2);
   EXPECT_EQ(out.str(), R"(  Loop{
     Discard{}
   }
@@ -187,7 +187,7 @@
 
   auto* l = create<LoopStatement>(body, continuing);
   std::ostringstream out;
-  l->to_str(out, 2);
+  l->to_str(Sem(), out, 2);
   EXPECT_EQ(out.str(), R"(  Loop{
     Discard{}
     continuing {
diff --git a/src/ast/member_accessor_expression.cc b/src/ast/member_accessor_expression.cc
index 20e6ee6..e58ad98 100644
--- a/src/ast/member_accessor_expression.cc
+++ b/src/ast/member_accessor_expression.cc
@@ -48,11 +48,13 @@
   return true;
 }
 
-void MemberAccessorExpression::to_str(std::ostream& out, size_t indent) const {
+void MemberAccessorExpression::to_str(const semantic::Info& sem,
+                                      std::ostream& out,
+                                      size_t indent) const {
   make_indent(out, indent);
-  out << "MemberAccessor[" << result_type_str() << "]{" << std::endl;
-  struct_->to_str(out, indent + 2);
-  member_->to_str(out, indent + 2);
+  out << "MemberAccessor[" << result_type_str(sem) << "]{" << std::endl;
+  struct_->to_str(sem, out, indent + 2);
+  member_->to_str(sem, out, indent + 2);
   make_indent(out, indent);
   out << "}" << std::endl;
 }
diff --git a/src/ast/member_accessor_expression.h b/src/ast/member_accessor_expression.h
index d9aef79..3e6fd9b 100644
--- a/src/ast/member_accessor_expression.h
+++ b/src/ast/member_accessor_expression.h
@@ -57,9 +57,12 @@
   bool IsValid() const override;
 
   /// Writes a representation of the node to the output stream
+  /// @param sem the semantic info for the program
   /// @param out the stream to write to
   /// @param indent number of spaces to indent the node when writing
-  void to_str(std::ostream& out, size_t indent) const override;
+  void to_str(const semantic::Info& sem,
+              std::ostream& out,
+              size_t indent) const override;
 
  private:
   MemberAccessorExpression(const MemberAccessorExpression&) = delete;
diff --git a/src/ast/member_accessor_expression_test.cc b/src/ast/member_accessor_expression_test.cc
index c7d1dd5..cc17106 100644
--- a/src/ast/member_accessor_expression_test.cc
+++ b/src/ast/member_accessor_expression_test.cc
@@ -78,7 +78,7 @@
   auto* stmt =
       create<MemberAccessorExpression>(Expr("structure"), Expr("member"));
   std::ostringstream out;
-  stmt->to_str(out, 2);
+  stmt->to_str(Sem(), out, 2);
   EXPECT_EQ(demangle(out.str()), R"(  MemberAccessor[not set]{
     Identifier[not set]{structure}
     Identifier[not set]{member}
diff --git a/src/ast/module.cc b/src/ast/module.cc
index 9d99c17..e9e3a63 100644
--- a/src/ast/module.cc
+++ b/src/ast/module.cc
@@ -81,7 +81,9 @@
                                   ctx->Clone(global_variables_));
 }
 
-void Module::to_str(std::ostream& out, size_t indent) const {
+void Module::to_str(const semantic::Info& sem,
+                    std::ostream& out,
+                    size_t indent) const {
   make_indent(out, indent);
   out << "Module{" << std::endl;
   indent += 2;
@@ -91,25 +93,25 @@
       out << alias->symbol().to_str() << " -> " << alias->type()->type_name()
           << std::endl;
       if (auto* str = alias->type()->As<type::Struct>()) {
-        str->impl()->to_str(out, indent);
+        str->impl()->to_str(sem, out, indent);
       }
     } else if (auto* str = ty->As<type::Struct>()) {
       out << str->symbol().to_str() << " ";
-      str->impl()->to_str(out, indent);
+      str->impl()->to_str(sem, out, indent);
     }
   }
   for (auto* var : global_variables_) {
-    var->to_str(out, indent);
+    var->to_str(sem, out, indent);
   }
   for (auto* func : functions_) {
-    func->to_str(out, indent);
+    func->to_str(sem, out, indent);
   }
   out << "}" << std::endl;
 }
 
-std::string Module::to_str() const {
+std::string Module::to_str(const semantic::Info& sem) const {
   std::ostringstream out;
-  to_str(out, 0);
+  to_str(sem, out, 0);
   return out.str();
 }
 
diff --git a/src/ast/module.h b/src/ast/module.h
index bbc3c0b..c094c7c 100644
--- a/src/ast/module.h
+++ b/src/ast/module.h
@@ -89,12 +89,16 @@
   Module* Clone(CloneContext* ctx) const override;
 
   /// Writes a representation of the node to the output stream
+  /// @param sem the semantic info for the program
   /// @param out the stream to write to
   /// @param indent number of spaces to indent the node when writing
-  void to_str(std::ostream& out, size_t indent) const override;
+  void to_str(const semantic::Info& sem,
+              std::ostream& out,
+              size_t indent) const override;
 
+  /// @param sem the semantic info for the program
   /// @returns a string representation of the Builder
-  std::string to_str() const;
+  std::string to_str(const semantic::Info& sem) const;
 
  private:
   std::vector<type::Type*> constructed_types_;
diff --git a/src/ast/module_clone_test.cc b/src/ast/module_clone_test.cc
index d2d72a1..8fa8350 100644
--- a/src/ast/module_clone_test.cc
+++ b/src/ast/module_clone_test.cc
@@ -122,8 +122,8 @@
 
   // Expect the AST printed with to_str() to match
   Demangler demanger;
-  EXPECT_EQ(demanger.Demangle(src.Symbols(), src.AST().to_str()),
-            demanger.Demangle(dst.Symbols(), dst.AST().to_str()));
+  EXPECT_EQ(demanger.Demangle(src.Symbols(), src.AST().to_str(src.Sem())),
+            demanger.Demangle(dst.Symbols(), dst.AST().to_str(dst.Sem())));
 
   // Check that none of the AST nodes or type pointers in dst are found in src
   std::unordered_set<ast::Node*> src_nodes;
@@ -135,7 +135,7 @@
     src_types.emplace(src_type);
   }
   for (auto* dst_node : dst.Nodes().Objects()) {
-    ASSERT_EQ(src_nodes.count(dst_node), 0u) << dst_node->str();
+    ASSERT_EQ(src_nodes.count(dst_node), 0u) << dst_node->str(dst.Sem());
   }
   for (auto* dst_type : dst.Types()) {
     ASSERT_EQ(src_types.count(dst_type), 0u) << dst_type->type_name();
diff --git a/src/ast/module_test.cc b/src/ast/module_test.cc
index f987023..2f62568 100644
--- a/src/ast/module_test.cc
+++ b/src/ast/module_test.cc
@@ -36,7 +36,7 @@
 }
 
 TEST_F(ModuleTest, ToStrEmitsPreambleAndPostamble) {
-  const auto str = Program(std::move(*this)).AST().to_str();
+  const auto str = Program(std::move(*this)).to_str();
   auto* const expected = "Module{\n}\n";
   EXPECT_EQ(str, expected);
 }
diff --git a/src/ast/node.cc b/src/ast/node.cc
index 8d0f1e0..41480b2 100644
--- a/src/ast/node.cc
+++ b/src/ast/node.cc
@@ -32,9 +32,9 @@
     out << " ";
 }
 
-std::string Node::str() const {
+std::string Node::str(const semantic::Info& sem) const {
   std::ostringstream out;
-  to_str(out, 0);
+  to_str(sem, out, 0);
   return out.str();
 }
 
diff --git a/src/ast/node.h b/src/ast/node.h
index 1c215e4..e7b9262 100644
--- a/src/ast/node.h
+++ b/src/ast/node.h
@@ -29,6 +29,9 @@
 namespace type {
 class Type;
 }
+namespace semantic {
+class Info;
+}
 
 namespace ast {
 
@@ -52,13 +55,17 @@
   virtual bool IsValid() const = 0;
 
   /// Writes a representation of the node to the output stream
+  /// @param sem the semantic info for the program
   /// @param out the stream to write to
   /// @param indent number of spaces to indent the node when writing
-  virtual void to_str(std::ostream& out, size_t indent) const = 0;
+  virtual void to_str(const semantic::Info& sem,
+                      std::ostream& out,
+                      size_t indent) const = 0;
 
   /// Convenience wrapper around the to_str() method.
+  /// @param sem the semantic info for the program
   /// @returns the node as a string
-  std::string str() const;
+  std::string str(const semantic::Info& sem) const;
 
  protected:
   /// Create a new node
diff --git a/src/ast/null_literal.cc b/src/ast/null_literal.cc
index edcf896..835ae29 100644
--- a/src/ast/null_literal.cc
+++ b/src/ast/null_literal.cc
@@ -27,7 +27,7 @@
 
 NullLiteral::~NullLiteral() = default;
 
-std::string NullLiteral::to_str() const {
+std::string NullLiteral::to_str(const semantic::Info&) const {
   return "null " + type()->type_name();
 }
 
diff --git a/src/ast/null_literal.h b/src/ast/null_literal.h
index 31ada3b..de6f7fc 100644
--- a/src/ast/null_literal.h
+++ b/src/ast/null_literal.h
@@ -34,8 +34,9 @@
   /// @returns the name for this literal. This name is unique to this value.
   std::string name() const override;
 
+  /// @param sem the semantic info for the program
   /// @returns the literal as a string
-  std::string to_str() const override;
+  std::string to_str(const semantic::Info& sem) const override;
 
   /// Clones this node and all transitive child nodes using the `CloneContext`
   /// `ctx`.
diff --git a/src/ast/null_literal_test.cc b/src/ast/null_literal_test.cc
index e50bd42..eac5bb0 100644
--- a/src/ast/null_literal_test.cc
+++ b/src/ast/null_literal_test.cc
@@ -40,7 +40,7 @@
 TEST_F(NullLiteralTest, ToStr) {
   auto* i = create<NullLiteral>(ty.i32());
 
-  EXPECT_EQ(i->to_str(), "null __i32");
+  EXPECT_EQ(i->to_str(Sem()), "null __i32");
 }
 
 TEST_F(NullLiteralTest, Name_I32) {
diff --git a/src/ast/return_statement.cc b/src/ast/return_statement.cc
index 38133c2..2f85773 100644
--- a/src/ast/return_statement.cc
+++ b/src/ast/return_statement.cc
@@ -44,7 +44,9 @@
   return true;
 }
 
-void ReturnStatement::to_str(std::ostream& out, size_t indent) const {
+void ReturnStatement::to_str(const semantic::Info& sem,
+                             std::ostream& out,
+                             size_t indent) const {
   make_indent(out, indent);
   out << "Return{";
 
@@ -54,7 +56,7 @@
     make_indent(out, indent + 2);
     out << "{" << std::endl;
 
-    value_->to_str(out, indent + 4);
+    value_->to_str(sem, out, indent + 4);
 
     make_indent(out, indent + 2);
     out << "}" << std::endl;
diff --git a/src/ast/return_statement.h b/src/ast/return_statement.h
index 5a56ba6..31c4fb5 100644
--- a/src/ast/return_statement.h
+++ b/src/ast/return_statement.h
@@ -55,9 +55,12 @@
   bool IsValid() const override;
 
   /// Writes a representation of the node to the output stream
+  /// @param sem the semantic info for the program
   /// @param out the stream to write to
   /// @param indent number of spaces to indent the node when writing
-  void to_str(std::ostream& out, size_t indent) const override;
+  void to_str(const semantic::Info& sem,
+              std::ostream& out,
+              size_t indent) const override;
 
  private:
   ReturnStatement(const ReturnStatement&) = delete;
diff --git a/src/ast/return_statement_test.cc b/src/ast/return_statement_test.cc
index 68506c0..d09352a 100644
--- a/src/ast/return_statement_test.cc
+++ b/src/ast/return_statement_test.cc
@@ -76,7 +76,7 @@
   auto* expr = Expr("expr");
   auto* r = create<ReturnStatement>(expr);
   std::ostringstream out;
-  r->to_str(out, 2);
+  r->to_str(Sem(), out, 2);
   EXPECT_EQ(demangle(out.str()), R"(  Return{
     {
       Identifier[not set]{expr}
@@ -88,7 +88,7 @@
 TEST_F(ReturnStatementTest, ToStr_WithoutValue) {
   auto* r = create<ReturnStatement>();
   std::ostringstream out;
-  r->to_str(out, 2);
+  r->to_str(Sem(), out, 2);
   EXPECT_EQ(out.str(), R"(  Return{}
 )");
 }
diff --git a/src/ast/scalar_constructor_expression.cc b/src/ast/scalar_constructor_expression.cc
index ebbecc6..4c90f19 100644
--- a/src/ast/scalar_constructor_expression.cc
+++ b/src/ast/scalar_constructor_expression.cc
@@ -41,11 +41,12 @@
   return literal_ != nullptr;
 }
 
-void ScalarConstructorExpression::to_str(std::ostream& out,
+void ScalarConstructorExpression::to_str(const semantic::Info& sem,
+                                         std::ostream& out,
                                          size_t indent) const {
   make_indent(out, indent);
-  out << "ScalarConstructor[" << result_type_str() << "]{" << literal_->to_str()
-      << "}" << std::endl;
+  out << "ScalarConstructor[" << result_type_str(sem) << "]{"
+      << literal_->to_str(sem) << "}" << std::endl;
 }
 
 }  // namespace ast
diff --git a/src/ast/scalar_constructor_expression.h b/src/ast/scalar_constructor_expression.h
index 5c96ef9..052eb05 100644
--- a/src/ast/scalar_constructor_expression.h
+++ b/src/ast/scalar_constructor_expression.h
@@ -51,9 +51,12 @@
   bool IsValid() const override;
 
   /// Writes a representation of the node to the output stream
+  /// @param sem the semantic info for the program
   /// @param out the stream to write to
   /// @param indent number of spaces to indent the node when writing
-  void to_str(std::ostream& out, size_t indent) const override;
+  void to_str(const semantic::Info& sem,
+              std::ostream& out,
+              size_t indent) const override;
 
  private:
   ScalarConstructorExpression(const ScalarConstructorExpression&) = delete;
diff --git a/src/ast/scalar_constructor_expression_test.cc b/src/ast/scalar_constructor_expression_test.cc
index c4a4559..46fde36 100644
--- a/src/ast/scalar_constructor_expression_test.cc
+++ b/src/ast/scalar_constructor_expression_test.cc
@@ -50,7 +50,7 @@
 TEST_F(ScalarConstructorExpressionTest, ToStr) {
   auto* c = Expr(true);
   std::ostringstream out;
-  c->to_str(out, 2);
+  c->to_str(Sem(), out, 2);
   EXPECT_EQ(out.str(), R"(  ScalarConstructor[not set]{true}
 )");
 }
diff --git a/src/ast/sint_literal.cc b/src/ast/sint_literal.cc
index bc78d50..3c946ac 100644
--- a/src/ast/sint_literal.cc
+++ b/src/ast/sint_literal.cc
@@ -27,7 +27,7 @@
 
 SintLiteral::~SintLiteral() = default;
 
-std::string SintLiteral::to_str() const {
+std::string SintLiteral::to_str(const semantic::Info&) const {
   return std::to_string(value_);
 }
 
diff --git a/src/ast/sint_literal.h b/src/ast/sint_literal.h
index 2df717d..61fa740 100644
--- a/src/ast/sint_literal.h
+++ b/src/ast/sint_literal.h
@@ -38,8 +38,9 @@
   /// @returns the name for this literal. This name is unique to this value.
   std::string name() const override;
 
+  /// @param sem the semantic info for the program
   /// @returns the literal as a string
-  std::string to_str() const override;
+  std::string to_str(const semantic::Info& sem) const override;
 
   /// Clones this node and all transitive child nodes using the `CloneContext`
   /// `ctx`.
diff --git a/src/ast/sint_literal_test.cc b/src/ast/sint_literal_test.cc
index 2fc5993..a6d39a4 100644
--- a/src/ast/sint_literal_test.cc
+++ b/src/ast/sint_literal_test.cc
@@ -46,7 +46,7 @@
 TEST_F(SintLiteralTest, ToStr) {
   auto* i = create<SintLiteral>(ty.i32(), -42);
 
-  EXPECT_EQ(i->to_str(), "-42");
+  EXPECT_EQ(i->to_str(Sem()), "-42");
 }
 
 TEST_F(SintLiteralTest, Name_I32) {
diff --git a/src/ast/stage_decoration.cc b/src/ast/stage_decoration.cc
index 53cb362..bd68fe2 100644
--- a/src/ast/stage_decoration.cc
+++ b/src/ast/stage_decoration.cc
@@ -27,7 +27,9 @@
 
 StageDecoration::~StageDecoration() = default;
 
-void StageDecoration::to_str(std::ostream& out, size_t indent) const {
+void StageDecoration::to_str(const semantic::Info&,
+                             std::ostream& out,
+                             size_t indent) const {
   make_indent(out, indent);
   out << "StageDecoration{" << stage_ << "}" << std::endl;
 }
diff --git a/src/ast/stage_decoration.h b/src/ast/stage_decoration.h
index 4208c7d..f34bd21 100644
--- a/src/ast/stage_decoration.h
+++ b/src/ast/stage_decoration.h
@@ -34,9 +34,12 @@
   PipelineStage value() const { return stage_; }
 
   /// Outputs the decoration to the given stream
+  /// @param sem the semantic info for the program
   /// @param out the stream to write to
   /// @param indent number of spaces to indent the node when writing
-  void to_str(std::ostream& out, size_t indent) const override;
+  void to_str(const semantic::Info& sem,
+              std::ostream& out,
+              size_t indent) const override;
 
   /// Clones this node and all transitive child nodes using the `CloneContext`
   /// `ctx`.
diff --git a/src/ast/stage_decoration_test.cc b/src/ast/stage_decoration_test.cc
index 82a5ccd..142f26d 100644
--- a/src/ast/stage_decoration_test.cc
+++ b/src/ast/stage_decoration_test.cc
@@ -39,7 +39,7 @@
 TEST_F(StageDecorationTest, ToStr) {
   auto* d = create<StageDecoration>(PipelineStage::kFragment);
   std::ostringstream out;
-  d->to_str(out, 0);
+  d->to_str(Sem(), out, 0);
   EXPECT_EQ(out.str(), R"(StageDecoration{fragment}
 )");
 }
diff --git a/src/ast/stride_decoration.cc b/src/ast/stride_decoration.cc
index 0dd1990..52f3ef5 100644
--- a/src/ast/stride_decoration.cc
+++ b/src/ast/stride_decoration.cc
@@ -27,7 +27,9 @@
 
 StrideDecoration::~StrideDecoration() = default;
 
-void StrideDecoration::to_str(std::ostream& out, size_t indent) const {
+void StrideDecoration::to_str(const semantic::Info&,
+                              std::ostream& out,
+                              size_t indent) const {
   make_indent(out, indent);
   out << "stride " << stride_;
 }
diff --git a/src/ast/stride_decoration.h b/src/ast/stride_decoration.h
index ad99778..7e4031f 100644
--- a/src/ast/stride_decoration.h
+++ b/src/ast/stride_decoration.h
@@ -35,9 +35,12 @@
   uint32_t stride() const { return stride_; }
 
   /// Outputs the decoration to the given stream
+  /// @param sem the semantic info for the program
   /// @param out the stream to write to
   /// @param indent number of spaces to indent the node when writing
-  void to_str(std::ostream& out, size_t indent) const override;
+  void to_str(const semantic::Info& sem,
+              std::ostream& out,
+              size_t indent) const override;
 
   /// Clones this node and all transitive child nodes using the `CloneContext`
   /// `ctx`.
diff --git a/src/ast/struct.cc b/src/ast/struct.cc
index cf696c5..0493e3a 100644
--- a/src/ast/struct.cc
+++ b/src/ast/struct.cc
@@ -66,16 +66,18 @@
   return true;
 }
 
-void Struct::to_str(std::ostream& out, size_t indent) const {
+void Struct::to_str(const semantic::Info& sem,
+                    std::ostream& out,
+                    size_t indent) const {
   out << "Struct{" << std::endl;
   for (auto* deco : decorations_) {
     make_indent(out, indent + 2);
     out << "[[";
-    deco->to_str(out, 0);
+    deco->to_str(sem, out, 0);
     out << "]]" << std::endl;
   }
   for (auto* member : members_) {
-    member->to_str(out, indent + 2);
+    member->to_str(sem, out, indent + 2);
   }
   make_indent(out, indent);
   out << "}" << std::endl;
diff --git a/src/ast/struct.h b/src/ast/struct.h
index 9b4cb38..36b0062 100644
--- a/src/ast/struct.h
+++ b/src/ast/struct.h
@@ -67,9 +67,12 @@
   bool IsValid() const override;
 
   /// Writes a representation of the node to the output stream
+  /// @param sem the semantic info for the program
   /// @param out the stream to write to
   /// @param indent number of spaces to indent the node when writing
-  void to_str(std::ostream& out, size_t indent) const override;
+  void to_str(const semantic::Info& sem,
+              std::ostream& out,
+              size_t indent) const override;
 
  private:
   Struct(const Struct&) = delete;
diff --git a/src/ast/struct_block_decoration.cc b/src/ast/struct_block_decoration.cc
index 2e1cf92..5c69f81 100644
--- a/src/ast/struct_block_decoration.cc
+++ b/src/ast/struct_block_decoration.cc
@@ -27,7 +27,9 @@
 
 StructBlockDecoration::~StructBlockDecoration() = default;
 
-void StructBlockDecoration::to_str(std::ostream& out, size_t indent) const {
+void StructBlockDecoration::to_str(const semantic::Info&,
+                                   std::ostream& out,
+                                   size_t indent) const {
   make_indent(out, indent);
   out << "block";
 }
diff --git a/src/ast/struct_block_decoration.h b/src/ast/struct_block_decoration.h
index 732fa5e..c9e7d8a 100644
--- a/src/ast/struct_block_decoration.h
+++ b/src/ast/struct_block_decoration.h
@@ -34,9 +34,12 @@
   ~StructBlockDecoration() override;
 
   /// Outputs the decoration to the given stream
+  /// @param sem the semantic info for the program
   /// @param out the stream to write to
   /// @param indent number of spaces to indent the node when writing
-  void to_str(std::ostream& out, size_t indent) const override;
+  void to_str(const semantic::Info& sem,
+              std::ostream& out,
+              size_t indent) const override;
 
   /// Clones this node and all transitive child nodes using the `CloneContext`
   /// `ctx`.
diff --git a/src/ast/struct_member.cc b/src/ast/struct_member.cc
index 0cc7a71..b6aa642 100644
--- a/src/ast/struct_member.cc
+++ b/src/ast/struct_member.cc
@@ -72,13 +72,15 @@
   return true;
 }
 
-void StructMember::to_str(std::ostream& out, size_t indent) const {
+void StructMember::to_str(const semantic::Info& sem,
+                          std::ostream& out,
+                          size_t indent) const {
   make_indent(out, indent);
   out << "StructMember{";
   if (decorations_.size() > 0) {
     out << "[[ ";
     for (auto* deco : decorations_)
-      out << deco->str() << " ";
+      out << deco->str(sem) << " ";
     out << "]] ";
   }
 
diff --git a/src/ast/struct_member.h b/src/ast/struct_member.h
index 9f06562..c087b2b 100644
--- a/src/ast/struct_member.h
+++ b/src/ast/struct_member.h
@@ -70,9 +70,12 @@
   bool IsValid() const override;
 
   /// Writes a representation of the node to the output stream
+  /// @param sem the semantic info for the program
   /// @param out the stream to write to
   /// @param indent number of spaces to indent the node when writing
-  void to_str(std::ostream& out, size_t indent) const override;
+  void to_str(const semantic::Info& sem,
+              std::ostream& out,
+              size_t indent) const override;
 
  private:
   StructMember(const StructMember&) = delete;
diff --git a/src/ast/struct_member_offset_decoration.cc b/src/ast/struct_member_offset_decoration.cc
index 09ebef6..87f13a6 100644
--- a/src/ast/struct_member_offset_decoration.cc
+++ b/src/ast/struct_member_offset_decoration.cc
@@ -28,7 +28,8 @@
 
 StructMemberOffsetDecoration::~StructMemberOffsetDecoration() = default;
 
-void StructMemberOffsetDecoration::to_str(std::ostream& out,
+void StructMemberOffsetDecoration::to_str(const semantic::Info&,
+                                          std::ostream& out,
                                           size_t indent) const {
   make_indent(out, indent);
   out << "offset " << std::to_string(offset_);
diff --git a/src/ast/struct_member_offset_decoration.h b/src/ast/struct_member_offset_decoration.h
index 445483b..031a758 100644
--- a/src/ast/struct_member_offset_decoration.h
+++ b/src/ast/struct_member_offset_decoration.h
@@ -36,9 +36,12 @@
   uint32_t offset() const { return offset_; }
 
   /// Outputs the decoration to the given stream
+  /// @param sem the semantic info for the program
   /// @param out the stream to write to
   /// @param indent number of spaces to indent the node when writing
-  void to_str(std::ostream& out, size_t indent) const override;
+  void to_str(const semantic::Info& sem,
+              std::ostream& out,
+              size_t indent) const override;
 
   /// Clones this node and all transitive child nodes using the `CloneContext`
   /// `ctx`.
diff --git a/src/ast/struct_member_test.cc b/src/ast/struct_member_test.cc
index 55ffc64..44c2bb9 100644
--- a/src/ast/struct_member_test.cc
+++ b/src/ast/struct_member_test.cc
@@ -75,14 +75,14 @@
 TEST_F(StructMemberTest, ToStr) {
   auto* st = Member("a", ty.i32(), {MemberOffset(4)});
   std::ostringstream out;
-  st->to_str(out, 2);
+  st->to_str(Sem(), out, 2);
   EXPECT_EQ(demangle(out.str()), "  StructMember{[[ offset 4 ]] a: __i32}\n");
 }
 
 TEST_F(StructMemberTest, ToStrNoDecorations) {
   auto* st = Member("a", ty.i32());
   std::ostringstream out;
-  st->to_str(out, 2);
+  st->to_str(Sem(), out, 2);
   EXPECT_EQ(demangle(out.str()), "  StructMember{a: __i32}\n");
 }
 
diff --git a/src/ast/struct_test.cc b/src/ast/struct_test.cc
index 8274c5a..103c22b 100644
--- a/src/ast/struct_test.cc
+++ b/src/ast/struct_test.cc
@@ -93,7 +93,7 @@
   auto* s = create<Struct>(StructMemberList{Member("a", ty.i32())}, decos);
 
   std::ostringstream out;
-  s->to_str(out, 2);
+  s->to_str(Sem(), out, 2);
   EXPECT_EQ(demangle(out.str()), R"(Struct{
     [[block]]
     StructMember{a: __i32}
diff --git a/src/ast/switch_statement.cc b/src/ast/switch_statement.cc
index 4bc34b3..89dc22d 100644
--- a/src/ast/switch_statement.cc
+++ b/src/ast/switch_statement.cc
@@ -49,16 +49,18 @@
   return true;
 }
 
-void SwitchStatement::to_str(std::ostream& out, size_t indent) const {
+void SwitchStatement::to_str(const semantic::Info& sem,
+                             std::ostream& out,
+                             size_t indent) const {
   make_indent(out, indent);
   out << "Switch{" << std::endl;
-  condition_->to_str(out, indent + 2);
+  condition_->to_str(sem, out, indent + 2);
 
   make_indent(out, indent + 2);
   out << "{" << std::endl;
 
   for (auto* stmt : body_) {
-    stmt->to_str(out, indent + 4);
+    stmt->to_str(sem, out, indent + 4);
   }
 
   make_indent(out, indent + 2);
diff --git a/src/ast/switch_statement.h b/src/ast/switch_statement.h
index b964724..6c180cd 100644
--- a/src/ast/switch_statement.h
+++ b/src/ast/switch_statement.h
@@ -60,9 +60,12 @@
   bool IsValid() const override;
 
   /// Writes a representation of the node to the output stream
+  /// @param sem the semantic info for the program
   /// @param out the stream to write to
   /// @param indent number of spaces to indent the node when writing
-  void to_str(std::ostream& out, size_t indent) const override;
+  void to_str(const semantic::Info& sem,
+              std::ostream& out,
+              size_t indent) const override;
 
  private:
   SwitchStatement(const SwitchStatement&) = delete;
diff --git a/src/ast/switch_statement_test.cc b/src/ast/switch_statement_test.cc
index d51eb55..86d0b90 100644
--- a/src/ast/switch_statement_test.cc
+++ b/src/ast/switch_statement_test.cc
@@ -137,7 +137,7 @@
 
   auto* stmt = create<SwitchStatement>(ident, CaseStatementList{});
   std::ostringstream out;
-  stmt->to_str(out, 2);
+  stmt->to_str(Sem(), out, 2);
   EXPECT_EQ(demangle(out.str()), R"(  Switch{
     Identifier[not set]{ident}
     {
@@ -157,7 +157,7 @@
 
   auto* stmt = create<SwitchStatement>(ident, body);
   std::ostringstream out;
-  stmt->to_str(out, 2);
+  stmt->to_str(Sem(), out, 2);
   EXPECT_EQ(demangle(out.str()), R"(  Switch{
     Identifier[not set]{ident}
     {
diff --git a/src/ast/type_constructor_expression.cc b/src/ast/type_constructor_expression.cc
index 2afde7b..532e0d0 100644
--- a/src/ast/type_constructor_expression.cc
+++ b/src/ast/type_constructor_expression.cc
@@ -53,14 +53,16 @@
   return true;
 }
 
-void TypeConstructorExpression::to_str(std::ostream& out, size_t indent) const {
+void TypeConstructorExpression::to_str(const semantic::Info& sem,
+                                       std::ostream& out,
+                                       size_t indent) const {
   make_indent(out, indent);
-  out << "TypeConstructor[" << result_type_str() << "]{" << std::endl;
+  out << "TypeConstructor[" << result_type_str(sem) << "]{" << std::endl;
   make_indent(out, indent + 2);
   out << type_->type_name() << std::endl;
 
   for (auto* val : values_) {
-    val->to_str(out, indent + 2);
+    val->to_str(sem, out, indent + 2);
   }
   make_indent(out, indent);
   out << "}" << std::endl;
diff --git a/src/ast/type_constructor_expression.h b/src/ast/type_constructor_expression.h
index 1fdbeec..9190a84 100644
--- a/src/ast/type_constructor_expression.h
+++ b/src/ast/type_constructor_expression.h
@@ -56,9 +56,12 @@
   bool IsValid() const override;
 
   /// Writes a representation of the node to the output stream
+  /// @param sem the semantic info for the program
   /// @param out the stream to write to
   /// @param indent number of spaces to indent the node when writing
-  void to_str(std::ostream& out, size_t indent) const override;
+  void to_str(const semantic::Info& sem,
+              std::ostream& out,
+              size_t indent) const override;
 
  private:
   TypeConstructorExpression(const TypeConstructorExpression&) = delete;
diff --git a/src/ast/type_constructor_expression_test.cc b/src/ast/type_constructor_expression_test.cc
index 4f1b3b1..e4421a4 100644
--- a/src/ast/type_constructor_expression_test.cc
+++ b/src/ast/type_constructor_expression_test.cc
@@ -107,7 +107,7 @@
 
   auto* t = create<TypeConstructorExpression>(&vec, expr);
   std::ostringstream out;
-  t->to_str(out, 2);
+  t->to_str(Sem(), out, 2);
   EXPECT_EQ(demangle(out.str()), R"(  TypeConstructor[not set]{
     __vec_3__f32
     Identifier[not set]{expr_1}
diff --git a/src/ast/uint_literal.cc b/src/ast/uint_literal.cc
index 076b896..e9dd464 100644
--- a/src/ast/uint_literal.cc
+++ b/src/ast/uint_literal.cc
@@ -27,7 +27,7 @@
 
 UintLiteral::~UintLiteral() = default;
 
-std::string UintLiteral::to_str() const {
+std::string UintLiteral::to_str(const semantic::Info&) const {
   return std::to_string(value_);
 }
 
diff --git a/src/ast/uint_literal.h b/src/ast/uint_literal.h
index 3464a88..c3ff8cf 100644
--- a/src/ast/uint_literal.h
+++ b/src/ast/uint_literal.h
@@ -38,8 +38,9 @@
   /// @returns the name for this literal. This name is unique to this value.
   std::string name() const override;
 
+  /// @param sem the semantic info for the program
   /// @returns the literal as a string
-  std::string to_str() const override;
+  std::string to_str(const semantic::Info& sem) const override;
 
   /// Clones this node and all transitive child nodes using the `CloneContext`
   /// `ctx`.
diff --git a/src/ast/uint_literal_test.cc b/src/ast/uint_literal_test.cc
index 1216bb2..6628269 100644
--- a/src/ast/uint_literal_test.cc
+++ b/src/ast/uint_literal_test.cc
@@ -44,7 +44,7 @@
 
 TEST_F(UintLiteralTest, ToStr) {
   auto* u = create<UintLiteral>(ty.u32(), 42);
-  EXPECT_EQ(u->to_str(), "42");
+  EXPECT_EQ(u->to_str(Sem()), "42");
 }
 
 }  // namespace
diff --git a/src/ast/unary_op_expression.cc b/src/ast/unary_op_expression.cc
index 890e1ed..495e49a 100644
--- a/src/ast/unary_op_expression.cc
+++ b/src/ast/unary_op_expression.cc
@@ -40,12 +40,14 @@
   return expr_ != nullptr && expr_->IsValid();
 }
 
-void UnaryOpExpression::to_str(std::ostream& out, size_t indent) const {
+void UnaryOpExpression::to_str(const semantic::Info& sem,
+                               std::ostream& out,
+                               size_t indent) const {
   make_indent(out, indent);
-  out << "UnaryOp[" << result_type_str() << "]{" << std::endl;
+  out << "UnaryOp[" << result_type_str(sem) << "]{" << std::endl;
   make_indent(out, indent + 2);
   out << op_ << std::endl;
-  expr_->to_str(out, indent + 2);
+  expr_->to_str(sem, out, indent + 2);
   make_indent(out, indent);
   out << "}" << std::endl;
 }
diff --git a/src/ast/unary_op_expression.h b/src/ast/unary_op_expression.h
index 0a9d352..c7d8017 100644
--- a/src/ast/unary_op_expression.h
+++ b/src/ast/unary_op_expression.h
@@ -54,9 +54,12 @@
   bool IsValid() const override;
 
   /// Writes a representation of the node to the output stream
+  /// @param sem the semantic info for the program
   /// @param out the stream to write to
   /// @param indent number of spaces to indent the node when writing
-  void to_str(std::ostream& out, size_t indent) const override;
+  void to_str(const semantic::Info& sem,
+              std::ostream& out,
+              size_t indent) const override;
 
  private:
   UnaryOpExpression(const UnaryOpExpression&) = delete;
diff --git a/src/ast/unary_op_expression_test.cc b/src/ast/unary_op_expression_test.cc
index 9b0717a..6abc1bb 100644
--- a/src/ast/unary_op_expression_test.cc
+++ b/src/ast/unary_op_expression_test.cc
@@ -69,7 +69,7 @@
   auto* ident = Expr("ident");
   auto* u = create<UnaryOpExpression>(UnaryOp::kNot, ident);
   std::ostringstream out;
-  u->to_str(out, 2);
+  u->to_str(Sem(), out, 2);
   EXPECT_EQ(demangle(out.str()), R"(  UnaryOp[not set]{
     not
     Identifier[not set]{ident}
diff --git a/src/ast/variable.cc b/src/ast/variable.cc
index 52e869b..cc0e676 100644
--- a/src/ast/variable.cc
+++ b/src/ast/variable.cc
@@ -110,7 +110,9 @@
   return true;
 }
 
-void Variable::info_to_str(std::ostream& out, size_t indent) const {
+void Variable::info_to_str(const semantic::Info&,
+                           std::ostream& out,
+                           size_t indent) const {
   make_indent(out, indent);
   out << symbol_.to_str() << std::endl;
   make_indent(out, indent);
@@ -119,20 +121,24 @@
   out << type_->type_name() << std::endl;
 }
 
-void Variable::constructor_to_str(std::ostream& out, size_t indent) const {
+void Variable::constructor_to_str(const semantic::Info& sem,
+                                  std::ostream& out,
+                                  size_t indent) const {
   if (constructor_ == nullptr)
     return;
 
   make_indent(out, indent);
   out << "{" << std::endl;
 
-  constructor_->to_str(out, indent + 2);
+  constructor_->to_str(sem, out, indent + 2);
 
   make_indent(out, indent);
   out << "}" << std::endl;
 }
 
-void Variable::to_str(std::ostream& out, size_t indent) const {
+void Variable::to_str(const semantic::Info& sem,
+                      std::ostream& out,
+                      size_t indent) const {
   make_indent(out, indent);
   out << "Variable";
   if (is_const()) {
@@ -144,14 +150,14 @@
     make_indent(out, indent + 2);
     out << "Decorations{" << std::endl;
     for (auto* deco : decorations_) {
-      deco->to_str(out, indent + 4);
+      deco->to_str(sem, out, indent + 4);
     }
     make_indent(out, indent + 2);
     out << "}" << std::endl;
   }
 
-  info_to_str(out, indent + 2);
-  constructor_to_str(out, indent + 2);
+  info_to_str(sem, out, indent + 2);
+  constructor_to_str(sem, out, indent + 2);
   make_indent(out, indent);
   out << "}" << std::endl;
 }
diff --git a/src/ast/variable.h b/src/ast/variable.h
index e721ad0..fb337b1 100644
--- a/src/ast/variable.h
+++ b/src/ast/variable.h
@@ -150,19 +150,28 @@
   bool IsValid() const override;
 
   /// Writes a representation of the node to the output stream
+  /// @param sem the semantic info for the program
   /// @param out the stream to write to
   /// @param indent number of spaces to indent the node when writing
-  void to_str(std::ostream& out, size_t indent) const override;
+  void to_str(const semantic::Info& sem,
+              std::ostream& out,
+              size_t indent) const override;
 
  protected:
   /// Output information for this variable.
+  /// @param sem the semantic info for the program
   /// @param out the stream to write to
   /// @param indent number of spaces to indent the node when writing
-  void info_to_str(std::ostream& out, size_t indent) const;
+  void info_to_str(const semantic::Info& sem,
+                   std::ostream& out,
+                   size_t indent) const;
   /// Output constructor for this variable.
+  /// @param sem the semantic info for the program
   /// @param out the stream to write to
   /// @param indent number of spaces to indent the node when writing
-  void constructor_to_str(std::ostream& out, size_t indent) const;
+  void constructor_to_str(const semantic::Info& sem,
+                          std::ostream& out,
+                          size_t indent) const;
 
  private:
   Variable(const Variable&) = delete;
diff --git a/src/ast/variable_decl_statement.cc b/src/ast/variable_decl_statement.cc
index 8bdddcb..2dd0a77 100644
--- a/src/ast/variable_decl_statement.cc
+++ b/src/ast/variable_decl_statement.cc
@@ -39,10 +39,12 @@
   return variable_ != nullptr && variable_->IsValid();
 }
 
-void VariableDeclStatement::to_str(std::ostream& out, size_t indent) const {
+void VariableDeclStatement::to_str(const semantic::Info& sem,
+                                   std::ostream& out,
+                                   size_t indent) const {
   make_indent(out, indent);
   out << "VariableDeclStatement{" << std::endl;
-  variable_->to_str(out, indent + 2);
+  variable_->to_str(sem, out, indent + 2);
   make_indent(out, indent);
   out << "}" << std::endl;
 }
diff --git a/src/ast/variable_decl_statement.h b/src/ast/variable_decl_statement.h
index bc2bed7..5acafd4 100644
--- a/src/ast/variable_decl_statement.h
+++ b/src/ast/variable_decl_statement.h
@@ -52,9 +52,12 @@
   bool IsValid() const override;
 
   /// Writes a representation of the node to the output stream
+  /// @param sem the semantic info for the program
   /// @param out the stream to write to
   /// @param indent number of spaces to indent the node when writing
-  void to_str(std::ostream& out, size_t indent) const override;
+  void to_str(const semantic::Info& sem,
+              std::ostream& out,
+              size_t indent) const override;
 
  private:
   VariableDeclStatement(const VariableDeclStatement&) = delete;
diff --git a/src/ast/variable_decl_statement_test.cc b/src/ast/variable_decl_statement_test.cc
index 62294fa..6524c0e 100644
--- a/src/ast/variable_decl_statement_test.cc
+++ b/src/ast/variable_decl_statement_test.cc
@@ -71,7 +71,7 @@
   auto* stmt =
       create<VariableDeclStatement>(Source{Source::Location{20, 2}}, var);
   std::ostringstream out;
-  stmt->to_str(out, 2);
+  stmt->to_str(Sem(), out, 2);
   EXPECT_EQ(demangle(out.str()), R"(  VariableDeclStatement{
     Variable{
       a
diff --git a/src/ast/variable_test.cc b/src/ast/variable_test.cc
index a7d2319..15a930f 100644
--- a/src/ast/variable_test.cc
+++ b/src/ast/variable_test.cc
@@ -103,7 +103,7 @@
   auto* v = Var("my_var", StorageClass::kFunction, ty.f32(), nullptr,
                 ast::VariableDecorationList{});
   std::ostringstream out;
-  v->to_str(out, 2);
+  v->to_str(Sem(), out, 2);
   EXPECT_EQ(demangle(out.str()), R"(  Variable{
     my_var
     function
@@ -146,7 +146,7 @@
                   });
 
   std::ostringstream out;
-  var->to_str(out, 2);
+  var->to_str(Sem(), out, 2);
   EXPECT_EQ(demangle(out.str()), R"(  Variable{
     Decorations{
       BindingDecoration{2}
diff --git a/src/ast/workgroup_decoration.cc b/src/ast/workgroup_decoration.cc
index 914278a..0d49f10 100644
--- a/src/ast/workgroup_decoration.cc
+++ b/src/ast/workgroup_decoration.cc
@@ -38,7 +38,9 @@
 
 WorkgroupDecoration::~WorkgroupDecoration() = default;
 
-void WorkgroupDecoration::to_str(std::ostream& out, size_t indent) const {
+void WorkgroupDecoration::to_str(const semantic::Info&,
+                                 std::ostream& out,
+                                 size_t indent) const {
   make_indent(out, indent);
   out << "WorkgroupDecoration{" << x_ << " " << y_ << " " << z_ << "}"
       << std::endl;
diff --git a/src/ast/workgroup_decoration.h b/src/ast/workgroup_decoration.h
index d394215..af6e949 100644
--- a/src/ast/workgroup_decoration.h
+++ b/src/ast/workgroup_decoration.h
@@ -51,9 +51,12 @@
   }
 
   /// Outputs the decoration to the given stream
+  /// @param sem the semantic info for the program
   /// @param out the stream to write to
   /// @param indent number of spaces to indent the node when writing
-  void to_str(std::ostream& out, size_t indent) const override;
+  void to_str(const semantic::Info& sem,
+              std::ostream& out,
+              size_t indent) const override;
 
   /// Clones this node and all transitive child nodes using the `CloneContext`
   /// `ctx`.
diff --git a/src/ast/workgroup_decoration_test.cc b/src/ast/workgroup_decoration_test.cc
index 4a749f1..df28db1 100644
--- a/src/ast/workgroup_decoration_test.cc
+++ b/src/ast/workgroup_decoration_test.cc
@@ -66,7 +66,7 @@
 TEST_F(WorkgroupDecorationTest, ToStr) {
   auto* d = create<WorkgroupDecoration>(2, 4, 6);
   std::ostringstream out;
-  d->to_str(out, 0);
+  d->to_str(Sem(), out, 0);
   EXPECT_EQ(out.str(), R"(WorkgroupDecoration{2 4 6}
 )");
 }