Simplify calls to ast::Node::[to_]str()

Add helpers on Program and ProgramBuilder that significantly simplify
usage.
Also demangle - this also reduces a bunch of copy-pasta code.

Change-Id: I6215c346e7f6e49c20aced058a6150603253ed93
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/39342
Commit-Queue: Ben Clayton <bclayton@google.com>
Reviewed-by: dan sinclair <dsinclair@chromium.org>
diff --git a/fuzzers/tint_ast_clone_fuzzer.cc b/fuzzers/tint_ast_clone_fuzzer.cc
index 65cdf9c..0a2774a 100644
--- a/fuzzers/tint_ast_clone_fuzzer.cc
+++ b/fuzzers/tint_ast_clone_fuzzer.cc
@@ -16,7 +16,6 @@
 #include <string>
 #include <unordered_set>
 
-#include "src/demangler.h"
 #include "src/reader/wgsl/parser_impl.h"
 #include "src/writer/wgsl/generator.h"
 
@@ -62,8 +61,7 @@
   tint::Program dst(src.Clone());
 
   // Expect the demangled AST printed with to_str() to match
-  tint::Demangler d;
-  ASSERT_EQ(d.Demangle(src), d.Demangle(dst));
+  ASSERT_EQ(src.to_str(), dst.to_str());
 
   // Check that none of the AST nodes or type pointers in dst are found in src
   std::unordered_set<tint::ast::Node*> src_nodes;
diff --git a/samples/main.cc b/samples/main.cc
index a0678ca..7652592 100644
--- a/samples/main.cc
+++ b/samples/main.cc
@@ -499,11 +499,7 @@
   }
 
   if (options.dump_ast) {
-    auto ast_str = program->to_str();
-    if (options.demangle) {
-      ast_str = tint::Demangler().Demangle(program->Symbols(), ast_str);
-    }
-    std::cout << std::endl << ast_str << std::endl;
+    std::cout << std::endl << program->to_str(options.demangle) << std::endl;
   }
   if (options.parse_only) {
     return 1;
diff --git a/src/ast/access_decoration_test.cc b/src/ast/access_decoration_test.cc
index e48fe71..31b6e3d 100644
--- a/src/ast/access_decoration_test.cc
+++ b/src/ast/access_decoration_test.cc
@@ -36,9 +36,7 @@
 
 TEST_F(AccessDecorationTest, ToStr) {
   auto* d = create<AccessDecoration>(ast::AccessControl::kReadOnly);
-  std::ostringstream out;
-  d->to_str(Sem(), out, 0);
-  EXPECT_EQ(out.str(), R"(AccessDecoration{read_only}
+  EXPECT_EQ(str(d), R"(AccessDecoration{read_only}
 )");
 }
 
diff --git a/src/ast/array_accessor_expression_test.cc b/src/ast/array_accessor_expression_test.cc
index b86266b..fbe3efd 100644
--- a/src/ast/array_accessor_expression_test.cc
+++ b/src/ast/array_accessor_expression_test.cc
@@ -92,13 +92,10 @@
   auto* idx = Expr("idx");
 
   auto* exp = create<ArrayAccessorExpression>(ary, idx);
-  std::ostringstream out;
-  exp->to_str(Sem(), out, 2);
-
-  EXPECT_EQ(demangle(out.str()), R"(  ArrayAccessor[not set]{
-    Identifier[not set]{ary}
-    Identifier[not set]{idx}
-  }
+  EXPECT_EQ(str(exp), R"(ArrayAccessor[not set]{
+  Identifier[not set]{ary}
+  Identifier[not set]{idx}
+}
 )");
 }
 
diff --git a/src/ast/assignment_statement_test.cc b/src/ast/assignment_statement_test.cc
index cbf1813..046e3cd 100644
--- a/src/ast/assignment_statement_test.cc
+++ b/src/ast/assignment_statement_test.cc
@@ -92,13 +92,10 @@
   auto* rhs = Expr("rhs");
 
   auto* stmt = create<AssignmentStatement>(lhs, rhs);
-  std::ostringstream out;
-  stmt->to_str(Sem(), out, 2);
-
-  EXPECT_EQ(demangle(out.str()), R"(  Assignment{
-    Identifier[not set]{lhs}
-    Identifier[not set]{rhs}
-  }
+  EXPECT_EQ(str(stmt), R"(Assignment{
+  Identifier[not set]{lhs}
+  Identifier[not set]{rhs}
+}
 )");
 }
 
diff --git a/src/ast/binary_expression_test.cc b/src/ast/binary_expression_test.cc
index 5a8f7bf..d37435c 100644
--- a/src/ast/binary_expression_test.cc
+++ b/src/ast/binary_expression_test.cc
@@ -105,13 +105,11 @@
   auto* rhs = Expr("rhs");
 
   auto* r = create<BinaryExpression>(BinaryOp::kEqual, lhs, rhs);
-  std::ostringstream out;
-  r->to_str(Sem(), out, 2);
-  EXPECT_EQ(demangle(out.str()), R"(  Binary[not set]{
-    Identifier[not set]{lhs}
-    equal
-    Identifier[not set]{rhs}
-  }
+  EXPECT_EQ(str(r), R"(Binary[not set]{
+  Identifier[not set]{lhs}
+  equal
+  Identifier[not set]{rhs}
+}
 )");
 }
 
diff --git a/src/ast/binding_decoration_test.cc b/src/ast/binding_decoration_test.cc
index a50a048..1d4a08f 100644
--- a/src/ast/binding_decoration_test.cc
+++ b/src/ast/binding_decoration_test.cc
@@ -39,9 +39,7 @@
 
 TEST_F(BindingDecorationTest, ToStr) {
   auto* d = create<BindingDecoration>(2);
-  std::ostringstream out;
-  d->to_str(Sem(), out, 0);
-  EXPECT_EQ(out.str(), R"(BindingDecoration{2}
+  EXPECT_EQ(str(d), R"(BindingDecoration{2}
 )");
 }
 
diff --git a/src/ast/bitcast_expression_test.cc b/src/ast/bitcast_expression_test.cc
index 2dce4b9..1380400 100644
--- a/src/ast/bitcast_expression_test.cc
+++ b/src/ast/bitcast_expression_test.cc
@@ -78,12 +78,9 @@
   auto* expr = Expr("expr");
 
   auto* exp = create<BitcastExpression>(ty.f32(), expr);
-  std::ostringstream out;
-  exp->to_str(Sem(), out, 2);
-
-  EXPECT_EQ(demangle(out.str()), R"(  Bitcast[not set]<__f32>{
-    Identifier[not set]{expr}
-  }
+  EXPECT_EQ(str(exp), R"(Bitcast[not set]<__f32>{
+  Identifier[not set]{expr}
+}
 )");
 }
 
diff --git a/src/ast/block_statement_test.cc b/src/ast/block_statement_test.cc
index 5290be8..ddbbcad 100644
--- a/src/ast/block_statement_test.cc
+++ b/src/ast/block_statement_test.cc
@@ -86,11 +86,9 @@
       create<DiscardStatement>(),
   });
 
-  std::ostringstream out;
-  b->to_str(Sem(), out, 2);
-  EXPECT_EQ(out.str(), R"(  Block{
-    Discard{}
-  }
+  EXPECT_EQ(str(b), R"(Block{
+  Discard{}
+}
 )");
 }
 
diff --git a/src/ast/bool_literal_test.cc b/src/ast/bool_literal_test.cc
index 01c6eac..59ee37a 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(Sem()), "true");
-  EXPECT_EQ(f->to_str(Sem()), "false");
+  EXPECT_EQ(str(t), "true");
+  EXPECT_EQ(str(f), "false");
 }
 
 }  // namespace
diff --git a/src/ast/break_statement_test.cc b/src/ast/break_statement_test.cc
index ffad482..78a0cf8 100644
--- a/src/ast/break_statement_test.cc
+++ b/src/ast/break_statement_test.cc
@@ -41,9 +41,7 @@
 
 TEST_F(BreakStatementTest, ToStr) {
   auto* stmt = create<BreakStatement>();
-  std::ostringstream out;
-  stmt->to_str(Sem(), out, 2);
-  EXPECT_EQ(out.str(), R"(  Break{}
+  EXPECT_EQ(str(stmt), R"(Break{}
 )");
 }
 
diff --git a/src/ast/builtin_decoration_test.cc b/src/ast/builtin_decoration_test.cc
index b115425..a0fd15b 100644
--- a/src/ast/builtin_decoration_test.cc
+++ b/src/ast/builtin_decoration_test.cc
@@ -39,9 +39,7 @@
 
 TEST_F(BuiltinDecorationTest, ToStr) {
   auto* d = create<BuiltinDecoration>(Builtin::kFragDepth);
-  std::ostringstream out;
-  d->to_str(Sem(), out, 0);
-  EXPECT_EQ(out.str(), R"(BuiltinDecoration{frag_depth}
+  EXPECT_EQ(str(d), R"(BuiltinDecoration{frag_depth}
 )");
 }
 
diff --git a/src/ast/call_expression_test.cc b/src/ast/call_expression_test.cc
index f8f3ee7..fd18009 100644
--- a/src/ast/call_expression_test.cc
+++ b/src/ast/call_expression_test.cc
@@ -96,13 +96,11 @@
 TEST_F(CallExpressionTest, ToStr_NoParams) {
   auto* func = Expr("func");
   auto* stmt = create<CallExpression>(func, ExpressionList{});
-  std::ostringstream out;
-  stmt->to_str(Sem(), out, 2);
-  EXPECT_EQ(demangle(out.str()), R"(  Call[not set]{
-    Identifier[not set]{func}
-    (
-    )
-  }
+  EXPECT_EQ(str(stmt), R"(Call[not set]{
+  Identifier[not set]{func}
+  (
+  )
+}
 )");
 }
 
@@ -113,15 +111,13 @@
   params.push_back(Expr("param2"));
 
   auto* stmt = create<CallExpression>(func, params);
-  std::ostringstream out;
-  stmt->to_str(Sem(), out, 2);
-  EXPECT_EQ(demangle(out.str()), R"(  Call[not set]{
-    Identifier[not set]{func}
-    (
-      Identifier[not set]{param1}
-      Identifier[not set]{param2}
-    )
-  }
+  EXPECT_EQ(str(stmt), R"(Call[not set]{
+  Identifier[not set]{func}
+  (
+    Identifier[not set]{param1}
+    Identifier[not set]{param2}
+  )
+}
 )");
 }
 
diff --git a/src/ast/call_statement_test.cc b/src/ast/call_statement_test.cc
index 844d1cb..57b72e9 100644
--- a/src/ast/call_statement_test.cc
+++ b/src/ast/call_statement_test.cc
@@ -57,13 +57,11 @@
   auto* c = create<CallStatement>(
       create<CallExpression>(Expr("func"), ExpressionList{}));
 
-  std::ostringstream out;
-  c->to_str(Sem(), out, 2);
-  EXPECT_EQ(demangle(out.str()), R"(  Call[not set]{
-    Identifier[not set]{func}
-    (
-    )
-  }
+  EXPECT_EQ(str(c), R"(Call[not set]{
+  Identifier[not set]{func}
+  (
+  )
+}
 )");
 }
 
diff --git a/src/ast/case_statement_test.cc b/src/ast/case_statement_test.cc
index 2a9779b..301036c 100644
--- a/src/ast/case_statement_test.cc
+++ b/src/ast/case_statement_test.cc
@@ -134,11 +134,9 @@
   });
   auto* c = create<CaseStatement>(CaseSelectorList{b}, body);
 
-  std::ostringstream out;
-  c->to_str(Sem(), out, 2);
-  EXPECT_EQ(out.str(), R"(  Case -2{
-    Discard{}
-  }
+  EXPECT_EQ(str(c), R"(Case -2{
+  Discard{}
+}
 )");
 }
 
@@ -151,11 +149,9 @@
   });
   auto* c = create<CaseStatement>(CaseSelectorList{b}, body);
 
-  std::ostringstream out;
-  c->to_str(Sem(), out, 2);
-  EXPECT_EQ(out.str(), R"(  Case 2{
-    Discard{}
-  }
+  EXPECT_EQ(str(c), R"(Case 2{
+  Discard{}
+}
 )");
 }
 
@@ -169,11 +165,9 @@
   });
   auto* c = create<CaseStatement>(b, body);
 
-  std::ostringstream out;
-  c->to_str(Sem(), out, 2);
-  EXPECT_EQ(out.str(), R"(  Case 1, 2{
-    Discard{}
-  }
+  EXPECT_EQ(str(c), R"(Case 1, 2{
+  Discard{}
+}
 )");
 }
 
@@ -183,11 +177,9 @@
   });
   auto* c = create<CaseStatement>(CaseSelectorList{}, body);
 
-  std::ostringstream out;
-  c->to_str(Sem(), out, 2);
-  EXPECT_EQ(out.str(), R"(  Default{
-    Discard{}
-  }
+  EXPECT_EQ(str(c), R"(Default{
+  Discard{}
+}
 )");
 }
 
diff --git a/src/ast/constant_id_decoration_test.cc b/src/ast/constant_id_decoration_test.cc
index b536ee6..1dd9bf0 100644
--- a/src/ast/constant_id_decoration_test.cc
+++ b/src/ast/constant_id_decoration_test.cc
@@ -38,9 +38,7 @@
 
 TEST_F(ConstantIdDecorationTest, ToStr) {
   auto* d = create<ConstantIdDecoration>(1200);
-  std::ostringstream out;
-  d->to_str(Sem(), out, 0);
-  EXPECT_EQ(out.str(), R"(ConstantIdDecoration{1200}
+  EXPECT_EQ(str(d), R"(ConstantIdDecoration{1200}
 )");
 }
 
diff --git a/src/ast/continue_statement_test.cc b/src/ast/continue_statement_test.cc
index 31c859a..356011e 100644
--- a/src/ast/continue_statement_test.cc
+++ b/src/ast/continue_statement_test.cc
@@ -41,9 +41,7 @@
 
 TEST_F(ContinueStatementTest, ToStr) {
   auto* stmt = create<ContinueStatement>();
-  std::ostringstream out;
-  stmt->to_str(Sem(), out, 2);
-  EXPECT_EQ(out.str(), R"(  Continue{}
+  EXPECT_EQ(str(stmt), R"(Continue{}
 )");
 }
 
diff --git a/src/ast/discard_statement_test.cc b/src/ast/discard_statement_test.cc
index 43e0771..6f6b3c3 100644
--- a/src/ast/discard_statement_test.cc
+++ b/src/ast/discard_statement_test.cc
@@ -53,9 +53,7 @@
 
 TEST_F(DiscardStatementTest, ToStr) {
   auto* stmt = create<DiscardStatement>();
-  std::ostringstream out;
-  stmt->to_str(Sem(), out, 2);
-  EXPECT_EQ(out.str(), R"(  Discard{}
+  EXPECT_EQ(str(stmt), R"(Discard{}
 )");
 }
 
diff --git a/src/ast/else_statement_test.cc b/src/ast/else_statement_test.cc
index f37fe56..95f3f0d 100644
--- a/src/ast/else_statement_test.cc
+++ b/src/ast/else_statement_test.cc
@@ -114,16 +114,14 @@
       create<DiscardStatement>(),
   });
   auto* e = create<ElseStatement>(cond, body);
-  std::ostringstream out;
-  e->to_str(Sem(), out, 2);
-  EXPECT_EQ(out.str(), R"(  Else{
-    (
-      ScalarConstructor[not set]{true}
-    )
-    {
-      Discard{}
-    }
+  EXPECT_EQ(str(e), R"(Else{
+  (
+    ScalarConstructor[not set]{true}
+  )
+  {
+    Discard{}
   }
+}
 )");
 }
 
@@ -132,13 +130,11 @@
       create<DiscardStatement>(),
   });
   auto* e = create<ElseStatement>(nullptr, body);
-  std::ostringstream out;
-  e->to_str(Sem(), out, 2);
-  EXPECT_EQ(out.str(), R"(  Else{
-    {
-      Discard{}
-    }
+  EXPECT_EQ(str(e), R"(Else{
+  {
+    Discard{}
   }
+}
 )");
 }
 
diff --git a/src/ast/fallthrough_statement_test.cc b/src/ast/fallthrough_statement_test.cc
index 4021bba..8072e1c 100644
--- a/src/ast/fallthrough_statement_test.cc
+++ b/src/ast/fallthrough_statement_test.cc
@@ -49,9 +49,7 @@
 
 TEST_F(FallthroughStatementTest, ToStr) {
   auto* stmt = create<FallthroughStatement>();
-  std::ostringstream out;
-  stmt->to_str(Sem(), out, 2);
-  EXPECT_EQ(out.str(), R"(  Fallthrough{}
+  EXPECT_EQ(str(stmt), R"(Fallthrough{}
 )");
 }
 
diff --git a/src/ast/float_literal_test.cc b/src/ast/float_literal_test.cc
index b329035..802b124 100644
--- a/src/ast/float_literal_test.cc
+++ b/src/ast/float_literal_test.cc
@@ -45,8 +45,7 @@
 
 TEST_F(FloatLiteralTest, ToStr) {
   auto* f = create<FloatLiteral>(ty.f32(), 42.1f);
-
-  EXPECT_EQ(f->to_str(Sem()), "42.099998");
+  EXPECT_EQ(str(f), "42.099998");
 }
 
 TEST_F(FloatLiteralTest, ToName) {
diff --git a/src/ast/function_test.cc b/src/ast/function_test.cc
index 39b6c62..f148bed 100644
--- a/src/ast/function_test.cc
+++ b/src/ast/function_test.cc
@@ -244,13 +244,11 @@
                  },
                  FunctionDecorationList{});
 
-  std::ostringstream out;
-  f->to_str(Sem(), out, 2);
-  EXPECT_EQ(demangle(out.str()), R"(  Function func -> __void
-  ()
-  {
-    Discard{}
-  }
+  EXPECT_EQ(str(f), R"(Function func -> __void
+()
+{
+  Discard{}
+}
 )");
 }
 
@@ -261,14 +259,12 @@
                  },
                  FunctionDecorationList{create<WorkgroupDecoration>(2, 4, 6)});
 
-  std::ostringstream out;
-  f->to_str(Sem(), out, 2);
-  EXPECT_EQ(demangle(out.str()), R"(  Function func -> __void
-  WorkgroupDecoration{2 4 6}
-  ()
-  {
-    Discard{}
-  }
+  EXPECT_EQ(str(f), R"(Function func -> __void
+WorkgroupDecoration{2 4 6}
+()
+{
+  Discard{}
+}
 )");
 }
 
@@ -282,19 +278,17 @@
                  },
                  FunctionDecorationList{});
 
-  std::ostringstream out;
-  f->to_str(Sem(), out, 2);
-  EXPECT_EQ(demangle(out.str()), R"(  Function func -> __void
-  (
-    Variable{
-      var
-      none
-      __i32
-    }
-  )
-  {
-    Discard{}
+  EXPECT_EQ(str(f), R"(Function func -> __void
+(
+  Variable{
+    var
+    none
+    __i32
   }
+)
+{
+  Discard{}
+}
 )");
 }
 
diff --git a/src/ast/group_decoration_test.cc b/src/ast/group_decoration_test.cc
index 5052c85..f600467 100644
--- a/src/ast/group_decoration_test.cc
+++ b/src/ast/group_decoration_test.cc
@@ -39,9 +39,7 @@
 
 TEST_F(GroupDecorationTest, ToStr) {
   auto* d = create<GroupDecoration>(2);
-  std::ostringstream out;
-  d->to_str(Sem(), out, 0);
-  EXPECT_EQ(out.str(), R"(GroupDecoration{2}
+  EXPECT_EQ(str(d), R"(GroupDecoration{2}
 )");
 }
 
diff --git a/src/ast/identifier_expression_test.cc b/src/ast/identifier_expression_test.cc
index 3d504ca..ba9f5ef 100644
--- a/src/ast/identifier_expression_test.cc
+++ b/src/ast/identifier_expression_test.cc
@@ -48,9 +48,7 @@
 
 TEST_F(IdentifierExpressionTest, ToStr) {
   auto* i = Expr("ident");
-  std::ostringstream out;
-  i->to_str(Sem(), out, 2);
-  EXPECT_EQ(demangle(out.str()), R"(  Identifier[not set]{ident}
+  EXPECT_EQ(str(i), R"(Identifier[not set]{ident}
 )");
 }
 
diff --git a/src/ast/if_statement_test.cc b/src/ast/if_statement_test.cc
index d071f7f..03cfda5 100644
--- a/src/ast/if_statement_test.cc
+++ b/src/ast/if_statement_test.cc
@@ -167,16 +167,14 @@
       create<BlockStatement>(StatementList{create<DiscardStatement>()});
   auto* stmt = create<IfStatement>(cond, body, ElseStatementList{});
 
-  std::ostringstream out;
-  stmt->to_str(Sem(), out, 2);
-  EXPECT_EQ(demangle(out.str()), R"(  If{
-    (
-      Identifier[not set]{cond}
-    )
-    {
-      Discard{}
-    }
+  EXPECT_EQ(str(stmt), R"(If{
+  (
+    Identifier[not set]{cond}
+  )
+  {
+    Discard{}
   }
+}
 )");
 }
 
@@ -195,30 +193,28 @@
           create<ElseStatement>(nullptr, else_body),
       });
 
-  std::ostringstream out;
-  stmt->to_str(Sem(), out, 2);
-  EXPECT_EQ(demangle(out.str()), R"(  If{
-    (
-      Identifier[not set]{cond}
-    )
-    {
-      Discard{}
-    }
+  EXPECT_EQ(str(stmt), R"(If{
+  (
+    Identifier[not set]{cond}
+  )
+  {
+    Discard{}
   }
-  Else{
-    (
-      Identifier[not set]{ident}
-    )
-    {
-      Discard{}
-    }
+}
+Else{
+  (
+    Identifier[not set]{ident}
+  )
+  {
+    Discard{}
   }
-  Else{
-    {
-      Discard{}
-      Discard{}
-    }
+}
+Else{
+  {
+    Discard{}
+    Discard{}
   }
+}
 )");
 }
 
diff --git a/src/ast/location_decoration_test.cc b/src/ast/location_decoration_test.cc
index 349e730..fe3bab4 100644
--- a/src/ast/location_decoration_test.cc
+++ b/src/ast/location_decoration_test.cc
@@ -41,9 +41,7 @@
 
 TEST_F(LocationDecorationTest, ToStr) {
   auto* d = create<LocationDecoration>(2);
-  std::ostringstream out;
-  d->to_str(Sem(), out, 0);
-  EXPECT_EQ(out.str(), R"(LocationDecoration{2}
+  EXPECT_EQ(str(d), R"(LocationDecoration{2}
 )");
 }
 
diff --git a/src/ast/loop_statement_test.cc b/src/ast/loop_statement_test.cc
index 0f0b4f4..c971a76 100644
--- a/src/ast/loop_statement_test.cc
+++ b/src/ast/loop_statement_test.cc
@@ -170,11 +170,9 @@
       create<BlockStatement>(StatementList{create<DiscardStatement>()});
 
   auto* l = create<LoopStatement>(body, nullptr);
-  std::ostringstream out;
-  l->to_str(Sem(), out, 2);
-  EXPECT_EQ(out.str(), R"(  Loop{
-    Discard{}
-  }
+  EXPECT_EQ(str(l), R"(Loop{
+  Discard{}
+}
 )");
 }
 
@@ -186,14 +184,12 @@
       create<BlockStatement>(StatementList{create<DiscardStatement>()});
 
   auto* l = create<LoopStatement>(body, continuing);
-  std::ostringstream out;
-  l->to_str(Sem(), out, 2);
-  EXPECT_EQ(out.str(), R"(  Loop{
+  EXPECT_EQ(str(l), R"(Loop{
+  Discard{}
+  continuing {
     Discard{}
-    continuing {
-      Discard{}
-    }
   }
+}
 )");
 }
 
diff --git a/src/ast/member_accessor_expression_test.cc b/src/ast/member_accessor_expression_test.cc
index cc17106..a4cfc80 100644
--- a/src/ast/member_accessor_expression_test.cc
+++ b/src/ast/member_accessor_expression_test.cc
@@ -77,12 +77,10 @@
 TEST_F(MemberAccessorExpressionTest, ToStr) {
   auto* stmt =
       create<MemberAccessorExpression>(Expr("structure"), Expr("member"));
-  std::ostringstream out;
-  stmt->to_str(Sem(), out, 2);
-  EXPECT_EQ(demangle(out.str()), R"(  MemberAccessor[not set]{
-    Identifier[not set]{structure}
-    Identifier[not set]{member}
-  }
+  EXPECT_EQ(str(stmt), R"(MemberAccessor[not set]{
+  Identifier[not set]{structure}
+  Identifier[not set]{member}
+}
 )");
 }
 
diff --git a/src/ast/module_clone_test.cc b/src/ast/module_clone_test.cc
index 8fa8350..e38c937 100644
--- a/src/ast/module_clone_test.cc
+++ b/src/ast/module_clone_test.cc
@@ -17,7 +17,6 @@
 #include "gtest/gtest.h"
 #include "src/ast/case_statement.h"
 #include "src/ast/module.h"
-#include "src/demangler.h"
 #include "src/program.h"
 #include "src/program_builder.h"
 #include "src/reader/wgsl/parser.h"
@@ -121,9 +120,7 @@
   Program dst(src.Clone());
 
   // Expect the AST printed with to_str() to match
-  Demangler demanger;
-  EXPECT_EQ(demanger.Demangle(src.Symbols(), src.AST().to_str(src.Sem())),
-            demanger.Demangle(dst.Symbols(), dst.AST().to_str(dst.Sem())));
+  EXPECT_EQ(src.to_str(), dst.to_str());
 
   // 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 +132,7 @@
     src_types.emplace(src_type);
   }
   for (auto* dst_node : dst.Nodes().Objects()) {
-    ASSERT_EQ(src_nodes.count(dst_node), 0u) << dst_node->str(dst.Sem());
+    ASSERT_EQ(src_nodes.count(dst_node), 0u) << dst.str(dst_node);
   }
   for (auto* dst_type : dst.Types()) {
     ASSERT_EQ(src_types.count(dst_type), 0u) << dst_type->type_name();
diff --git a/src/ast/null_literal_test.cc b/src/ast/null_literal_test.cc
index eac5bb0..bd99d71 100644
--- a/src/ast/null_literal_test.cc
+++ b/src/ast/null_literal_test.cc
@@ -39,8 +39,7 @@
 
 TEST_F(NullLiteralTest, ToStr) {
   auto* i = create<NullLiteral>(ty.i32());
-
-  EXPECT_EQ(i->to_str(Sem()), "null __i32");
+  EXPECT_EQ(str(i), "null __i32");
 }
 
 TEST_F(NullLiteralTest, Name_I32) {
diff --git a/src/ast/return_statement_test.cc b/src/ast/return_statement_test.cc
index d09352a..fec6da6 100644
--- a/src/ast/return_statement_test.cc
+++ b/src/ast/return_statement_test.cc
@@ -75,21 +75,17 @@
 TEST_F(ReturnStatementTest, ToStr_WithValue) {
   auto* expr = Expr("expr");
   auto* r = create<ReturnStatement>(expr);
-  std::ostringstream out;
-  r->to_str(Sem(), out, 2);
-  EXPECT_EQ(demangle(out.str()), R"(  Return{
-    {
-      Identifier[not set]{expr}
-    }
+  EXPECT_EQ(str(r), R"(Return{
+  {
+    Identifier[not set]{expr}
   }
+}
 )");
 }
 
 TEST_F(ReturnStatementTest, ToStr_WithoutValue) {
   auto* r = create<ReturnStatement>();
-  std::ostringstream out;
-  r->to_str(Sem(), out, 2);
-  EXPECT_EQ(out.str(), R"(  Return{}
+  EXPECT_EQ(str(r), R"(Return{}
 )");
 }
 
diff --git a/src/ast/scalar_constructor_expression_test.cc b/src/ast/scalar_constructor_expression_test.cc
index 46fde36..1418b0a 100644
--- a/src/ast/scalar_constructor_expression_test.cc
+++ b/src/ast/scalar_constructor_expression_test.cc
@@ -49,9 +49,7 @@
 
 TEST_F(ScalarConstructorExpressionTest, ToStr) {
   auto* c = Expr(true);
-  std::ostringstream out;
-  c->to_str(Sem(), out, 2);
-  EXPECT_EQ(out.str(), R"(  ScalarConstructor[not set]{true}
+  EXPECT_EQ(str(c), R"(ScalarConstructor[not set]{true}
 )");
 }
 
diff --git a/src/ast/sint_literal_test.cc b/src/ast/sint_literal_test.cc
index a6d39a4..3f9516d 100644
--- a/src/ast/sint_literal_test.cc
+++ b/src/ast/sint_literal_test.cc
@@ -45,8 +45,7 @@
 
 TEST_F(SintLiteralTest, ToStr) {
   auto* i = create<SintLiteral>(ty.i32(), -42);
-
-  EXPECT_EQ(i->to_str(Sem()), "-42");
+  EXPECT_EQ(str(i), "-42");
 }
 
 TEST_F(SintLiteralTest, Name_I32) {
diff --git a/src/ast/stage_decoration_test.cc b/src/ast/stage_decoration_test.cc
index 142f26d..5b0306a 100644
--- a/src/ast/stage_decoration_test.cc
+++ b/src/ast/stage_decoration_test.cc
@@ -38,9 +38,7 @@
 
 TEST_F(StageDecorationTest, ToStr) {
   auto* d = create<StageDecoration>(PipelineStage::kFragment);
-  std::ostringstream out;
-  d->to_str(Sem(), out, 0);
-  EXPECT_EQ(out.str(), R"(StageDecoration{fragment}
+  EXPECT_EQ(str(d), R"(StageDecoration{fragment}
 )");
 }
 
diff --git a/src/ast/struct_member_test.cc b/src/ast/struct_member_test.cc
index 44c2bb9..3c2851b 100644
--- a/src/ast/struct_member_test.cc
+++ b/src/ast/struct_member_test.cc
@@ -74,16 +74,12 @@
 
 TEST_F(StructMemberTest, ToStr) {
   auto* st = Member("a", ty.i32(), {MemberOffset(4)});
-  std::ostringstream out;
-  st->to_str(Sem(), out, 2);
-  EXPECT_EQ(demangle(out.str()), "  StructMember{[[ offset 4 ]] a: __i32}\n");
+  EXPECT_EQ(str(st), "StructMember{[[ offset 4 ]] a: __i32}\n");
 }
 
 TEST_F(StructMemberTest, ToStrNoDecorations) {
   auto* st = Member("a", ty.i32());
-  std::ostringstream out;
-  st->to_str(Sem(), out, 2);
-  EXPECT_EQ(demangle(out.str()), "  StructMember{a: __i32}\n");
+  EXPECT_EQ(str(st), "StructMember{a: __i32}\n");
 }
 
 }  // namespace
diff --git a/src/ast/struct_test.cc b/src/ast/struct_test.cc
index 103c22b..8d5cc9f 100644
--- a/src/ast/struct_test.cc
+++ b/src/ast/struct_test.cc
@@ -92,12 +92,10 @@
   decos.push_back(create<StructBlockDecoration>());
   auto* s = create<Struct>(StructMemberList{Member("a", ty.i32())}, decos);
 
-  std::ostringstream out;
-  s->to_str(Sem(), out, 2);
-  EXPECT_EQ(demangle(out.str()), R"(Struct{
-    [[block]]
-    StructMember{a: __i32}
-  }
+  EXPECT_EQ(str(s), R"(Struct{
+  [[block]]
+  StructMember{a: __i32}
+}
 )");
 }
 
diff --git a/src/ast/switch_statement_test.cc b/src/ast/switch_statement_test.cc
index 86d0b90..cf455f0 100644
--- a/src/ast/switch_statement_test.cc
+++ b/src/ast/switch_statement_test.cc
@@ -136,13 +136,11 @@
   auto* ident = Expr("ident");
 
   auto* stmt = create<SwitchStatement>(ident, CaseStatementList{});
-  std::ostringstream out;
-  stmt->to_str(Sem(), out, 2);
-  EXPECT_EQ(demangle(out.str()), R"(  Switch{
-    Identifier[not set]{ident}
-    {
-    }
+  EXPECT_EQ(str(stmt), R"(Switch{
+  Identifier[not set]{ident}
+  {
   }
+}
 )");
 }
 
@@ -156,15 +154,13 @@
       create<CaseStatement>(lit, create<BlockStatement>(StatementList{})));
 
   auto* stmt = create<SwitchStatement>(ident, body);
-  std::ostringstream out;
-  stmt->to_str(Sem(), out, 2);
-  EXPECT_EQ(demangle(out.str()), R"(  Switch{
-    Identifier[not set]{ident}
-    {
-      Case 2{
-      }
+  EXPECT_EQ(str(stmt), R"(Switch{
+  Identifier[not set]{ident}
+  {
+    Case 2{
     }
   }
+}
 )");
 }
 
diff --git a/src/ast/test_helper.h b/src/ast/test_helper.h
index 5621a32..ba07295 100644
--- a/src/ast/test_helper.h
+++ b/src/ast/test_helper.h
@@ -20,7 +20,6 @@
 #include <utility>
 
 #include "gtest/gtest.h"
-#include "src/demangler.h"
 #include "src/program_builder.h"
 
 namespace tint {
@@ -28,18 +27,7 @@
 
 /// Helper class for testing
 template <typename BASE>
-class TestHelperBase : public BASE, public ProgramBuilder {
- public:
-  /// Demangles the given string
-  /// @param s the string to demangle
-  /// @returns the demangled string
-  std::string demangle(const std::string& s) {
-    return demanger.Demangle(Symbols(), s);
-  }
-
-  /// A demangler
-  Demangler demanger;
-};
+class TestHelperBase : public BASE, public ProgramBuilder {};
 using TestHelper = TestHelperBase<testing::Test>;
 
 template <typename T>
diff --git a/src/ast/type_constructor_expression_test.cc b/src/ast/type_constructor_expression_test.cc
index e4421a4..c09ea2f 100644
--- a/src/ast/type_constructor_expression_test.cc
+++ b/src/ast/type_constructor_expression_test.cc
@@ -106,14 +106,12 @@
   expr.push_back(Expr("expr_3"));
 
   auto* t = create<TypeConstructorExpression>(&vec, expr);
-  std::ostringstream out;
-  t->to_str(Sem(), out, 2);
-  EXPECT_EQ(demangle(out.str()), R"(  TypeConstructor[not set]{
-    __vec_3__f32
-    Identifier[not set]{expr_1}
-    Identifier[not set]{expr_2}
-    Identifier[not set]{expr_3}
-  }
+  EXPECT_EQ(str(t), R"(TypeConstructor[not set]{
+  __vec_3__f32
+  Identifier[not set]{expr_1}
+  Identifier[not set]{expr_2}
+  Identifier[not set]{expr_3}
+}
 )");
 }
 
diff --git a/src/ast/uint_literal_test.cc b/src/ast/uint_literal_test.cc
index 6628269..3da63b8 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(Sem()), "42");
+  EXPECT_EQ(str(u), "42");
 }
 
 }  // namespace
diff --git a/src/ast/unary_op_expression_test.cc b/src/ast/unary_op_expression_test.cc
index 6abc1bb..fbd24ad 100644
--- a/src/ast/unary_op_expression_test.cc
+++ b/src/ast/unary_op_expression_test.cc
@@ -68,12 +68,10 @@
 TEST_F(UnaryOpExpressionTest, ToStr) {
   auto* ident = Expr("ident");
   auto* u = create<UnaryOpExpression>(UnaryOp::kNot, ident);
-  std::ostringstream out;
-  u->to_str(Sem(), out, 2);
-  EXPECT_EQ(demangle(out.str()), R"(  UnaryOp[not set]{
-    not
-    Identifier[not set]{ident}
-  }
+  EXPECT_EQ(str(u), R"(UnaryOp[not set]{
+  not
+  Identifier[not set]{ident}
+}
 )");
 }
 
diff --git a/src/ast/variable_decl_statement_test.cc b/src/ast/variable_decl_statement_test.cc
index 6524c0e..bf18be5 100644
--- a/src/ast/variable_decl_statement_test.cc
+++ b/src/ast/variable_decl_statement_test.cc
@@ -70,15 +70,13 @@
 
   auto* stmt =
       create<VariableDeclStatement>(Source{Source::Location{20, 2}}, var);
-  std::ostringstream out;
-  stmt->to_str(Sem(), out, 2);
-  EXPECT_EQ(demangle(out.str()), R"(  VariableDeclStatement{
-    Variable{
-      a
-      none
-      __f32
-    }
+  EXPECT_EQ(str(stmt), R"(VariableDeclStatement{
+  Variable{
+    a
+    none
+    __f32
   }
+}
 )");
 }
 
diff --git a/src/ast/variable_test.cc b/src/ast/variable_test.cc
index 15a930f..f700381 100644
--- a/src/ast/variable_test.cc
+++ b/src/ast/variable_test.cc
@@ -102,13 +102,11 @@
 TEST_F(VariableTest, to_str) {
   auto* v = Var("my_var", StorageClass::kFunction, ty.f32(), nullptr,
                 ast::VariableDecorationList{});
-  std::ostringstream out;
-  v->to_str(Sem(), out, 2);
-  EXPECT_EQ(demangle(out.str()), R"(  Variable{
-    my_var
-    function
-    __f32
-  }
+  EXPECT_EQ(str(v), R"(Variable{
+  my_var
+  function
+  __f32
+}
 )");
 }
 
@@ -145,20 +143,18 @@
                       create<GroupDecoration>(1),
                   });
 
-  std::ostringstream out;
-  var->to_str(Sem(), out, 2);
-  EXPECT_EQ(demangle(out.str()), R"(  Variable{
-    Decorations{
-      BindingDecoration{2}
-      GroupDecoration{1}
-    }
-    my_var
-    function
-    __f32
-    {
-      Identifier[not set]{expr}
-    }
+  EXPECT_EQ(str(var), R"(Variable{
+  Decorations{
+    BindingDecoration{2}
+    GroupDecoration{1}
   }
+  my_var
+  function
+  __f32
+  {
+    Identifier[not set]{expr}
+  }
+}
 )");
 }
 
diff --git a/src/ast/workgroup_decoration_test.cc b/src/ast/workgroup_decoration_test.cc
index df28db1..f45a0ba 100644
--- a/src/ast/workgroup_decoration_test.cc
+++ b/src/ast/workgroup_decoration_test.cc
@@ -65,9 +65,7 @@
 
 TEST_F(WorkgroupDecorationTest, ToStr) {
   auto* d = create<WorkgroupDecoration>(2, 4, 6);
-  std::ostringstream out;
-  d->to_str(Sem(), out, 0);
-  EXPECT_EQ(out.str(), R"(WorkgroupDecoration{2 4 6}
+  EXPECT_EQ(str(d), R"(WorkgroupDecoration{2 4 6}
 )");
 }
 
diff --git a/src/demangler.cc b/src/demangler.cc
index cdfcaa5..54d316e 100644
--- a/src/demangler.cc
+++ b/src/demangler.cc
@@ -58,8 +58,4 @@
   return ret;
 }
 
-std::string Demangler::Demangle(const Program& program) const {
-  return Demangle(program.Symbols(), program.AST().to_str(program.Sem()));
-}
-
 }  // namespace tint
diff --git a/src/demangler.h b/src/demangler.h
index f5ecb03..6f28e93 100644
--- a/src/demangler.h
+++ b/src/demangler.h
@@ -19,7 +19,6 @@
 
 namespace tint {
 
-class Program;
 class SymbolTable;
 
 /// Helper to demangle strings and replace symbols with original names
@@ -36,12 +35,6 @@
   /// @returns the string with any symbol replacements performed.
   std::string Demangle(const SymbolTable& symbols,
                        const std::string& str) const;
-
-  /// Returns the string returned by the `program.AST().to_str()` of the
-  /// program with all symbols replaced with their original names.
-  /// @param program the program where the symbols are registered
-  /// @returns the string with any symbol replacements performed.
-  std::string Demangle(const Program& program) const;
 };
 
 }  // namespace tint
diff --git a/src/program.cc b/src/program.cc
index 39bf5a0..36df9fd 100644
--- a/src/program.cc
+++ b/src/program.cc
@@ -19,6 +19,7 @@
 
 #include "src/ast/module.h"
 #include "src/clone_context.h"
+#include "src/demangler.h"
 #include "src/program_builder.h"
 #include "src/type_determiner.h"
 
@@ -98,9 +99,17 @@
   return is_valid_;
 }
 
-std::string Program::to_str() const {
+std::string Program::to_str(bool demangle) const {
   AssertNotMoved();
-  return ast_->to_str(Sem());
+  auto str = ast_->to_str(Sem());
+  if (demangle) {
+    str = Demangler().Demangle(Symbols(), str);
+  }
+  return str;
+}
+
+std::string Program::str(const ast::Node* node) const {
+  return Demangler().Demangle(Symbols(), node->str(Sem()));
 }
 
 void Program::AssertNotMoved() const {
diff --git a/src/program.h b/src/program.h
index 0e624e7..224db0f 100644
--- a/src/program.h
+++ b/src/program.h
@@ -105,8 +105,27 @@
   /// information
   bool IsValid() const;
 
+  /// @param demangle whether to automatically demangle the symbols in the
+  /// returned string
   /// @returns a string describing this program.
-  std::string to_str() const;
+  std::string to_str(bool demangle) const;
+
+  /// @returns a demangled string describing this program.
+  std::string to_str() const { return to_str(true); }
+
+  /// Writes a representation of the node to the output stream
+  /// @note unlike str(), to_str() does not automatically demangle the string.
+  /// @param node the AST node
+  /// @param out the stream to write to
+  /// @param indent number of spaces to indent the node when writing
+  void to_str(const ast::Node* node, std::ostream& out, size_t indent) const {
+    node->to_str(Sem(), out, indent);
+  }
+
+  /// Returns a demangled, string representation of `node`.
+  /// @param node the AST node
+  /// @returns a string representation of the node
+  std::string str(const ast::Node* node) const;
 
  private:
   Program(const Program&) = delete;
diff --git a/src/program_builder.cc b/src/program_builder.cc
index 5f21743..d4d08fe 100644
--- a/src/program_builder.cc
+++ b/src/program_builder.cc
@@ -19,6 +19,7 @@
 #include <sstream>
 
 #include "src/clone_context.h"
+#include "src/demangler.h"
 #include "src/type/struct_type.h"
 
 namespace tint {
@@ -54,6 +55,10 @@
   return !diagnostics_.contains_errors() && ast_->IsValid();
 }
 
+std::string ProgramBuilder::str(const ast::Node* node) const {
+  return Demangler().Demangle(Symbols(), node->str(Sem()));
+}
+
 void ProgramBuilder::MarkAsMoved() {
   AssertNotMoved();
   moved_ = true;
diff --git a/src/program_builder.h b/src/program_builder.h
index 582717d..e507137 100644
--- a/src/program_builder.h
+++ b/src/program_builder.h
@@ -150,6 +150,12 @@
     return symbols_;
   }
 
+  /// @returns a reference to the program's SymbolTable
+  const SymbolTable& Symbols() const {
+    AssertNotMoved();
+    return symbols_;
+  }
+
   /// @returns a reference to the program's diagnostics
   diag::List& Diagnostics() {
     AssertNotMoved();
@@ -175,6 +181,20 @@
   /// information
   bool IsValid() const;
 
+  /// Writes a representation of the node to the output stream
+  /// @note unlike str(), to_str() does not automatically demangle the string.
+  /// @param node the AST node
+  /// @param out the stream to write to
+  /// @param indent number of spaces to indent the node when writing
+  void to_str(const ast::Node* node, std::ostream& out, size_t indent) const {
+    node->to_str(Sem(), out, indent);
+  }
+
+  /// Returns a demangled, string representation of `node`.
+  /// @param node the AST node
+  /// @returns a string representation of the node
+  std::string str(const ast::Node* node) const;
+
   /// creates a new ast::Node owned by the Module. When the Module is
   /// destructed, the ast::Node will also be destructed.
   /// @param source the Source of the node
diff --git a/src/reader/spirv/function_call_test.cc b/src/reader/spirv/function_call_test.cc
index 0184d16..9b97b1e 100644
--- a/src/reader/spirv/function_call_test.cc
+++ b/src/reader/spirv/function_call_test.cc
@@ -46,7 +46,7 @@
      OpFunctionEnd
   )"));
   ASSERT_TRUE(p->BuildAndParseInternalModule()) << p->error();
-  const auto got = p->program().to_str();
+  const auto got = p->program().to_str(false);
   const char* expect = R"(Module{
   Function tint_symbol_1 -> __void
   ()
@@ -217,7 +217,7 @@
   )"));
   ASSERT_TRUE(p->BuildAndParseInternalModule()) << p->error();
   EXPECT_TRUE(p->error().empty());
-  const auto program_ast_str = Demangler().Demangle(p->program());
+  const auto program_ast_str = p->program().to_str();
   EXPECT_THAT(program_ast_str, HasSubstr(R"(Module{
   Function x_50 -> __u32
   (
diff --git a/src/reader/spirv/function_decl_test.cc b/src/reader/spirv/function_decl_test.cc
index 0af7a00..f7f5d61 100644
--- a/src/reader/spirv/function_decl_test.cc
+++ b/src/reader/spirv/function_decl_test.cc
@@ -59,7 +59,7 @@
   ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions());
   FunctionEmitter fe(p.get(), *spirv_function(p.get(), 100));
   EXPECT_TRUE(fe.Emit());
-  auto got = Demangler().Demangle(p->program());
+  auto got = p->program().to_str();
   std::string expect = R"(Module{
   Function x_100 -> __void
   ()
@@ -83,7 +83,7 @@
   FunctionEmitter fe(p.get(), *spirv_function(p.get(), 100));
   EXPECT_TRUE(fe.Emit());
 
-  auto got = Demangler().Demangle(p->program());
+  auto got = p->program().to_str();
   std::string expect = R"(Module{
   Function x_100 -> __f32
   ()
@@ -115,7 +115,7 @@
   FunctionEmitter fe(p.get(), *spirv_function(p.get(), 100));
   EXPECT_TRUE(fe.Emit());
 
-  auto got = Demangler().Demangle(p->program());
+  auto got = p->program().to_str();
   std::string expect = R"(Module{
   Function x_100 -> __void
   (
@@ -159,7 +159,7 @@
   FunctionEmitter fe(p.get(), *spirv_function(p.get(), 100));
   EXPECT_TRUE(fe.Emit());
 
-  auto got = Demangler().Demangle(p->program());
+  auto got = p->program().to_str();
   std::string expect = R"(Module{
   Function x_100 -> __void
   (
diff --git a/src/reader/spirv/function_memory_test.cc b/src/reader/spirv/function_memory_test.cc
index 89cfe2b..b42cb4b 100644
--- a/src/reader/spirv/function_memory_test.cc
+++ b/src/reader/spirv/function_memory_test.cc
@@ -800,7 +800,7 @@
   auto p = parser(test::Assemble(assembly));
   ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions())
       << assembly << p->error();
-  const auto module_str = Demangler().Demangle(p->program());
+  const auto module_str = p->program().to_str();
   EXPECT_THAT(module_str, HasSubstr(R"(
   RTArr -> __array__u32_stride_4
   S Struct{
diff --git a/src/reader/spirv/function_misc_test.cc b/src/reader/spirv/function_misc_test.cc
index 809cf38..4971866 100644
--- a/src/reader/spirv/function_misc_test.cc
+++ b/src/reader/spirv/function_misc_test.cc
@@ -328,14 +328,12 @@
     Program program(p->program());
     EXPECT_TRUE(fe.success());
     ASSERT_NE(result, nullptr);
-    std::ostringstream ss;
-    result->to_str(program.Sem(), ss, 0);
-    auto str = Demangler().Demangle(program.Symbols(), ss.str());
-    EXPECT_THAT(str, Eq(GetParam().expected_expr));
+    auto got = program.str(result);
+    EXPECT_EQ(got, GetParam().expected_expr);
   } else {
     EXPECT_EQ(result, nullptr);
     EXPECT_FALSE(fe.success());
-    EXPECT_THAT(p->error(), Eq(GetParam().expected_error));
+    EXPECT_EQ(p->error(), GetParam().expected_error);
   }
 }
 
diff --git a/src/reader/spirv/parser_impl.cc b/src/reader/spirv/parser_impl.cc
index 3173fd6..8676836 100644
--- a/src/reader/spirv/parser_impl.cc
+++ b/src/reader/spirv/parser_impl.cc
@@ -1504,8 +1504,8 @@
   }
   auto* type = expr.type;
   if (!type) {
-    Fail() << "internal error: unmapped type for: "
-           << expr.expr->str(builder_.Sem()) << "\n";
+    Fail() << "internal error: unmapped type for: " << builder_.str(expr.expr)
+           << "\n";
     return {};
   }
   if (requires_unsigned) {
diff --git a/src/reader/spirv/parser_impl_convert_type_test.cc b/src/reader/spirv/parser_impl_convert_type_test.cc
index 02dead2..cdabeb2 100644
--- a/src/reader/spirv/parser_impl_convert_type_test.cc
+++ b/src/reader/spirv/parser_impl_convert_type_test.cc
@@ -566,10 +566,7 @@
   EXPECT_TRUE(type->Is<type::Struct>());
 
   Program program = p->program();
-
-  std::stringstream ss;
-  type->As<type::Struct>()->impl()->to_str(program.Sem(), ss, 0);
-  EXPECT_THAT(Demangler().Demangle(program.Symbols(), ss.str()), Eq(R"(Struct{
+  EXPECT_THAT(program.str(type->As<type::Struct>()->impl()), Eq(R"(Struct{
   StructMember{field0: __u32}
   StructMember{field1: __f32}
 }
@@ -590,10 +587,7 @@
   EXPECT_TRUE(type->Is<type::Struct>());
 
   Program program = p->program();
-
-  std::stringstream ss;
-  type->As<type::Struct>()->impl()->to_str(program.Sem(), ss, 0);
-  EXPECT_THAT(Demangler().Demangle(program.Symbols(), ss.str()), Eq(R"(Struct{
+  EXPECT_THAT(program.str(type->As<type::Struct>()->impl()), Eq(R"(Struct{
   [[block]]
   StructMember{field0: __u32}
 }
@@ -618,10 +612,7 @@
   EXPECT_TRUE(type->Is<type::Struct>());
 
   Program program = p->program();
-
-  std::stringstream ss;
-  type->As<type::Struct>()->impl()->to_str(program.Sem(), ss, 0);
-  EXPECT_THAT(Demangler().Demangle(program.Symbols(), ss.str()), Eq(R"(Struct{
+  EXPECT_THAT(program.str(type->As<type::Struct>()->impl()), Eq(R"(Struct{
   StructMember{[[ offset 0 ]] field0: __f32}
   StructMember{[[ offset 8 ]] field1: __vec_2__f32}
   StructMember{[[ offset 16 ]] field2: __mat_2_2__f32}
diff --git a/src/reader/spirv/parser_impl_function_decl_test.cc b/src/reader/spirv/parser_impl_function_decl_test.cc
index 80c47be..d0320af 100644
--- a/src/reader/spirv/parser_impl_function_decl_test.cc
+++ b/src/reader/spirv/parser_impl_function_decl_test.cc
@@ -55,7 +55,7 @@
   EXPECT_TRUE(p->BuildAndParseInternalModule());
   EXPECT_TRUE(p->error().empty());
   Program program = p->program();
-  const auto program_ast = program.to_str();
+  const auto program_ast = program.to_str(false);
   EXPECT_THAT(program_ast, Not(HasSubstr("Function{")));
 }
 
@@ -67,7 +67,7 @@
   EXPECT_TRUE(p->BuildAndParseInternalModule());
   EXPECT_TRUE(p->error().empty());
   Program program = p->program();
-  const auto program_ast = program.to_str();
+  const auto program_ast = program.to_str(false);
   EXPECT_THAT(program_ast, Not(HasSubstr("Function{")));
 }
 
@@ -83,7 +83,7 @@
   ASSERT_TRUE(p->BuildAndParseInternalModule());
   ASSERT_TRUE(p->error().empty()) << p->error();
   Program program = p->program();
-  const auto program_ast = program.to_str();
+  const auto program_ast = program.to_str(false);
   EXPECT_THAT(program_ast, HasSubstr(R"(
   Function )" + program.Symbols().Get("main").to_str() +
                                      R"( -> __void
@@ -104,7 +104,7 @@
   ASSERT_TRUE(p->BuildAndParseInternalModule());
   ASSERT_TRUE(p->error().empty()) << p->error();
   Program program = p->program();
-  const auto program_ast = program.to_str();
+  const auto program_ast = program.to_str(false);
   EXPECT_THAT(program_ast, HasSubstr(R"(
   Function )" + program.Symbols().Get("main").to_str() +
                                      R"( -> __void
@@ -125,7 +125,7 @@
   ASSERT_TRUE(p->BuildAndParseInternalModule());
   ASSERT_TRUE(p->error().empty()) << p->error();
   Program program = p->program();
-  const auto program_ast = program.to_str();
+  const auto program_ast = program.to_str(false);
   EXPECT_THAT(program_ast, HasSubstr(R"(
   Function )" + program.Symbols().Get("main").to_str() +
                                      R"( -> __void
@@ -148,7 +148,7 @@
   ASSERT_TRUE(p->BuildAndParseInternalModule());
   ASSERT_TRUE(p->error().empty()) << p->error();
   Program program = p->program();
-  const auto program_ast = program.to_str();
+  const auto program_ast = program.to_str(false);
   EXPECT_THAT(program_ast, HasSubstr(R"(
   Function )" + program.Symbols().Get("frag_main").to_str() +
                                      R"( -> __void
@@ -173,7 +173,7 @@
   EXPECT_TRUE(p->BuildAndParseInternalModule());
   EXPECT_TRUE(p->error().empty());
   Program program = p->program();
-  const auto program_ast = program.to_str();
+  const auto program_ast = program.to_str(false);
   EXPECT_THAT(program_ast, HasSubstr(R"(
   Function )" + program.Symbols().Get("main").to_str() +
                                      R"( -> __void
@@ -208,7 +208,7 @@
   EXPECT_TRUE(p->BuildAndParseInternalModule());
   EXPECT_TRUE(p->error().empty());
   Program program = p->program();
-  const auto program_ast = Demangler().Demangle(program);
+  const auto program_ast = program.to_str();
   EXPECT_THAT(program_ast, HasSubstr(R"(
   Function leaf -> __u32
   ()
@@ -276,7 +276,7 @@
   EXPECT_TRUE(p->BuildAndParseInternalModule());
   EXPECT_TRUE(p->error().empty());
   Program program = p->program();
-  const auto program_ast = Demangler().Demangle(program);
+  const auto program_ast = program.to_str();
   EXPECT_THAT(program_ast, HasSubstr(R"(
   Function ret_float -> __f32
   ()
@@ -306,7 +306,7 @@
   EXPECT_TRUE(p->BuildAndParseInternalModule());
   EXPECT_TRUE(p->error().empty());
   Program program = p->program();
-  const auto program_ast = Demangler().Demangle(program);
+  const auto program_ast = program.to_str();
   EXPECT_THAT(program_ast, HasSubstr(R"(
   Function mixed_params -> __void
   (
@@ -346,7 +346,7 @@
   EXPECT_TRUE(p->BuildAndParseInternalModule());
   EXPECT_TRUE(p->error().empty());
   Program program = p->program();
-  const auto program_ast = Demangler().Demangle(program);
+  const auto program_ast = program.to_str();
   EXPECT_THAT(program_ast, HasSubstr(R"(
   Function mixed_params -> __void
   (
diff --git a/src/reader/spirv/parser_impl_handle_test.cc b/src/reader/spirv/parser_impl_handle_test.cc
index 78422fd..4a226ac 100644
--- a/src/reader/spirv/parser_impl_handle_test.cc
+++ b/src/reader/spirv/parser_impl_handle_test.cc
@@ -1129,7 +1129,7 @@
   auto p = parser(test::Assemble(assembly));
   ASSERT_TRUE(p->BuildAndParseInternalModule()) << p->error() << assembly;
   EXPECT_TRUE(p->error().empty()) << p->error();
-  const auto program = Demangler().Demangle(p->program());
+  const auto program = p->program().to_str();
   EXPECT_THAT(program, HasSubstr(GetParam().var_decl)) << program;
 }
 
@@ -1305,7 +1305,7 @@
   auto p = parser(test::Assemble(assembly));
   ASSERT_TRUE(p->BuildAndParseInternalModule()) << p->error() << assembly;
   EXPECT_TRUE(p->error().empty()) << p->error();
-  const auto program = Demangler().Demangle(p->program());
+  const auto program = p->program().to_str();
   EXPECT_THAT(program, HasSubstr(GetParam().var_decl))
       << "DECLARATIONS ARE BAD " << program;
   EXPECT_THAT(program, HasSubstr(GetParam().texture_builtin))
@@ -2554,7 +2554,7 @@
   auto p = parser(test::Assemble(assembly));
   ASSERT_TRUE(p->BuildAndParseInternalModule()) << p->error() << assembly;
   EXPECT_TRUE(p->error().empty()) << p->error();
-  const auto program = Demangler().Demangle(p->program());
+  const auto program = p->program().to_str();
   EXPECT_THAT(program, HasSubstr(GetParam().var_decl))
       << "DECLARATIONS ARE BAD " << program;
   EXPECT_THAT(program, HasSubstr(GetParam().texture_builtin))
@@ -3787,8 +3787,7 @@
       Program program = p->program();
       for (auto* expr : result) {
         ASSERT_NE(expr, nullptr);
-        result_strings.push_back(
-            Demangler().Demangle(program.Symbols(), expr->str(program.Sem())));
+        result_strings.push_back(program.str(expr));
       }
       EXPECT_THAT(result_strings,
                   ::testing::ContainerEq(GetParam().expected_expressions));
diff --git a/src/reader/spirv/parser_impl_module_var_test.cc b/src/reader/spirv/parser_impl_module_var_test.cc
index 1acf2d1..bf5a42a 100644
--- a/src/reader/spirv/parser_impl_module_var_test.cc
+++ b/src/reader/spirv/parser_impl_module_var_test.cc
@@ -144,7 +144,7 @@
 
   EXPECT_TRUE(p->BuildAndParseInternalModule());
   EXPECT_TRUE(p->error().empty());
-  const auto module_str = Demangler().Demangle(p->program());
+  const auto module_str = p->program().to_str();
   EXPECT_THAT(module_str, HasSubstr(R"(
   Variable{
     x_52
@@ -163,7 +163,7 @@
 
   EXPECT_TRUE(p->BuildAndParseInternalModule());
   EXPECT_TRUE(p->error().empty());
-  const auto module_str = Demangler().Demangle(p->program());
+  const auto module_str = p->program().to_str();
   EXPECT_THAT(module_str, HasSubstr(R"(
   Variable{
     the_counter
@@ -182,7 +182,7 @@
 
   EXPECT_TRUE(p->BuildAndParseInternalModule());
   EXPECT_TRUE(p->error().empty());
-  const auto module_str = Demangler().Demangle(p->program());
+  const auto module_str = p->program().to_str();
   EXPECT_THAT(module_str, HasSubstr(R"(
   Variable{
     my_own_private_idaho
@@ -201,7 +201,7 @@
 
   EXPECT_TRUE(p->BuildAndParseInternalModule());
   EXPECT_TRUE(p->error().empty());
-  const auto module_str = Demangler().Demangle(p->program());
+  const auto module_str = p->program().to_str();
   EXPECT_THAT(module_str, HasSubstr(R"(
   Variable{
     Decorations{
@@ -251,7 +251,7 @@
   EXPECT_EQ(position_info.pointer_type_id, 11u);
   EXPECT_EQ(position_info.storage_class, SpvStorageClassOutput);
   EXPECT_EQ(position_info.per_vertex_var_id, 1u);
-  const auto module_str = Demangler().Demangle(p->program());
+  const auto module_str = p->program().to_str();
   EXPECT_THAT(module_str, HasSubstr(R"(
   Variable{
     Decorations{
@@ -331,7 +331,7 @@
   auto p = parser(test::Assemble(assembly));
   EXPECT_TRUE(p->BuildAndParseInternalModule());
   EXPECT_TRUE(p->error().empty());
-  const auto module_str = Demangler().Demangle(p->program());
+  const auto module_str = p->program().to_str();
   EXPECT_THAT(module_str, HasSubstr(R"(
     Assignment{
       Identifier[not set]{gl_Position}
@@ -384,7 +384,7 @@
   auto p = parser(test::Assemble(assembly));
   EXPECT_TRUE(p->BuildAndParseInternalModule());
   EXPECT_TRUE(p->error().empty());
-  const auto module_str = Demangler().Demangle(p->program());
+  const auto module_str = p->program().to_str();
   EXPECT_THAT(module_str, HasSubstr(R"(
     Assignment{
       Identifier[not set]{gl_Position}
@@ -415,7 +415,7 @@
   auto p = parser(test::Assemble(assembly));
   EXPECT_TRUE(p->BuildAndParseInternalModule());
   EXPECT_TRUE(p->error().empty());
-  const auto module_str = Demangler().Demangle(p->program());
+  const auto module_str = p->program().to_str();
   EXPECT_THAT(module_str, HasSubstr(R"(
     Assignment{
       MemberAccessor[not set]{
@@ -446,7 +446,7 @@
   auto p = parser(test::Assemble(assembly));
   EXPECT_TRUE(p->BuildAndParseInternalModule());
   EXPECT_TRUE(p->error().empty());
-  const auto module_str = Demangler().Demangle(p->program());
+  const auto module_str = p->program().to_str();
   EXPECT_THAT(module_str, HasSubstr(R"(
   {
     Assignment{
@@ -476,7 +476,7 @@
   auto p = parser(test::Assemble(assembly));
   EXPECT_TRUE(p->BuildAndParseInternalModule());
   EXPECT_TRUE(p->error().empty());
-  const auto module_str = Demangler().Demangle(p->program());
+  const auto module_str = p->program().to_str();
   EXPECT_EQ(module_str, R"(Module{
   Variable{
     Decorations{
@@ -532,7 +532,7 @@
   auto p = parser(test::Assemble(assembly));
   EXPECT_TRUE(p->BuildAndParseInternalModule());
   EXPECT_TRUE(p->error().empty());
-  const auto module_str = Demangler().Demangle(p->program());
+  const auto module_str = p->program().to_str();
   EXPECT_EQ(module_str, R"(Module{
   Variable{
     x_900
@@ -599,7 +599,7 @@
   auto p = parser(test::Assemble(assembly));
   EXPECT_TRUE(p->BuildAndParseInternalModule()) << p->error();
   EXPECT_TRUE(p->error().empty());
-  const auto module_str = Demangler().Demangle(p->program());
+  const auto module_str = p->program().to_str();
   EXPECT_EQ(module_str, R"(Module{
   Variable{
     Decorations{
@@ -650,7 +650,7 @@
   auto p = parser(test::Assemble(assembly));
   EXPECT_TRUE(p->BuildAndParseInternalModule());
   EXPECT_TRUE(p->error().empty());
-  const auto module_str = Demangler().Demangle(p->program());
+  const auto module_str = p->program().to_str();
   EXPECT_EQ(module_str, R"(Module{
   Function x_500 -> __void
   ()
@@ -694,7 +694,7 @@
   auto p = parser(test::Assemble(assembly));
   EXPECT_TRUE(p->BuildAndParseInternalModule());
   EXPECT_TRUE(p->error().empty());
-  const auto module_str = Demangler().Demangle(p->program());
+  const auto module_str = p->program().to_str();
   EXPECT_EQ(module_str, R"(Module{
   Variable{
     x_900
@@ -730,7 +730,7 @@
   auto p = parser(test::Assemble(assembly));
   EXPECT_TRUE(p->BuildAndParseInternalModule()) << p->error();
   EXPECT_TRUE(p->error().empty());
-  const auto module_str = Demangler().Demangle(p->program());
+  const auto module_str = p->program().to_str();
   EXPECT_EQ(module_str, R"(Module{
   Function x_500 -> __void
   ()
@@ -757,7 +757,7 @@
   auto p = parser(test::Assemble(assembly));
   EXPECT_TRUE(p->BuildAndParseInternalModule()) << p->error();
   EXPECT_TRUE(p->error().empty()) << p->error();
-  const auto module_str = Demangler().Demangle(p->program());
+  const auto module_str = p->program().to_str();
   EXPECT_EQ(module_str, R"(Module{
   Function x_500 -> __void
   ()
@@ -861,7 +861,7 @@
   )"));
   ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions());
   EXPECT_TRUE(p->error().empty());
-  const auto module_str = Demangler().Demangle(p->program());
+  const auto module_str = p->program().to_str();
   EXPECT_THAT(module_str, HasSubstr(R"(Variable{
     x_1
     private
@@ -918,7 +918,7 @@
   )"));
   ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions());
   EXPECT_TRUE(p->error().empty());
-  const auto module_str = Demangler().Demangle(p->program());
+  const auto module_str = p->program().to_str();
   EXPECT_THAT(module_str, HasSubstr(R"(Variable{
     x_1
     private
@@ -967,7 +967,7 @@
   )"));
   ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error();
   EXPECT_TRUE(p->error().empty());
-  const auto module_str = Demangler().Demangle(p->program());
+  const auto module_str = p->program().to_str();
   EXPECT_THAT(module_str, HasSubstr(R"(Variable{
     x_1
     private
@@ -1011,7 +1011,7 @@
   )"));
   ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions());
   EXPECT_TRUE(p->error().empty());
-  const auto module_str = Demangler().Demangle(p->program());
+  const auto module_str = p->program().to_str();
   EXPECT_THAT(module_str, HasSubstr(R"(Variable{
     x_200
     private
@@ -1034,7 +1034,7 @@
   )"));
   ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions());
   EXPECT_TRUE(p->error().empty());
-  const auto module_str = Demangler().Demangle(p->program());
+  const auto module_str = p->program().to_str();
   EXPECT_THAT(module_str, HasSubstr(R"(Variable{
     x_200
     private
@@ -1057,7 +1057,7 @@
   )"));
   ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions());
   EXPECT_TRUE(p->error().empty());
-  const auto module_str = Demangler().Demangle(p->program());
+  const auto module_str = p->program().to_str();
   EXPECT_THAT(module_str, HasSubstr(R"(Variable{
     x_200
     private
@@ -1080,7 +1080,7 @@
   )"));
   ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions());
   EXPECT_TRUE(p->error().empty());
-  const auto module_str = Demangler().Demangle(p->program());
+  const auto module_str = p->program().to_str();
   EXPECT_THAT(module_str, HasSubstr(R"(Variable{
     x_200
     private
@@ -1103,7 +1103,7 @@
   )"));
   ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions());
   EXPECT_TRUE(p->error().empty());
-  const auto module_str = Demangler().Demangle(p->program());
+  const auto module_str = p->program().to_str();
   EXPECT_THAT(module_str, HasSubstr(R"(Variable{
     x_200
     private
@@ -1126,7 +1126,7 @@
   )"));
   ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions());
   EXPECT_TRUE(p->error().empty());
-  const auto module_str = Demangler().Demangle(p->program());
+  const auto module_str = p->program().to_str();
   EXPECT_THAT(module_str, HasSubstr(R"(Variable{
     x_200
     private
@@ -1149,7 +1149,7 @@
   )"));
   ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions());
   EXPECT_TRUE(p->error().empty());
-  const auto module_str = Demangler().Demangle(p->program());
+  const auto module_str = p->program().to_str();
   EXPECT_THAT(module_str, HasSubstr(R"(Variable{
     x_200
     private
@@ -1172,7 +1172,7 @@
   )"));
   ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions());
   EXPECT_TRUE(p->error().empty());
-  const auto module_str = Demangler().Demangle(p->program());
+  const auto module_str = p->program().to_str();
   EXPECT_THAT(module_str, HasSubstr(R"(Variable{
     x_200
     private
@@ -1195,7 +1195,7 @@
   )"));
   ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions());
   EXPECT_TRUE(p->error().empty());
-  const auto module_str = Demangler().Demangle(p->program());
+  const auto module_str = p->program().to_str();
   EXPECT_THAT(module_str, HasSubstr(R"(Variable{
     x_200
     private
@@ -1224,7 +1224,7 @@
   )"));
   ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions());
   EXPECT_TRUE(p->error().empty());
-  const auto module_str = Demangler().Demangle(p->program());
+  const auto module_str = p->program().to_str();
   EXPECT_THAT(module_str, HasSubstr(R"(Variable{
     x_200
     private
@@ -1260,7 +1260,7 @@
   )"));
   ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions());
   EXPECT_TRUE(p->error().empty());
-  const auto module_str = Demangler().Demangle(p->program());
+  const auto module_str = p->program().to_str();
   EXPECT_THAT(module_str, HasSubstr(R"(Variable{
     x_200
     private
@@ -1296,7 +1296,7 @@
   )"));
   ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions());
   EXPECT_TRUE(p->error().empty());
-  const auto module_str = Demangler().Demangle(p->program());
+  const auto module_str = p->program().to_str();
   EXPECT_THAT(module_str, HasSubstr(R"(Variable{
     x_200
     private
@@ -1333,7 +1333,7 @@
   )"));
   ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions());
   EXPECT_TRUE(p->error().empty());
-  const auto module_str = Demangler().Demangle(p->program());
+  const auto module_str = p->program().to_str();
   EXPECT_THAT(module_str, HasSubstr(R"(Variable{
     x_200
     private
@@ -1356,7 +1356,7 @@
   )"));
   ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions());
   EXPECT_TRUE(p->error().empty());
-  const auto module_str = Demangler().Demangle(p->program());
+  const auto module_str = p->program().to_str();
   EXPECT_THAT(module_str, HasSubstr(R"(Variable{
     x_200
     private
@@ -1379,7 +1379,7 @@
   )"));
   ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions());
   EXPECT_TRUE(p->error().empty());
-  const auto module_str = Demangler().Demangle(p->program());
+  const auto module_str = p->program().to_str();
   EXPECT_THAT(module_str, HasSubstr(R"(Variable{
     x_200
     private
@@ -1404,7 +1404,7 @@
   )"));
   ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions());
   EXPECT_TRUE(p->error().empty());
-  const auto module_str = Demangler().Demangle(p->program());
+  const auto module_str = p->program().to_str();
   EXPECT_THAT(module_str, HasSubstr(R"(Variable{
     x_200
     private
@@ -1433,7 +1433,7 @@
   )"));
   ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error();
   EXPECT_TRUE(p->error().empty());
-  const auto module_str = Demangler().Demangle(p->program());
+  const auto module_str = p->program().to_str();
   EXPECT_THAT(module_str, HasSubstr(R"(Variable{
     x_200
     private
@@ -1462,7 +1462,7 @@
   )"));
   ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error();
   EXPECT_TRUE(p->error().empty());
-  const auto module_str = Demangler().Demangle(p->program());
+  const auto module_str = p->program().to_str();
   EXPECT_THAT(module_str, HasSubstr(R"(Variable{
     x_200
     private
@@ -1493,7 +1493,7 @@
   )"));
   ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error();
   EXPECT_TRUE(p->error().empty());
-  const auto module_str = Demangler().Demangle(p->program());
+  const auto module_str = p->program().to_str();
   EXPECT_THAT(module_str, HasSubstr(R"(
   Variable{
     Decorations{
@@ -1545,7 +1545,7 @@
   )"));
   ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error();
   EXPECT_TRUE(p->error().empty());
-  const auto module_str = Demangler().Demangle(p->program());
+  const auto module_str = p->program().to_str();
   EXPECT_THAT(module_str, HasSubstr(R"(
   Variable{
     Decorations{
@@ -1599,7 +1599,7 @@
   )"));
   ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error();
   EXPECT_TRUE(p->error().empty());
-  const auto module_str = Demangler().Demangle(p->program());
+  const auto module_str = p->program().to_str();
   EXPECT_THAT(module_str, HasSubstr(R"(
   Variable{
     Decorations{
@@ -1653,7 +1653,7 @@
   )"));
   ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions());
   EXPECT_TRUE(p->error().empty());
-  const auto module_str = Demangler().Demangle(p->program());
+  const auto module_str = p->program().to_str();
   EXPECT_THAT(module_str, HasSubstr(R"(
   S Struct{
     [[block]]
@@ -1684,7 +1684,7 @@
   )"));
   ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error();
   EXPECT_TRUE(p->error().empty());
-  const auto module_str = Demangler().Demangle(p->program());
+  const auto module_str = p->program().to_str();
   EXPECT_THAT(module_str, HasSubstr(R"(
   S Struct{
     [[block]]
@@ -1713,7 +1713,7 @@
   )"));
   ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error();
   EXPECT_TRUE(p->error().empty());
-  const auto module_str = Demangler().Demangle(p->program());
+  const auto module_str = p->program().to_str();
   EXPECT_THAT(module_str, HasSubstr(R"(
   S Struct{
     [[block]]
@@ -1762,7 +1762,7 @@
   )"));
   ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error();
   EXPECT_TRUE(p->error().empty());
-  const auto module_str = Demangler().Demangle(p->program());
+  const auto module_str = p->program().to_str();
   EXPECT_THAT(module_str, HasSubstr(R"(
   S Struct{
     [[block]]
@@ -1791,7 +1791,7 @@
   )"));
   ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error();
   EXPECT_TRUE(p->error().empty());
-  const auto module_str = Demangler().Demangle(p->program());
+  const auto module_str = p->program().to_str();
   EXPECT_THAT(module_str, HasSubstr(R"(
   S Struct{
     [[block]]
@@ -1823,7 +1823,7 @@
   )"));
   ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error();
   EXPECT_TRUE(p->error().empty());
-  const auto module_str = Demangler().Demangle(p->program());
+  const auto module_str = p->program().to_str();
   EXPECT_THAT(module_str, HasSubstr(R"(
   S Struct{
     [[block]]
@@ -1847,7 +1847,7 @@
   )"));
   ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error();
   EXPECT_TRUE(p->error().empty());
-  const auto module_str = Demangler().Demangle(p->program());
+  const auto module_str = p->program().to_str();
   EXPECT_THAT(module_str, HasSubstr(R"(
   VariableConst{
     Decorations{
@@ -1872,7 +1872,7 @@
   )"));
   ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error();
   EXPECT_TRUE(p->error().empty());
-  const auto module_str = Demangler().Demangle(p->program());
+  const auto module_str = p->program().to_str();
   EXPECT_THAT(module_str, HasSubstr(R"(
   VariableConst{
     Decorations{
@@ -1897,7 +1897,7 @@
   )"));
   ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error();
   EXPECT_TRUE(p->error().empty());
-  const auto module_str = Demangler().Demangle(p->program());
+  const auto module_str = p->program().to_str();
   EXPECT_THAT(module_str, HasSubstr(R"(
   VariableConst{
     Decorations{
@@ -1922,7 +1922,7 @@
   )"));
   ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error();
   EXPECT_TRUE(p->error().empty());
-  const auto module_str = Demangler().Demangle(p->program());
+  const auto module_str = p->program().to_str();
   EXPECT_THAT(module_str, HasSubstr(R"(
   VariableConst{
     Decorations{
@@ -1947,7 +1947,7 @@
   )"));
   ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error();
   EXPECT_TRUE(p->error().empty());
-  const auto module_str = Demangler().Demangle(p->program());
+  const auto module_str = p->program().to_str();
   EXPECT_THAT(module_str, HasSubstr(R"(
   VariableConst{
     Decorations{
@@ -1973,7 +1973,7 @@
   )"));
   ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error();
   EXPECT_TRUE(p->error().empty());
-  const auto module_str = Demangler().Demangle(p->program());
+  const auto module_str = p->program().to_str();
   EXPECT_THAT(module_str, HasSubstr(R"(
   VariableConst{
     myconst
diff --git a/src/reader/spirv/parser_impl_named_types_test.cc b/src/reader/spirv/parser_impl_named_types_test.cc
index 2494080..c62b71b 100644
--- a/src/reader/spirv/parser_impl_named_types_test.cc
+++ b/src/reader/spirv/parser_impl_named_types_test.cc
@@ -41,7 +41,7 @@
     %s = OpTypeStruct %uint %uint
   )"));
   EXPECT_TRUE(p->BuildAndParseInternalModule());
-  EXPECT_THAT(Demangler().Demangle(p->program()), HasSubstr("S Struct"));
+  EXPECT_THAT(p->program().to_str(), HasSubstr("S Struct"));
 }
 
 TEST_F(SpvParserTest, NamedTypes_NamedStruct) {
@@ -51,7 +51,7 @@
     %s = OpTypeStruct %uint %uint
   )"));
   EXPECT_TRUE(p->BuildAndParseInternalModule());
-  EXPECT_THAT(Demangler().Demangle(p->program()), HasSubstr("mystruct Struct"));
+  EXPECT_THAT(p->program().to_str(), HasSubstr("mystruct Struct"));
 }
 
 TEST_F(SpvParserTest, NamedTypes_Dup_EmitBoth) {
@@ -61,7 +61,7 @@
     %s2 = OpTypeStruct %uint %uint
   )"));
   EXPECT_TRUE(p->BuildAndParseInternalModule()) << p->error();
-  EXPECT_THAT(Demangler().Demangle(p->program()), HasSubstr(R"(S Struct{
+  EXPECT_THAT(p->program().to_str(), HasSubstr(R"(S Struct{
     StructMember{field0: __u32}
     StructMember{field1: __u32}
   }
@@ -82,7 +82,7 @@
     %arr = OpTypeRuntimeArray %uint
   )"));
   EXPECT_TRUE(p->BuildAndParseInternalModule());
-  EXPECT_THAT(Demangler().Demangle(p->program()),
+  EXPECT_THAT(p->program().to_str(),
               HasSubstr("RTArr -> __array__u32_stride_8\n"));
 }
 
@@ -95,7 +95,7 @@
     %arr2 = OpTypeRuntimeArray %uint
   )"));
   EXPECT_TRUE(p->BuildAndParseInternalModule());
-  EXPECT_THAT(Demangler().Demangle(p->program()),
+  EXPECT_THAT(p->program().to_str(),
               HasSubstr("RTArr -> __array__u32_stride_8\n  RTArr_1 -> "
                         "__array__u32_stride_8\n"));
 }
@@ -108,7 +108,7 @@
     %arr = OpTypeRuntimeArray %uint
   )"));
   EXPECT_TRUE(p->BuildAndParseInternalModule());
-  EXPECT_THAT(Demangler().Demangle(p->program()),
+  EXPECT_THAT(p->program().to_str(),
               HasSubstr("myrtarr -> __array__u32_stride_8\n"));
 }
 
@@ -122,7 +122,7 @@
     %arr2 = OpTypeArray %uint %uint_5
   )"));
   EXPECT_TRUE(p->BuildAndParseInternalModule());
-  EXPECT_THAT(Demangler().Demangle(p->program()),
+  EXPECT_THAT(p->program().to_str(),
               HasSubstr("myarr -> __array__u32_5_stride_8"));
 }
 
@@ -136,7 +136,7 @@
     %arr2 = OpTypeArray %uint %uint_5
   )"));
   EXPECT_TRUE(p->BuildAndParseInternalModule());
-  EXPECT_THAT(Demangler().Demangle(p->program()),
+  EXPECT_THAT(p->program().to_str(),
               HasSubstr("Arr -> __array__u32_5_stride_8\n  Arr_1 -> "
                         "__array__u32_5_stride_8"));
 }
diff --git a/src/reader/spirv/parser_impl_test_helper.h b/src/reader/spirv/parser_impl_test_helper.h
index 89aa2af..fb4a2ff 100644
--- a/src/reader/spirv/parser_impl_test_helper.h
+++ b/src/reader/spirv/parser_impl_test_helper.h
@@ -69,7 +69,7 @@
                             const ast::StatementList& stmts) {
   std::ostringstream outs;
   for (const auto* stmt : stmts) {
-    stmt->to_str(program.Sem(), outs, 0);
+    program.to_str(stmt, outs, 0);
   }
   return Demangler().Demangle(program.Symbols(), outs.str());
 }
@@ -82,7 +82,7 @@
                             const ast::StatementList& stmts) {
   std::ostringstream outs;
   for (const auto* stmt : stmts) {
-    stmt->to_str(builder.Sem(), outs, 0);
+    builder.to_str(stmt, outs, 0);
   }
   return Demangler().Demangle(builder.Symbols(), outs.str());
 }
diff --git a/src/type/test_helper.h b/src/type/test_helper.h
index 68832b0..0665100 100644
--- a/src/type/test_helper.h
+++ b/src/type/test_helper.h
@@ -20,7 +20,6 @@
 #include <utility>
 
 #include "gtest/gtest.h"
-#include "src/demangler.h"
 #include "src/program_builder.h"
 
 namespace tint {
@@ -28,18 +27,7 @@
 
 /// Helper class for testing
 template <typename BASE>
-class TestHelperBase : public BASE, public ProgramBuilder {
- public:
-  /// Demangles the given string
-  /// @param s the string to demangle
-  /// @returns the demangled string
-  std::string demangle(const std::string& s) {
-    return demanger.Demangle(this, s);
-  }
-
-  /// A demangler
-  Demangler demanger;
-};
+class TestHelperBase : public BASE, public ProgramBuilder {};
 using TestHelper = TestHelperBase<testing::Test>;
 
 template <typename T>
diff --git a/src/type_determiner.cc b/src/type_determiner.cc
index 65ec83f..bf5e601 100644
--- a/src/type_determiner.cc
+++ b/src/type_determiner.cc
@@ -289,7 +289,7 @@
   }
 
   set_error(stmt->source(), "unknown statement type for type determination: " +
-                                stmt->str(builder_->Sem()));
+                                builder_->str(stmt));
   return false;
 }
 
diff --git a/src/type_determiner_test.cc b/src/type_determiner_test.cc
index 50d3f27..b9548a9 100644
--- a/src/type_determiner_test.cc
+++ b/src/type_determiner_test.cc
@@ -3175,7 +3175,7 @@
       ident->intrinsic_signature());
   ASSERT_NE(sig, nullptr);
 
-  auto got = to_str(param.function, sig);
+  auto got = ::tint::to_str(param.function, sig);
   auto* expected = expected_texture_overload(param.overload);
   EXPECT_EQ(got, expected);
 }
diff --git a/src/validator/validator_impl.cc b/src/validator/validator_impl.cc
index 0c418a1..5227a2f 100644
--- a/src/validator/validator_impl.cc
+++ b/src/validator/validator_impl.cc
@@ -360,14 +360,10 @@
                                    ? selector->As<ast::UintLiteral>()->value()
                                    : selector->As<ast::SintLiteral>()->value());
       if (selector_set.count(v)) {
-        auto v_str =
-            selector->type()->Is<type::U32>()
-                ? selector->As<ast::UintLiteral>()->to_str(program_->Sem())
-                : selector->As<ast::SintLiteral>()->to_str(program_->Sem());
         add_error(case_stmt->source(), "v-0027",
                   "a literal value must not appear more than once in "
                   "the case selectors for a switch statement: '" +
-                      v_str + "'");
+                      program_->str(selector) + "'");
         return false;
       }
       selector_set.emplace(v);
diff --git a/src/writer/hlsl/generator_impl.cc b/src/writer/hlsl/generator_impl.cc
index e7ec1ce..e667955 100644
--- a/src/writer/hlsl/generator_impl.cc
+++ b/src/writer/hlsl/generator_impl.cc
@@ -1141,7 +1141,7 @@
     return EmitUnaryOp(pre, out, u);
   }
 
-  error_ = "unknown expression type: " + expr->str(program_->Sem());
+  error_ = "unknown expression type: " + program_->str(expr);
   return false;
 }
 
@@ -2179,7 +2179,7 @@
     return EmitVariable(out, v->variable(), false);
   }
 
-  error_ = "unknown statement type: " + stmt->str(program_->Sem());
+  error_ = "unknown statement type: " + program_->str(stmt);
   return false;
 }
 
diff --git a/src/writer/msl/generator_impl.cc b/src/writer/msl/generator_impl.cc
index eaae6c0..b0298c9 100644
--- a/src/writer/msl/generator_impl.cc
+++ b/src/writer/msl/generator_impl.cc
@@ -1177,7 +1177,7 @@
     return EmitUnaryOp(u);
   }
 
-  error_ = "unknown expression type: " + expr->str(program_->Sem());
+  error_ = "unknown expression type: " + program_->str(expr);
   return false;
 }
 
@@ -1844,7 +1844,7 @@
     return EmitVariable(v->variable(), false);
   }
 
-  error_ = "unknown statement type: " + stmt->str(program_->Sem());
+  error_ = "unknown statement type: " + program_->str(stmt);
   return false;
 }
 
@@ -2032,7 +2032,7 @@
         }
         current_offset = offset;
       } else {
-        error_ = "unsupported member decoration: " + deco->str(program_->Sem());
+        error_ = "unsupported member decoration: " + program_->str(deco);
         return false;
       }
     }
diff --git a/src/writer/spirv/builder.cc b/src/writer/spirv/builder.cc
index 4fb1b76..b6c8266 100644
--- a/src/writer/spirv/builder.cc
+++ b/src/writer/spirv/builder.cc
@@ -533,7 +533,7 @@
     return GenerateUnaryOpExpression(u);
   }
 
-  error_ = "unknown expression type: " + expr->str(program_->Sem());
+  error_ = "unknown expression type: " + program_->str(expr);
   return 0;
 }
 
@@ -1091,7 +1091,7 @@
       }
 
     } else {
-      error_ = "invalid accessor in list: " + accessor->str(program_->Sem());
+      error_ = "invalid accessor in list: " + program_->str(accessor);
       return 0;
     }
   }
@@ -2763,7 +2763,7 @@
     return GenerateVariableDeclStatement(v);
   }
 
-  error_ = "Unknown statement: " + stmt->str(program_->Sem());
+  error_ = "Unknown statement: " + program_->str(stmt);
   return false;
 }
 
diff --git a/src/writer/wgsl/generator_impl.cc b/src/writer/wgsl/generator_impl.cc
index 20561e1..eeb4dcd 100644
--- a/src/writer/wgsl/generator_impl.cc
+++ b/src/writer/wgsl/generator_impl.cc
@@ -550,7 +550,7 @@
   auto* impl = str->impl();
   for (auto* deco : impl->decorations()) {
     out_ << "[[";
-    deco->to_str(program_->Sem(), out_, 0);
+    program_->to_str(deco, out_, 0);
     out_ << "]]" << std::endl;
   }
   out_ << "struct " << program_->Symbols().NameFor(str->symbol()) << " {"
@@ -817,7 +817,7 @@
     return EmitVariable(v->variable());
   }
 
-  error_ = "unknown statement type: " + stmt->str(program_->Sem());
+  error_ = "unknown statement type: " + program_->str(stmt);
   return false;
 }