Add File & Range information to tint::Source

This is the first step in improving the error messages produced while parsing.

The `line` and `column` information of `Source` has been moved to `Source::Location`.

`Source::Range` has been added that contains a `Location` interval - allowing error messages to highlight the full region of the error.

The `File` information provides an optional file path, and pre-splits the content into lines. These lines can be used to print the full line containing an error.

This CL contains a few temporary changes that help split up this work, and to ease integration with Tint.

Bug: tint:282
Change-Id: I7aa501b0a9631f286e8e93fd7396bdbe38175727
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/31420
Reviewed-by: dan sinclair <dsinclair@chromium.org>
Reviewed-by: David Neto <dneto@google.com>
Commit-Queue: David Neto <dneto@google.com>
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 5bde30c..c90d0ca 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -207,6 +207,7 @@
   reader/reader.cc
   reader/reader.h
   scope_stack.h
+  source.cc
   source.h
   transform/bound_array_accessors_transform.cc
   transform/bound_array_accessors_transform.h
diff --git a/src/ast/array_accessor_expression_test.cc b/src/ast/array_accessor_expression_test.cc
index a08a837..1470bc0 100644
--- a/src/ast/array_accessor_expression_test.cc
+++ b/src/ast/array_accessor_expression_test.cc
@@ -40,8 +40,8 @@
 
   ArrayAccessorExpression exp(Source{20, 2}, std::move(ary), std::move(idx));
   auto src = exp.source();
-  EXPECT_EQ(src.line, 20u);
-  EXPECT_EQ(src.column, 2u);
+  EXPECT_EQ(src.range.begin.line, 20u);
+  EXPECT_EQ(src.range.begin.column, 2u);
 }
 
 TEST_F(ArrayAccessorExpressionTest, IsArrayAccessor) {
diff --git a/src/ast/assignment_statement_test.cc b/src/ast/assignment_statement_test.cc
index 85a29d6..b157020 100644
--- a/src/ast/assignment_statement_test.cc
+++ b/src/ast/assignment_statement_test.cc
@@ -41,8 +41,8 @@
 
   AssignmentStatement stmt(Source{20, 2}, std::move(lhs), std::move(rhs));
   auto src = stmt.source();
-  EXPECT_EQ(src.line, 20u);
-  EXPECT_EQ(src.column, 2u);
+  EXPECT_EQ(src.range.begin.line, 20u);
+  EXPECT_EQ(src.range.begin.column, 2u);
 }
 
 TEST_F(AssignmentStatementTest, IsAssign) {
diff --git a/src/ast/binary_expression_test.cc b/src/ast/binary_expression_test.cc
index b7c50b1..9a7510d 100644
--- a/src/ast/binary_expression_test.cc
+++ b/src/ast/binary_expression_test.cc
@@ -45,8 +45,8 @@
   BinaryExpression r(Source{20, 2}, BinaryOp::kEqual, std::move(lhs),
                      std::move(rhs));
   auto src = r.source();
-  EXPECT_EQ(src.line, 20u);
-  EXPECT_EQ(src.column, 2u);
+  EXPECT_EQ(src.range.begin.line, 20u);
+  EXPECT_EQ(src.range.begin.column, 2u);
 }
 
 TEST_F(BinaryExpressionTest, IsBinaryal) {
diff --git a/src/ast/bitcast_expression_test.cc b/src/ast/bitcast_expression_test.cc
index accd1ff..7ef635a 100644
--- a/src/ast/bitcast_expression_test.cc
+++ b/src/ast/bitcast_expression_test.cc
@@ -41,8 +41,8 @@
 
   BitcastExpression exp(Source{20, 2}, &f32, std::move(expr));
   auto src = exp.source();
-  EXPECT_EQ(src.line, 20u);
-  EXPECT_EQ(src.column, 2u);
+  EXPECT_EQ(src.range.begin.line, 20u);
+  EXPECT_EQ(src.range.begin.column, 2u);
 }
 
 TEST_F(BitcastExpressionTest, IsBitcast) {
diff --git a/src/ast/block_statement_test.cc b/src/ast/block_statement_test.cc
index 2d040d3..ac3f65b 100644
--- a/src/ast/block_statement_test.cc
+++ b/src/ast/block_statement_test.cc
@@ -62,8 +62,8 @@
 TEST_F(BlockStatementTest, Creation_WithSource) {
   BlockStatement b(Source{20, 2});
   auto src = b.source();
-  EXPECT_EQ(src.line, 20u);
-  EXPECT_EQ(src.column, 2u);
+  EXPECT_EQ(src.range.begin.line, 20u);
+  EXPECT_EQ(src.range.begin.column, 2u);
 }
 
 TEST_F(BlockStatementTest, IsBlock) {
diff --git a/src/ast/break_statement_test.cc b/src/ast/break_statement_test.cc
index 4e39386..485030d 100644
--- a/src/ast/break_statement_test.cc
+++ b/src/ast/break_statement_test.cc
@@ -25,8 +25,8 @@
 TEST_F(BreakStatementTest, Creation_WithSource) {
   BreakStatement stmt(Source{20, 2});
   auto src = stmt.source();
-  EXPECT_EQ(src.line, 20u);
-  EXPECT_EQ(src.column, 2u);
+  EXPECT_EQ(src.range.begin.line, 20u);
+  EXPECT_EQ(src.range.begin.column, 2u);
 }
 
 TEST_F(BreakStatementTest, IsBreak) {
diff --git a/src/ast/call_expression_test.cc b/src/ast/call_expression_test.cc
index a89441f..120100f 100644
--- a/src/ast/call_expression_test.cc
+++ b/src/ast/call_expression_test.cc
@@ -46,8 +46,8 @@
   auto func = std::make_unique<IdentifierExpression>("func");
   CallExpression stmt(Source{20, 2}, std::move(func), {});
   auto src = stmt.source();
-  EXPECT_EQ(src.line, 20u);
-  EXPECT_EQ(src.column, 2u);
+  EXPECT_EQ(src.range.begin.line, 20u);
+  EXPECT_EQ(src.range.begin.column, 2u);
 }
 
 TEST_F(CallExpressionTest, IsCall) {
diff --git a/src/ast/case_statement_test.cc b/src/ast/case_statement_test.cc
index b834c59..82f6868 100644
--- a/src/ast/case_statement_test.cc
+++ b/src/ast/case_statement_test.cc
@@ -76,8 +76,8 @@
 
   CaseStatement c(Source{20, 2}, std::move(b), std::move(body));
   auto src = c.source();
-  EXPECT_EQ(src.line, 20u);
-  EXPECT_EQ(src.column, 2u);
+  EXPECT_EQ(src.range.begin.line, 20u);
+  EXPECT_EQ(src.range.begin.column, 2u);
 }
 
 TEST_F(CaseStatementTest, IsDefault_WithoutSelectors) {
diff --git a/src/ast/continue_statement_test.cc b/src/ast/continue_statement_test.cc
index 4ba18e9..fa5ac43 100644
--- a/src/ast/continue_statement_test.cc
+++ b/src/ast/continue_statement_test.cc
@@ -25,8 +25,8 @@
 TEST_F(ContinueStatementTest, Creation_WithSource) {
   ContinueStatement stmt(Source{20, 2});
   auto src = stmt.source();
-  EXPECT_EQ(src.line, 20u);
-  EXPECT_EQ(src.column, 2u);
+  EXPECT_EQ(src.range.begin.line, 20u);
+  EXPECT_EQ(src.range.begin.column, 2u);
 }
 
 TEST_F(ContinueStatementTest, IsContinue) {
diff --git a/src/ast/else_statement_test.cc b/src/ast/else_statement_test.cc
index d791ad0..a230c2a 100644
--- a/src/ast/else_statement_test.cc
+++ b/src/ast/else_statement_test.cc
@@ -46,8 +46,8 @@
 TEST_F(ElseStatementTest, Creation_WithSource) {
   ElseStatement e(Source{20, 2}, std::make_unique<BlockStatement>());
   auto src = e.source();
-  EXPECT_EQ(src.line, 20u);
-  EXPECT_EQ(src.column, 2u);
+  EXPECT_EQ(src.range.begin.line, 20u);
+  EXPECT_EQ(src.range.begin.column, 2u);
 }
 
 TEST_F(ElseStatementTest, IsElse) {
diff --git a/src/ast/fallthrough_statement_test.cc b/src/ast/fallthrough_statement_test.cc
index c7dbe58..adbb558 100644
--- a/src/ast/fallthrough_statement_test.cc
+++ b/src/ast/fallthrough_statement_test.cc
@@ -31,8 +31,8 @@
 TEST_F(FallthroughStatementTest, Creation_WithSource) {
   FallthroughStatement stmt(Source{20, 2});
   auto src = stmt.source();
-  EXPECT_EQ(src.line, 20u);
-  EXPECT_EQ(src.column, 2u);
+  EXPECT_EQ(src.range.begin.line, 20u);
+  EXPECT_EQ(src.range.begin.column, 2u);
 }
 
 TEST_F(FallthroughStatementTest, IsFallthrough) {
diff --git a/src/ast/function_test.cc b/src/ast/function_test.cc
index 20d37ed..0dbfbef 100644
--- a/src/ast/function_test.cc
+++ b/src/ast/function_test.cc
@@ -58,8 +58,8 @@
 
   Function f(Source{20, 2}, "func", std::move(params), &void_type);
   auto src = f.source();
-  EXPECT_EQ(src.line, 20u);
-  EXPECT_EQ(src.column, 2u);
+  EXPECT_EQ(src.range.begin.line, 20u);
+  EXPECT_EQ(src.range.begin.column, 2u);
 }
 
 TEST_F(FunctionTest, AddDuplicateReferencedVariables) {
diff --git a/src/ast/identifier_expression_test.cc b/src/ast/identifier_expression_test.cc
index ad0ff98..fbb5dd7 100644
--- a/src/ast/identifier_expression_test.cc
+++ b/src/ast/identifier_expression_test.cc
@@ -32,8 +32,8 @@
   EXPECT_EQ(i.name(), "ident");
 
   auto src = i.source();
-  EXPECT_EQ(src.line, 20u);
-  EXPECT_EQ(src.column, 2u);
+  EXPECT_EQ(src.range.begin.line, 20u);
+  EXPECT_EQ(src.range.begin.column, 2u);
 }
 
 TEST_F(IdentifierExpressionTest, IsIdentifier) {
diff --git a/src/ast/if_statement_test.cc b/src/ast/if_statement_test.cc
index bd1e9c9..779e889 100644
--- a/src/ast/if_statement_test.cc
+++ b/src/ast/if_statement_test.cc
@@ -45,8 +45,8 @@
 
   IfStatement stmt(Source{20, 2}, std::move(cond), std::move(body));
   auto src = stmt.source();
-  EXPECT_EQ(src.line, 20u);
-  EXPECT_EQ(src.column, 2u);
+  EXPECT_EQ(src.range.begin.line, 20u);
+  EXPECT_EQ(src.range.begin.column, 2u);
 }
 
 TEST_F(IfStatementTest, IsIf) {
diff --git a/src/ast/loop_statement_test.cc b/src/ast/loop_statement_test.cc
index bf61292..cf1dc50 100644
--- a/src/ast/loop_statement_test.cc
+++ b/src/ast/loop_statement_test.cc
@@ -52,8 +52,8 @@
 
   LoopStatement l(Source{20, 2}, std::move(body), std::move(continuing));
   auto src = l.source();
-  EXPECT_EQ(src.line, 20u);
-  EXPECT_EQ(src.column, 2u);
+  EXPECT_EQ(src.range.begin.line, 20u);
+  EXPECT_EQ(src.range.begin.column, 2u);
 }
 
 TEST_F(LoopStatementTest, IsLoop) {
diff --git a/src/ast/member_accessor_expression_test.cc b/src/ast/member_accessor_expression_test.cc
index 888884e..66ac1f7 100644
--- a/src/ast/member_accessor_expression_test.cc
+++ b/src/ast/member_accessor_expression_test.cc
@@ -43,8 +43,8 @@
 
   MemberAccessorExpression stmt(Source{20, 2}, std::move(str), std::move(mem));
   auto src = stmt.source();
-  EXPECT_EQ(src.line, 20u);
-  EXPECT_EQ(src.column, 2u);
+  EXPECT_EQ(src.range.begin.line, 20u);
+  EXPECT_EQ(src.range.begin.column, 2u);
 }
 
 TEST_F(MemberAccessorExpressionTest, IsMemberAccessor) {
diff --git a/src/ast/node.h b/src/ast/node.h
index f02d85e..4eb57e4 100644
--- a/src/ast/node.h
+++ b/src/ast/node.h
@@ -34,10 +34,11 @@
   /// @param source the source data
   void set_source(const Source& source) { source_ = source; }
 
+  // TODO(bclayton): Deprecate - use source().range.begin instead
   /// @returns the line the node was declared on
-  size_t line() const { return source_.line; }
+  size_t line() const { return source_.range.begin.line; }
   /// @returns the column the node was declared on
-  size_t column() const { return source_.column; }
+  size_t column() const { return source_.range.begin.column; }
 
   /// @returns true if the node is valid
   virtual bool IsValid() const = 0;
diff --git a/src/ast/return_statement_test.cc b/src/ast/return_statement_test.cc
index 2477015..76d9e2a 100644
--- a/src/ast/return_statement_test.cc
+++ b/src/ast/return_statement_test.cc
@@ -36,8 +36,8 @@
 TEST_F(ReturnStatementTest, Creation_WithSource) {
   ReturnStatement r(Source{20, 2});
   auto src = r.source();
-  EXPECT_EQ(src.line, 20u);
-  EXPECT_EQ(src.column, 2u);
+  EXPECT_EQ(src.range.begin.line, 20u);
+  EXPECT_EQ(src.range.begin.column, 2u);
 }
 
 TEST_F(ReturnStatementTest, IsReturn) {
diff --git a/src/ast/scalar_constructor_expression_test.cc b/src/ast/scalar_constructor_expression_test.cc
index ea3dddc..28f7012 100644
--- a/src/ast/scalar_constructor_expression_test.cc
+++ b/src/ast/scalar_constructor_expression_test.cc
@@ -37,8 +37,8 @@
   auto b = std::make_unique<BoolLiteral>(&bool_type, true);
   ScalarConstructorExpression c(Source{20, 2}, std::move(b));
   auto src = c.source();
-  EXPECT_EQ(src.line, 20u);
-  EXPECT_EQ(src.column, 2u);
+  EXPECT_EQ(src.range.begin.line, 20u);
+  EXPECT_EQ(src.range.begin.column, 2u);
 }
 
 TEST_F(ScalarConstructorExpressionTest, IsValid) {
diff --git a/src/ast/switch_statement_test.cc b/src/ast/switch_statement_test.cc
index dd3eeac..dc5b5b0 100644
--- a/src/ast/switch_statement_test.cc
+++ b/src/ast/switch_statement_test.cc
@@ -53,8 +53,8 @@
 
   SwitchStatement stmt(Source{20, 2}, std::move(ident), CaseStatementList());
   auto src = stmt.source();
-  EXPECT_EQ(src.line, 20u);
-  EXPECT_EQ(src.column, 2u);
+  EXPECT_EQ(src.range.begin.line, 20u);
+  EXPECT_EQ(src.range.begin.column, 2u);
 }
 
 TEST_F(SwitchStatementTest, IsSwitch) {
diff --git a/src/ast/type_constructor_expression_test.cc b/src/ast/type_constructor_expression_test.cc
index c223fb5..dfceda0 100644
--- a/src/ast/type_constructor_expression_test.cc
+++ b/src/ast/type_constructor_expression_test.cc
@@ -48,8 +48,8 @@
 
   TypeConstructorExpression t(Source{20, 2}, &f32, std::move(expr));
   auto src = t.source();
-  EXPECT_EQ(src.line, 20u);
-  EXPECT_EQ(src.column, 2u);
+  EXPECT_EQ(src.range.begin.line, 20u);
+  EXPECT_EQ(src.range.begin.column, 2u);
 }
 
 TEST_F(TypeConstructorExpressionTest, IsTypeConstructor) {
diff --git a/src/ast/unary_op_expression_test.cc b/src/ast/unary_op_expression_test.cc
index 1d5e3194..5b32af3 100644
--- a/src/ast/unary_op_expression_test.cc
+++ b/src/ast/unary_op_expression_test.cc
@@ -38,8 +38,8 @@
   auto ident = std::make_unique<IdentifierExpression>("ident");
   UnaryOpExpression u(Source{20, 2}, UnaryOp::kNot, std::move(ident));
   auto src = u.source();
-  EXPECT_EQ(src.line, 20u);
-  EXPECT_EQ(src.column, 2u);
+  EXPECT_EQ(src.range.begin.line, 20u);
+  EXPECT_EQ(src.range.begin.column, 2u);
 }
 
 TEST_F(UnaryOpExpressionTest, IsUnaryOp) {
diff --git a/src/ast/variable_decl_statement_test.cc b/src/ast/variable_decl_statement_test.cc
index d96b07b..61fcb07 100644
--- a/src/ast/variable_decl_statement_test.cc
+++ b/src/ast/variable_decl_statement_test.cc
@@ -39,8 +39,8 @@
 
   VariableDeclStatement stmt(Source{20, 2}, std::move(var));
   auto src = stmt.source();
-  EXPECT_EQ(src.line, 20u);
-  EXPECT_EQ(src.column, 2u);
+  EXPECT_EQ(src.range.begin.line, 20u);
+  EXPECT_EQ(src.range.begin.column, 2u);
 }
 
 TEST_F(VariableDeclStatementTest, IsVariableDecl) {
diff --git a/src/reader/spirv/function.cc b/src/reader/spirv/function.cc
index 9c62f9b..a44b3bc 100644
--- a/src/reader/spirv/function.cc
+++ b/src/reader/spirv/function.cc
@@ -429,7 +429,7 @@
 /// @param src a source record
 /// @returns true if |src| is a non-default Source
 bool HasSource(const Source& src) {
-  return src.line != 0 || src.column != 0;
+  return src.range.begin.line > 0 || src.range.begin.column != 0;
 }
 
 }  // namespace
diff --git a/src/reader/spirv/parser_impl.cc b/src/reader/spirv/parser_impl.cc
index d57ee6c..af3d07d 100644
--- a/src/reader/spirv/parser_impl.cc
+++ b/src/reader/spirv/parser_impl.cc
@@ -474,12 +474,12 @@
 }
 
 void ParserImpl::RegisterLineNumbers() {
-  Source instruction_number{0, 0};
+  Source::Location instruction_number{0, 0};
 
   // Has there been an OpLine since the last OpNoLine or start of the module?
   bool in_op_line_scope = false;
   // The source location provided by the most recent OpLine instruction.
-  Source op_line_source{0, 0};
+  Source::Location op_line_source{0, 0};
   const bool run_on_debug_insts = true;
   module_->ForEachInst(
       [this, &in_op_line_scope, &op_line_source,
@@ -515,7 +515,7 @@
   if (where == inst_source_.end()) {
     return {};
   }
-  return where->second;
+  return Source{where->second};
 }
 
 bool ParserImpl::ParseInternalModuleExceptFunctions() {
diff --git a/src/reader/spirv/parser_impl.h b/src/reader/spirv/parser_impl.h
index 7becccd..dfffc8d 100644
--- a/src/reader/spirv/parser_impl.h
+++ b/src/reader/spirv/parser_impl.h
@@ -454,7 +454,8 @@
   // is in effect for the instruction, map the instruction to its position
   // in the SPIR-V module, counting by instructions, where the first
   // instruction is line 1.
-  std::unordered_map<const spvtools::opt::Instruction*, Source> inst_source_;
+  std::unordered_map<const spvtools::opt::Instruction*, Source::Location>
+      inst_source_;
 
   // The set of IDs that are imports of the GLSL.std.450 extended instruction
   // sets.
diff --git a/src/reader/spirv/parser_impl_test.cc b/src/reader/spirv/parser_impl_test.cc
index 4d4138f..cee5eb2 100644
--- a/src/reader/spirv/parser_impl_test.cc
+++ b/src/reader/spirv/parser_impl_test.cc
@@ -139,14 +139,14 @@
   EXPECT_TRUE(p->error().empty());
   // Use instruction counting.
   auto s5 = p->GetSourceForResultIdForTest(5);
-  EXPECT_EQ(7u, s5.line);
-  EXPECT_EQ(0u, s5.column);
+  EXPECT_EQ(7u, s5.range.begin.line);
+  EXPECT_EQ(0u, s5.range.begin.column);
   auto s60 = p->GetSourceForResultIdForTest(60);
-  EXPECT_EQ(8u, s60.line);
-  EXPECT_EQ(0u, s60.column);
+  EXPECT_EQ(8u, s60.range.begin.line);
+  EXPECT_EQ(0u, s60.range.begin.column);
   auto s1 = p->GetSourceForResultIdForTest(1);
-  EXPECT_EQ(10u, s1.line);
-  EXPECT_EQ(0u, s1.column);
+  EXPECT_EQ(10u, s1.range.begin.line);
+  EXPECT_EQ(0u, s1.range.begin.column);
 }
 
 TEST_F(SpvParserTest, Impl_Source_WithOpLine_WithOpNoLine) {
@@ -172,15 +172,15 @@
   EXPECT_TRUE(p->error().empty());
   // Use the information from the OpLine that is still in scope.
   auto s5 = p->GetSourceForResultIdForTest(5);
-  EXPECT_EQ(42u, s5.line);
-  EXPECT_EQ(53u, s5.column);
+  EXPECT_EQ(42u, s5.range.begin.line);
+  EXPECT_EQ(53u, s5.range.begin.column);
   auto s60 = p->GetSourceForResultIdForTest(60);
-  EXPECT_EQ(42u, s60.line);
-  EXPECT_EQ(53u, s60.column);
+  EXPECT_EQ(42u, s60.range.begin.line);
+  EXPECT_EQ(53u, s60.range.begin.column);
   // After OpNoLine, revert back to instruction counting.
   auto s1 = p->GetSourceForResultIdForTest(1);
-  EXPECT_EQ(13u, s1.line);
-  EXPECT_EQ(0u, s1.column);
+  EXPECT_EQ(13u, s1.range.begin.line);
+  EXPECT_EQ(0u, s1.range.begin.column);
 }
 
 TEST_F(SpvParserTest, Impl_Source_InvalidId) {
@@ -201,8 +201,8 @@
   EXPECT_TRUE(p->Parse());
   EXPECT_TRUE(p->error().empty());
   auto s99 = p->GetSourceForResultIdForTest(99);
-  EXPECT_EQ(0u, s99.line);
-  EXPECT_EQ(0u, s99.column);
+  EXPECT_EQ(0u, s99.range.begin.line);
+  EXPECT_EQ(0u, s99.range.begin.column);
 }
 
 TEST_F(SpvParserTest, Impl_IsValidIdentifier) {
diff --git a/src/reader/wgsl/lexer.cc b/src/reader/wgsl/lexer.cc
index 42019fa..2fa4eb8 100644
--- a/src/reader/wgsl/lexer.cc
+++ b/src/reader/wgsl/lexer.cc
@@ -32,8 +32,10 @@
 
 }  // namespace
 
-Lexer::Lexer(const std::string& input)
-    : input_(input), len_(static_cast<uint32_t>(input.size())) {}
+Lexer::Lexer(Source::File const* file)
+    : file_(file),
+      len_(static_cast<uint32_t>(file->content.size())),
+      location_{1, 1} {}
 
 Lexer::~Lexer() = default;
 
@@ -79,7 +81,11 @@
 }
 
 Source Lexer::make_source() const {
-  return Source{line_, column_};
+  Source src{};
+  src.file = file_;
+  src.range.begin = location_;
+  src.range.end = location_;
+  return src;
 }
 
 bool Lexer::is_eof() const {
@@ -103,24 +109,24 @@
 }
 
 bool Lexer::matches(size_t pos, const std::string& substr) {
-  if (pos >= input_.size())
+  if (pos >= len_)
     return false;
-  return input_.substr(pos, substr.size()) == substr;
+  return file_->content.substr(pos, substr.size()) == substr;
 }
 
 void Lexer::skip_whitespace() {
   for (;;) {
     auto pos = pos_;
-    while (!is_eof() && is_whitespace(input_[pos_])) {
+    while (!is_eof() && is_whitespace(file_->content[pos_])) {
       if (matches(pos_, "\n")) {
         pos_++;
-        line_++;
-        column_ = 1;
+        location_.line++;
+        location_.column = 1;
         continue;
       }
 
       pos_++;
-      column_++;
+      location_.column++;
     }
 
     skip_comments();
@@ -139,7 +145,7 @@
 
   while (!is_eof() && !matches(pos_, "\n")) {
     pos_++;
-    column_++;
+    location_.column++;
   }
 }
 
@@ -152,7 +158,7 @@
   if (matches(end, "-")) {
     end++;
   }
-  while (end < len_ && is_digit(input_[end])) {
+  while (end < len_ && is_digit(file_->content[end])) {
     end++;
   }
 
@@ -161,7 +167,7 @@
   }
   end++;
 
-  while (end < len_ && is_digit(input_[end])) {
+  while (end < len_ && is_digit(file_->content[end])) {
     end++;
   }
 
@@ -173,7 +179,7 @@
     }
 
     auto exp_start = end;
-    while (end < len_ && isdigit(input_[end])) {
+    while (end < len_ && isdigit(file_->content[end])) {
       end++;
     }
 
@@ -182,14 +188,14 @@
       return {};
   }
 
-  auto str = input_.substr(start, end - start);
+  auto str = file_->content.substr(start, end - start);
   if (str == "." || str == "-.")
     return {};
 
   pos_ = end;
-  column_ += (end - start);
+  location_.column += (end - start);
 
-  auto res = strtod(input_.c_str() + start, nullptr);
+  auto res = strtod(file_->content.c_str() + start, nullptr);
   // This handles if the number is a really small in the exponent
   if (res > 0 && res < static_cast<double>(std::numeric_limits<float>::min())) {
     return {Token::Type::kError, source, "f32 (" + str + " too small"};
@@ -205,28 +211,31 @@
   return {source, static_cast<float>(res)};
 }
 
-Token Lexer::build_token_from_int_if_possible(const Source& source,
+Token Lexer::build_token_from_int_if_possible(Source source,
                                               size_t start,
                                               size_t end,
                                               int32_t base) {
-  auto res = strtoll(input_.c_str() + start, nullptr, base);
+  auto res = strtoll(file_->content.c_str() + start, nullptr, base);
   if (matches(pos_, "u")) {
     if (static_cast<uint64_t>(res) >
         static_cast<uint64_t>(std::numeric_limits<uint32_t>::max())) {
-      return {Token::Type::kError, source,
-              "u32 (" + input_.substr(start, end - start) + ") too large"};
+      return {
+          Token::Type::kError, source,
+          "u32 (" + file_->content.substr(start, end - start) + ") too large"};
     }
     pos_ += 1;
     return {source, static_cast<uint32_t>(res)};
   }
 
   if (res < static_cast<int64_t>(std::numeric_limits<int32_t>::min())) {
-    return {Token::Type::kError, source,
-            "i32 (" + input_.substr(start, end - start) + ") too small"};
+    return {
+        Token::Type::kError, source,
+        "i32 (" + file_->content.substr(start, end - start) + ") too small"};
   }
   if (res > static_cast<int64_t>(std::numeric_limits<int32_t>::max())) {
-    return {Token::Type::kError, source,
-            "i32 (" + input_.substr(start, end - start) + ") too large"};
+    return {
+        Token::Type::kError, source,
+        "i32 (" + file_->content.substr(start, end - start) + ") too large"};
   }
   return {source, static_cast<int32_t>(res)};
 }
@@ -245,12 +254,12 @@
   }
   end += 2;
 
-  while (!is_eof() && is_hex(input_[end])) {
+  while (!is_eof() && is_hex(file_->content[end])) {
     end += 1;
   }
 
   pos_ = end;
-  column_ += (end - start);
+  location_.column += (end - start);
 
   return build_token_from_int_if_possible(source, start, end, 16);
 }
@@ -264,41 +273,41 @@
   if (matches(end, "-")) {
     end++;
   }
-  if (end >= len_ || !is_digit(input_[end])) {
+  if (end >= len_ || !is_digit(file_->content[end])) {
     return {};
   }
 
   auto first = end;
-  while (end < len_ && is_digit(input_[end])) {
+  while (end < len_ && is_digit(file_->content[end])) {
     end++;
   }
 
   // If the first digit is a zero this must only be zero as leading zeros
   // are not allowed.
-  if (input_[first] == '0' && (end - first != 1))
+  if (file_->content[first] == '0' && (end - first != 1))
     return {};
 
   pos_ = end;
-  column_ += (end - start);
+  location_.column += (end - start);
 
   return build_token_from_int_if_possible(source, start, end, 10);
 }
 
 Token Lexer::try_ident() {
   // Must begin with an a-zA-Z_
-  if (!is_alpha(input_[pos_])) {
+  if (!is_alpha(file_->content[pos_])) {
     return {};
   }
 
   auto source = make_source();
 
   auto s = pos_;
-  while (!is_eof() && is_alphanum(input_[pos_])) {
+  while (!is_eof() && is_alphanum(file_->content[pos_])) {
     pos_++;
-    column_++;
+    location_.column++;
   }
 
-  auto str = input_.substr(s, pos_ - s);
+  auto str = file_->content.substr(s, pos_ - s);
   auto t = check_reserved(source, str);
   if (!t.IsUninitialized()) {
     return t;
@@ -325,10 +334,10 @@
   }
   auto end = pos_;
   pos_++;
-  column_ += (pos_ - start) + 1;
+  location_.column += (pos_ - start) + 1;
 
   return {Token::Type::kStringLiteral, source,
-          input_.substr(start, end - start)};
+          file_->content.substr(start, end - start)};
 }
 
 Token Lexer::try_punctuation() {
@@ -338,131 +347,131 @@
   if (matches(pos_, "[[")) {
     type = Token::Type::kAttrLeft;
     pos_ += 2;
-    column_ += 2;
+    location_.column += 2;
   } else if (matches(pos_, "]]")) {
     type = Token::Type::kAttrRight;
     pos_ += 2;
-    column_ += 2;
+    location_.column += 2;
   } else if (matches(pos_, "(")) {
     type = Token::Type::kParenLeft;
     pos_ += 1;
-    column_ += 1;
+    location_.column += 1;
   } else if (matches(pos_, ")")) {
     type = Token::Type::kParenRight;
     pos_ += 1;
-    column_ += 1;
+    location_.column += 1;
   } else if (matches(pos_, "[")) {
     type = Token::Type::kBracketLeft;
     pos_ += 1;
-    column_ += 1;
+    location_.column += 1;
   } else if (matches(pos_, "]")) {
     type = Token::Type::kBracketRight;
     pos_ += 1;
-    column_ += 1;
+    location_.column += 1;
   } else if (matches(pos_, "{")) {
     type = Token::Type::kBraceLeft;
     pos_ += 1;
-    column_ += 1;
+    location_.column += 1;
   } else if (matches(pos_, "}")) {
     type = Token::Type::kBraceRight;
     pos_ += 1;
-    column_ += 1;
+    location_.column += 1;
   } else if (matches(pos_, "&&")) {
     type = Token::Type::kAndAnd;
     pos_ += 2;
-    column_ += 2;
+    location_.column += 2;
   } else if (matches(pos_, "&")) {
     type = Token::Type::kAnd;
     pos_ += 1;
-    column_ += 1;
+    location_.column += 1;
   } else if (matches(pos_, "/")) {
     type = Token::Type::kForwardSlash;
     pos_ += 1;
-    column_ += 1;
+    location_.column += 1;
   } else if (matches(pos_, "!=")) {
     type = Token::Type::kNotEqual;
     pos_ += 2;
-    column_ += 2;
+    location_.column += 2;
   } else if (matches(pos_, "!")) {
     type = Token::Type::kBang;
     pos_ += 1;
-    column_ += 1;
+    location_.column += 1;
   } else if (matches(pos_, "::")) {
     type = Token::Type::kNamespace;
     pos_ += 2;
-    column_ += 2;
+    location_.column += 2;
   } else if (matches(pos_, ":")) {
     type = Token::Type::kColon;
     pos_ += 1;
-    column_ += 1;
+    location_.column += 1;
   } else if (matches(pos_, ",")) {
     type = Token::Type::kComma;
     pos_ += 1;
-    column_ += 1;
+    location_.column += 1;
   } else if (matches(pos_, "==")) {
     type = Token::Type::kEqualEqual;
     pos_ += 2;
-    column_ += 2;
+    location_.column += 2;
   } else if (matches(pos_, "=")) {
     type = Token::Type::kEqual;
     pos_ += 1;
-    column_ += 1;
+    location_.column += 1;
   } else if (matches(pos_, ">=")) {
     type = Token::Type::kGreaterThanEqual;
     pos_ += 2;
-    column_ += 2;
+    location_.column += 2;
   } else if (matches(pos_, ">")) {
     type = Token::Type::kGreaterThan;
     pos_ += 1;
-    column_ += 1;
+    location_.column += 1;
   } else if (matches(pos_, "<=")) {
     type = Token::Type::kLessThanEqual;
     pos_ += 2;
-    column_ += 2;
+    location_.column += 2;
   } else if (matches(pos_, "<")) {
     type = Token::Type::kLessThan;
     pos_ += 1;
-    column_ += 1;
+    location_.column += 1;
   } else if (matches(pos_, "%")) {
     type = Token::Type::kMod;
     pos_ += 1;
-    column_ += 1;
+    location_.column += 1;
   } else if (matches(pos_, "->")) {
     type = Token::Type::kArrow;
     pos_ += 2;
-    column_ += 2;
+    location_.column += 2;
   } else if (matches(pos_, "-")) {
     type = Token::Type::kMinus;
     pos_ += 1;
-    column_ += 1;
+    location_.column += 1;
   } else if (matches(pos_, ".")) {
     type = Token::Type::kPeriod;
     pos_ += 1;
-    column_ += 1;
+    location_.column += 1;
   } else if (matches(pos_, "+")) {
     type = Token::Type::kPlus;
     pos_ += 1;
-    column_ += 1;
+    location_.column += 1;
   } else if (matches(pos_, "||")) {
     type = Token::Type::kOrOr;
     pos_ += 2;
-    column_ += 2;
+    location_.column += 2;
   } else if (matches(pos_, "|")) {
     type = Token::Type::kOr;
     pos_ += 1;
-    column_ += 1;
+    location_.column += 1;
   } else if (matches(pos_, ";")) {
     type = Token::Type::kSemicolon;
     pos_ += 1;
-    column_ += 1;
+    location_.column += 1;
   } else if (matches(pos_, "*")) {
     type = Token::Type::kStar;
     pos_ += 1;
-    column_ += 1;
+    location_.column += 1;
   } else if (matches(pos_, "^")) {
     type = Token::Type::kXor;
     pos_ += 1;
-    column_ += 1;
+    location_.column += 1;
   }
 
   return {type, source};
diff --git a/src/reader/wgsl/lexer.h b/src/reader/wgsl/lexer.h
index 0a38b50..1bfaf0a 100644
--- a/src/reader/wgsl/lexer.h
+++ b/src/reader/wgsl/lexer.h
@@ -28,8 +28,8 @@
 class Lexer {
  public:
   /// Creates a new Lexer
-  /// @param input the input to parse
-  explicit Lexer(const std::string& input);
+  /// @param file the input file to parse
+  explicit Lexer(Source::File const* file);
   ~Lexer();
 
   /// Returns the next token in the input stream
@@ -40,7 +40,7 @@
   void skip_whitespace();
   void skip_comments();
 
-  Token build_token_from_int_if_possible(const Source& source,
+  Token build_token_from_int_if_possible(Source source,
                                          size_t start,
                                          size_t end,
                                          int32_t base);
@@ -63,15 +63,13 @@
   bool matches(size_t pos, const std::string& substr);
 
   /// The source to parse
-  std::string input_;
+  Source::File const* file_;
   /// The length of the input
   uint32_t len_ = 0;
   /// The current position within the input
   uint32_t pos_ = 0;
-  /// The current line within the input
-  uint32_t line_ = 1;
-  /// The current column within the input
-  uint32_t column_ = 1;
+  /// The current location within the input
+  Source::Location location_;
 };
 
 }  // namespace wgsl
diff --git a/src/reader/wgsl/lexer_test.cc b/src/reader/wgsl/lexer_test.cc
index 81e27e4..bccd13f 100644
--- a/src/reader/wgsl/lexer_test.cc
+++ b/src/reader/wgsl/lexer_test.cc
@@ -26,13 +26,15 @@
 using LexerTest = testing::Test;
 
 TEST_F(LexerTest, Empty) {
-  Lexer l("");
+  Source::File file("test.wgsl", "");
+  Lexer l(&file);
   auto t = l.next();
   EXPECT_TRUE(t.IsEof());
 }
 
 TEST_F(LexerTest, Skips_Whitespace) {
-  Lexer l("\t\r\n\t    ident\t\n\t  \r ");
+  Source::File file("test.wgsl", "\t\r\n\t    ident\t\n\t  \r ");
+  Lexer l(&file);
 
   auto t = l.next();
   EXPECT_TRUE(t.IsIdentifier());
@@ -45,10 +47,11 @@
 }
 
 TEST_F(LexerTest, Skips_Comments) {
-  Lexer l(R"(#starts with comment
+  Source::File file("test.wgsl", R"(#starts with comment
 ident1 #ends with comment
 # blank line
  ident2)");
+  Lexer l(&file);
 
   auto t = l.next();
   EXPECT_TRUE(t.IsIdentifier());
@@ -67,7 +70,8 @@
 }
 
 TEST_F(LexerTest, StringTest_Parse) {
-  Lexer l(R"(id "this is string content" id2)");
+  Source::File file("test.wgsl", R"(id "this is string content" id2)");
+  Lexer l(&file);
 
   auto t = l.next();
   EXPECT_TRUE(t.IsIdentifier());
@@ -89,7 +93,8 @@
 }
 
 TEST_F(LexerTest, StringTest_Unterminated) {
-  Lexer l(R"(id "this is string content)");
+  Source::File file("test.wgsl", R"(id "this is string content)");
+  Lexer l(&file);
 
   auto t = l.next();
   EXPECT_TRUE(t.IsIdentifier());
@@ -116,7 +121,8 @@
 using FloatTest = testing::TestWithParam<FloatData>;
 TEST_P(FloatTest, Parse) {
   auto params = GetParam();
-  Lexer l(std::string(params.input));
+  Source::File file("test.wgsl", params.input);
+  Lexer l(&file);
 
   auto t = l.next();
   EXPECT_TRUE(t.IsFloatLiteral());
@@ -149,7 +155,8 @@
 
 using FloatTest_Invalid = testing::TestWithParam<const char*>;
 TEST_P(FloatTest_Invalid, Handles) {
-  Lexer l(GetParam());
+  Source::File file("test.wgsl", GetParam());
+  Lexer l(&file);
 
   auto t = l.next();
   EXPECT_FALSE(t.IsFloatLiteral());
@@ -166,7 +173,8 @@
 
 using IdentifierTest = testing::TestWithParam<const char*>;
 TEST_P(IdentifierTest, Parse) {
-  Lexer l(GetParam());
+  Source::File file("test.wgsl", GetParam());
+  Lexer l(&file);
 
   auto t = l.next();
   EXPECT_TRUE(t.IsIdentifier());
@@ -180,7 +188,8 @@
     testing::Values("test01", "_test_", "test_", "_test", "_01", "_test01"));
 
 TEST_F(LexerTest, IdentifierTest_DoesNotStartWithNumber) {
-  Lexer l("01test");
+  Source::File file("test.wgsl", "01test");
+  Lexer l(&file);
 
   auto t = l.next();
   EXPECT_FALSE(t.IsIdentifier());
@@ -198,7 +207,8 @@
 using IntegerTest_HexSigned = testing::TestWithParam<HexSignedIntData>;
 TEST_P(IntegerTest_HexSigned, Matches) {
   auto params = GetParam();
-  Lexer l(std::string(params.input));
+  Source::File file("test.wgsl", params.input);
+  Lexer l(&file);
 
   auto t = l.next();
   EXPECT_TRUE(t.IsSintLiteral());
@@ -218,14 +228,16 @@
         HexSignedIntData{"0x7FFFFFFF", std::numeric_limits<int32_t>::max()}));
 
 TEST_F(LexerTest, IntegerTest_HexSignedTooLarge) {
-  Lexer l("0x80000000");
+  Source::File file("test.wgsl", "0x80000000");
+  Lexer l(&file);
   auto t = l.next();
   ASSERT_TRUE(t.IsError());
   EXPECT_EQ(t.to_str(), "i32 (0x80000000) too large");
 }
 
 TEST_F(LexerTest, IntegerTest_HexSignedTooSmall) {
-  Lexer l("-0x8000000F");
+  Source::File file("test.wgsl", "-0x8000000F");
+  Lexer l(&file);
   auto t = l.next();
   ASSERT_TRUE(t.IsError());
   EXPECT_EQ(t.to_str(), "i32 (-0x8000000F) too small");
@@ -242,7 +254,8 @@
 using IntegerTest_HexUnsigned = testing::TestWithParam<HexUnsignedIntData>;
 TEST_P(IntegerTest_HexUnsigned, Matches) {
   auto params = GetParam();
-  Lexer l(std::string(params.input));
+  Source::File file("test.wgsl", params.input);
+  Lexer l(&file);
 
   auto t = l.next();
   EXPECT_TRUE(t.IsUintLiteral());
@@ -265,7 +278,8 @@
                                        std::numeric_limits<uint32_t>::max()}));
 
 TEST_F(LexerTest, IntegerTest_HexUnsignedTooLarge) {
-  Lexer l("0xffffffffffu");
+  Source::File file("test.wgsl", "0xffffffffffu");
+  Lexer l(&file);
   auto t = l.next();
   ASSERT_TRUE(t.IsError());
   EXPECT_EQ(t.to_str(), "u32 (0xffffffffff) too large");
@@ -282,7 +296,8 @@
 using IntegerTest_Unsigned = testing::TestWithParam<UnsignedIntData>;
 TEST_P(IntegerTest_Unsigned, Matches) {
   auto params = GetParam();
-  Lexer l(params.input);
+  Source::File file("test.wgsl", params.input);
+  Lexer l(&file);
 
   auto t = l.next();
   EXPECT_TRUE(t.IsUintLiteral());
@@ -308,7 +323,8 @@
 using IntegerTest_Signed = testing::TestWithParam<SignedIntData>;
 TEST_P(IntegerTest_Signed, Matches) {
   auto params = GetParam();
-  Lexer l(params.input);
+  Source::File file("test.wgsl", params.input);
+  Lexer l(&file);
 
   auto t = l.next();
   EXPECT_TRUE(t.IsSintLiteral());
@@ -328,7 +344,8 @@
 
 using IntegerTest_Invalid = testing::TestWithParam<const char*>;
 TEST_P(IntegerTest_Invalid, Parses) {
-  Lexer l(GetParam());
+  Source::File file("test.wgsl", GetParam());
+  Lexer l(&file);
 
   auto t = l.next();
   EXPECT_FALSE(t.IsSintLiteral());
@@ -349,7 +366,8 @@
 using PunctuationTest = testing::TestWithParam<TokenData>;
 TEST_P(PunctuationTest, Parses) {
   auto params = GetParam();
-  Lexer l(params.input);
+  Source::File file("test.wgsl", params.input);
+  Lexer l(&file);
 
   auto t = l.next();
   EXPECT_TRUE(t.Is(params.type));
@@ -398,7 +416,8 @@
 using KeywordTest = testing::TestWithParam<TokenData>;
 TEST_P(KeywordTest, Parses) {
   auto params = GetParam();
-  Lexer l(params.input);
+  Source::File file("test.wgsl", params.input);
+  Lexer l(&file);
 
   auto t = l.next();
   EXPECT_TRUE(t.Is(params.type)) << params.input;
@@ -547,7 +566,8 @@
 using KeywordTest_Reserved = testing::TestWithParam<const char*>;
 TEST_P(KeywordTest_Reserved, Parses) {
   auto* keyword = GetParam();
-  Lexer l(keyword);
+  Source::File file("test.wgsl", keyword);
+  Lexer l(&file);
 
   auto t = l.next();
   EXPECT_TRUE(t.IsReservedKeyword());
diff --git a/src/reader/wgsl/parser.cc b/src/reader/wgsl/parser.cc
index 2b2adc7..e24066a 100644
--- a/src/reader/wgsl/parser.cc
+++ b/src/reader/wgsl/parser.cc
@@ -20,8 +20,14 @@
 namespace reader {
 namespace wgsl {
 
-Parser::Parser(Context* ctx, const std::string& input)
-    : Reader(ctx), impl_(std::make_unique<ParserImpl>(ctx, input)) {}
+Parser::Parser(Context* ctx, Source::File const* file)
+    : Reader(ctx), impl_(std::make_unique<ParserImpl>(ctx, file, false)) {}
+
+Parser::Parser(Context* ctx, const std::string& content)
+    : Reader(ctx),
+      impl_(std::make_unique<ParserImpl>(ctx,
+                                         new Source::File("", content),
+                                         true)) {}
 
 Parser::~Parser() = default;
 
diff --git a/src/reader/wgsl/parser.h b/src/reader/wgsl/parser.h
index 6fe3390..ff835b8 100644
--- a/src/reader/wgsl/parser.h
+++ b/src/reader/wgsl/parser.h
@@ -19,6 +19,7 @@
 #include <string>
 
 #include "src/reader/reader.h"
+#include "src/source.h"
 
 namespace tint {
 namespace reader {
@@ -29,10 +30,18 @@
 /// Parser for WGSL source data
 class Parser : public Reader {
  public:
-  /// Creates a new parser
+  /// Creates a new parser from the given file.
   /// @param ctx the non-null context object
-  /// @param input the input string to parse
-  Parser(Context* ctx, const std::string& input);
+  /// @param file the input source file to parse
+  Parser(Context* ctx, Source::File const* file);
+
+  /// Creates a new parser from the given file content.
+  /// @param ctx the non-null context object
+  /// @param content the input string to parse
+  /// TODO(bclayton): Remove this constructor.
+  /// It purely exists to break up changes into bite sized pieces.
+  Parser(Context* ctx, const std::string& content);
+
   ~Parser() override;
 
   /// Run the parser
diff --git a/src/reader/wgsl/parser_impl.cc b/src/reader/wgsl/parser_impl.cc
index df0ddfd..e8100f7 100644
--- a/src/reader/wgsl/parser_impl.cc
+++ b/src/reader/wgsl/parser_impl.cc
@@ -119,10 +119,17 @@
 
 }  // namespace
 
-ParserImpl::ParserImpl(Context* ctx, const std::string& input)
-    : ctx_(*ctx), lexer_(std::make_unique<Lexer>(input)) {}
+ParserImpl::ParserImpl(Context* ctx, Source::File const* file, bool owns_file)
+    : ctx_(*ctx),
+      lexer_(std::make_unique<Lexer>(file)),
+      file_(file),
+      owns_file_(owns_file) {}
 
-ParserImpl::~ParserImpl() = default;
+ParserImpl::~ParserImpl() {
+  if (owns_file_) {
+    delete file_;
+  }
+}
 
 void ParserImpl::set_error(const Token& t, const std::string& err) {
   auto prefix =
diff --git a/src/reader/wgsl/parser_impl.h b/src/reader/wgsl/parser_impl.h
index 24c4a0c..4fb3644 100644
--- a/src/reader/wgsl/parser_impl.h
+++ b/src/reader/wgsl/parser_impl.h
@@ -76,17 +76,21 @@
 /// ParserImpl for WGSL source data
 class ParserImpl {
  public:
-  /// Creates a new parser
+  /// Creates a new parser using the given file
   /// @param ctx the non-null context object
-  /// @param input the input string to parse
-  ParserImpl(Context* ctx, const std::string& input);
+  /// @param file the input source file to parse
+  /// @param owns_file if true, the file will be deleted on parser destruction.
+  /// TODO(bclayton): Remove owns_file.
+  /// It purely exists to break up changes into bite sized pieces.
+  ParserImpl(Context* ctx, Source::File const* file, bool owns_file = false);
+
   ~ParserImpl();
 
   /// Run the parser
   /// @returns true if the parse was successful, false otherwise.
   bool Parse();
 
-  /// @returns true if an error was encountered
+  /// @returns true if an error was encountered.
   bool has_error() const { return error_.size() > 0; }
   /// @returns the parser error string
   const std::string& error() const { return error_; }
@@ -411,6 +415,9 @@
   std::deque<Token> token_queue_;
   std::unordered_map<std::string, ast::type::Type*> registered_constructs_;
   ast::Module module_;
+
+  Source::File const* file_;
+  bool owns_file_;
 };
 
 }  // namespace wgsl
diff --git a/src/reader/wgsl/parser_impl_test_helper.cc b/src/reader/wgsl/parser_impl_test_helper.cc
index d3746cf..4621823 100644
--- a/src/reader/wgsl/parser_impl_test_helper.cc
+++ b/src/reader/wgsl/parser_impl_test_helper.cc
@@ -27,6 +27,7 @@
 
 void ParserImplTest::TearDown() {
   impl_ = nullptr;
+  files_.clear();
 }
 
 }  // namespace wgsl
diff --git a/src/reader/wgsl/parser_impl_test_helper.h b/src/reader/wgsl/parser_impl_test_helper.h
index 0961b15..cbefc11 100644
--- a/src/reader/wgsl/parser_impl_test_helper.h
+++ b/src/reader/wgsl/parser_impl_test_helper.h
@@ -43,7 +43,9 @@
   /// @param str the string to parse
   /// @returns the parser implementation
   ParserImpl* parser(const std::string& str) {
-    impl_ = std::make_unique<ParserImpl>(&ctx_, str);
+    auto file = std::make_unique<Source::File>("test.wgsl", str);
+    impl_ = std::make_unique<ParserImpl>(&ctx_, file.get());
+    files_.emplace_back(std::move(file));
     return impl_.get();
   }
 
@@ -51,6 +53,7 @@
   TypeManager* tm() { return &(ctx_.type_mgr()); }
 
  private:
+  std::vector<std::unique_ptr<Source::File>> files_;
   std::unique_ptr<ParserImpl> impl_;
   Context ctx_;
 };
@@ -67,13 +70,18 @@
   void SetUp() override { ctx_.Reset(); }
 
   /// Tears down the test helper
-  void TearDown() override { impl_ = nullptr; }
+  void TearDown() override {
+    impl_ = nullptr;
+    files_.clear();
+  }
 
   /// Retrieves the parser from the helper
   /// @param str the string to parse
   /// @returns the parser implementation
   ParserImpl* parser(const std::string& str) {
-    impl_ = std::make_unique<ParserImpl>(&ctx_, str);
+    auto file = std::make_unique<Source::File>("test.wgsl", str);
+    impl_ = std::make_unique<ParserImpl>(&ctx_, file.get());
+    files_.emplace_back(std::move(file));
     return impl_.get();
   }
 
@@ -81,6 +89,7 @@
   TypeManager* tm() { return &(ctx_.type_mgr()); }
 
  private:
+  std::vector<std::unique_ptr<Source::File>> files_;
   std::unique_ptr<ParserImpl> impl_;
   Context ctx_;
 };
diff --git a/src/reader/wgsl/parser_impl_variable_decl_test.cc b/src/reader/wgsl/parser_impl_variable_decl_test.cc
index 05519c7..2598376 100644
--- a/src/reader/wgsl/parser_impl_variable_decl_test.cc
+++ b/src/reader/wgsl/parser_impl_variable_decl_test.cc
@@ -29,8 +29,8 @@
   ASSERT_NE(var, nullptr);
   ASSERT_EQ(var->name(), "my_var");
   ASSERT_NE(var->type(), nullptr);
-  ASSERT_EQ(var->source().line, 1u);
-  ASSERT_EQ(var->source().column, 1u);
+  ASSERT_EQ(var->source().range.begin.line, 1u);
+  ASSERT_EQ(var->source().range.begin.column, 1u);
   ASSERT_TRUE(var->type()->IsF32());
 }
 
diff --git a/src/reader/wgsl/parser_test.cc b/src/reader/wgsl/parser_test.cc
index 363da93..01990fd 100644
--- a/src/reader/wgsl/parser_test.cc
+++ b/src/reader/wgsl/parser_test.cc
@@ -26,14 +26,14 @@
 
 TEST_F(ParserTest, Empty) {
   Context ctx;
-  Parser p(&ctx, "");
+  Source::File file("test.wgsl", "");
+  Parser p(&ctx, &file);
   ASSERT_TRUE(p.Parse()) << p.error();
 }
 
 TEST_F(ParserTest, Parses) {
   Context ctx;
-
-  Parser p(&ctx, R"(
+  Source::File file("test.wgsl", R"(
 [[location(0)]] var<out> gl_FragColor : vec4<f32>;
 
 [[stage(vertex)]]
@@ -41,6 +41,7 @@
   gl_FragColor = vec4<f32>(.4, .2, .3, 1);
 }
 )");
+  Parser p(&ctx, &file);
   ASSERT_TRUE(p.Parse()) << p.error();
 
   auto m = p.module();
@@ -50,10 +51,11 @@
 
 TEST_F(ParserTest, HandlesError) {
   Context ctx;
-  Parser p(&ctx, R"(
+  Source::File file("test.wgsl", R"(
 fn main() ->  {  # missing return type
   return;
 })");
+  Parser p(&ctx, &file);
 
   ASSERT_FALSE(p.Parse());
   ASSERT_TRUE(p.has_error());
diff --git a/src/reader/wgsl/token.h b/src/reader/wgsl/token.h
index 184d1d9..9d92061 100644
--- a/src/reader/wgsl/token.h
+++ b/src/reader/wgsl/token.h
@@ -778,10 +778,12 @@
   /// @returns true if token is a 'workgroup_size'
   bool IsWorkgroupSize() const { return type_ == Type::kWorkgroupSize; }
 
+  // TODO(bclayton): Deprecate - use source().range.begin instead
   /// @returns the source line of the token
-  size_t line() const { return source_.line; }
+  size_t line() const { return source_.range.begin.line; }
   /// @returns the source column of the token
-  size_t column() const { return source_.column; }
+  size_t column() const { return source_.range.begin.column; }
+
   /// @returns the source information for this token
   Source source() const { return source_; }
 
diff --git a/src/source.cc b/src/source.cc
new file mode 100644
index 0000000..d148453
--- /dev/null
+++ b/src/source.cc
@@ -0,0 +1,38 @@
+// 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 <sstream>
+
+#include "source.h"
+
+namespace tint {
+namespace {
+std::vector<std::string> split_lines(const std::string& str) {
+  std::stringstream stream(str);
+  std::string line;
+  std::vector<std::string> lines;
+  while (std::getline(stream, line, '\n')) {
+    lines.emplace_back(std::move(line));
+  }
+  return lines;
+}
+}  // namespace
+
+Source::File::File(const std::string& file_path,
+                   const std::string& file_content)
+    : path(file_path), content(file_content), lines(split_lines(content)) {}
+
+Source::File::~File() = default;
+
+}  // namespace tint
diff --git a/src/source.h b/src/source.h
index 0305350..20242c9 100644
--- a/src/source.h
+++ b/src/source.h
@@ -18,14 +18,71 @@
 
 #include <stddef.h>
 
+#include <string>
+#include <vector>
+
 namespace tint {
 
-/// Represents a line and column position
-struct Source {
-  /// The line the token appeared on
-  size_t line = 0;
-  /// The column the token appeared in
-  size_t column = 0;
+/// Source describes a range of characters within a source file.
+class Source {
+ public:
+  /// File describes a source file, including path and content.
+  class File {
+   public:
+    /// Constructs the File with the given file path and content.
+    File(const std::string& file_path, const std::string& file_content);
+    ~File();
+
+    const std::string path;                /// file path (optional)
+    const std::string content;             /// file content
+    const std::vector<std::string> lines;  /// |content| split by lines
+  };
+
+  /// Location holds a 1-based line and column index.
+  /// 0's for |line| or |column| represent invalid values.
+  class Location {
+   public:
+    size_t line = 0;
+    size_t column = 0;
+  };
+
+  /// Range holds a Location interval described by [begin, end).
+  class Range {
+   public:
+    /// Constructs a zero initialized Range.
+    inline Range() = default;
+
+    /// Constructs a zero-length Range starting at |loc|.
+    inline explicit Range(const Location& loc) : begin(loc), end(loc) {}
+
+    /// Constructs the Range beginning at |b| and ending at |e|.
+    inline Range(const Location& b, const Location& e) : begin(b), end(e) {}
+
+    Location begin;  /// The location of the first character in the range.
+    Location end;  /// The location of one-past the last character in the range.
+  };
+
+  /// Constructs the Source with an zero initialized Range and null File.
+  inline Source() = default;
+
+  /// Constructs the Source with the Range |rng| and a null File.
+  inline explicit Source(const Range& rng) : range(rng) {}
+
+  /// Constructs the Source with the Range |loc| and a null File.
+  inline explicit Source(const Location& loc) : range(Range(loc)) {}
+
+  /// Constructs the Source with the Range |rng| and File |f|.
+  inline Source(const Range& rng, File const* f) : range(rng), file(f) {}
+
+  /// Constructs the Source with the zero-length range starting at |line| and
+  /// |column| with a null File.
+  /// TODO(bclayton): Remove this constructor.
+  /// It purely exists to break up changes into bite sized pieces.
+  inline explicit Source(size_t line, size_t column)
+      : Source(Location{line, column}) {}
+
+  Range range;
+  File const* file = nullptr;
 };
 
 }  // namespace tint
diff --git a/src/type_determiner.cc b/src/type_determiner.cc
index 2686d88..0e5656d 100644
--- a/src/type_determiner.cc
+++ b/src/type_determiner.cc
@@ -62,9 +62,9 @@
 
 void TypeDeterminer::set_error(const Source& src, const std::string& msg) {
   error_ = "";
-  if (src.line > 0) {
-    error_ +=
-        std::to_string(src.line) + ":" + std::to_string(src.column) + ": ";
+  if (src.range.begin.line > 0) {
+    error_ += std::to_string(src.range.begin.line) + ":" +
+              std::to_string(src.range.begin.column) + ": ";
   }
   error_ += msg;
 }
diff --git a/src/validator_impl.cc b/src/validator_impl.cc
index 585bb18..78e09f1 100644
--- a/src/validator_impl.cc
+++ b/src/validator_impl.cc
@@ -36,8 +36,8 @@
 ValidatorImpl::~ValidatorImpl() = default;
 
 void ValidatorImpl::set_error(const Source& src, const std::string& msg) {
-  error_ +=
-      std::to_string(src.line) + ":" + std::to_string(src.column) + ": " + msg;
+  error_ += std::to_string(src.range.begin.line) + ":" +
+            std::to_string(src.range.begin.column) + ": " + msg;
 }
 
 bool ValidatorImpl::Validate(const ast::Module* module) {