// Copyright 2021 The Dawn & Tint Authors
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
//  1. Redistributions of source code must retain the above copyright notice, this
//     list of conditions and the following disclaimer.
//
//  2. Redistributions in binary form must reproduce the above copyright notice,
//     this list of conditions and the following disclaimer in the documentation
//     and/or other materials provided with the distribution.
//
//  3. Neither the name of the copyright holder nor the names of its
//     contributors may be used to endorse or promote products derived from
//     this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

package parser_test

import (
	"testing"

	"dawn.googlesource.com/dawn/tools/src/fileutils"
	"dawn.googlesource.com/dawn/tools/src/oswrapper"
	"dawn.googlesource.com/dawn/tools/src/tint/intrinsic/ast"
	"dawn.googlesource.com/dawn/tools/src/tint/intrinsic/parser"
	"github.com/google/go-cmp/cmp"
)

var ignoreSource = cmp.FilterPath(func(p cmp.Path) bool {
	return p.Last().String() == ".Source"
}, cmp.Ignore())

func TestParser(t *testing.T) {
	type test struct {
		location string
		src      string
		expect   ast.AST
	}

	for _, test := range []test{
		{
			fileutils.ThisLine(),
			"enum E {}",
			ast.AST{
				Enums: []ast.EnumDecl{{Name: "E"}},
			},
		}, { ///////////////////////////////////////////////////////////////////
			fileutils.ThisLine(),
			"enum E { A @attr B C }",
			ast.AST{
				Enums: []ast.EnumDecl{{
					Name: "E",
					Entries: []ast.EnumEntry{
						{Name: "A"},
						{
							Attributes: ast.Attributes{{
								Name:   "attr",
								Values: nil,
							}},
							Name: "B",
						},
						{Name: "C"},
					},
				}},
			},
		}, { ///////////////////////////////////////////////////////////////////
			fileutils.ThisLine(),
			"type T",
			ast.AST{
				Types: []ast.TypeDecl{{Name: "T"}},
			},
		}, { ///////////////////////////////////////////////////////////////////
			fileutils.ThisLine(),
			"type T<A, B, C>",
			ast.AST{
				Types: []ast.TypeDecl{{
					Name: "T",
					TemplateParams: []ast.TemplateParam{
						{Name: "A"},
						{Name: "B"},
						{Name: "C"},
					},
				}},
			},
		}, { ///////////////////////////////////////////////////////////////////
			fileutils.ThisLine(),
			"@attr type T",
			ast.AST{
				Types: []ast.TypeDecl{{
					Attributes: ast.Attributes{
						{Name: "attr", Values: nil},
					},
					Name: "T",
				}},
			},
		}, { ///////////////////////////////////////////////////////////////////
			fileutils.ThisLine(),
			"@attr_a @attr_b type T",
			ast.AST{
				Types: []ast.TypeDecl{{
					Attributes: ast.Attributes{
						{Name: "attr_a", Values: nil},
						{Name: "attr_b", Values: nil},
					},
					Name: "T",
				}},
			},
		}, { ///////////////////////////////////////////////////////////////////
			fileutils.ThisLine(),
			`@attr("a", "b") type T`, ast.AST{
				Types: []ast.TypeDecl{{
					Attributes: ast.Attributes{
						{Name: "attr", Values: []any{"a", "b"}},
					},
					Name: "T",
				}},
			},
		}, { ///////////////////////////////////////////////////////////////////
			fileutils.ThisLine(),
			`@attr(1, "x", 2.0) type T`, ast.AST{
				Types: []ast.TypeDecl{{
					Attributes: ast.Attributes{
						{Name: "attr", Values: []any{1, "x", 2.0}},
					},
					Name: "T",
				}},
			},
		}, { ///////////////////////////////////////////////////////////////////
			fileutils.ThisLine(),
			"match M : A",
			ast.AST{
				Matchers: []ast.MatcherDecl{{
					Name: "M",
					Options: ast.MatcherOptions{
						Types: ast.TemplatedNames{
							{Name: "A"},
						},
					},
				}},
			},
		}, { ///////////////////////////////////////////////////////////////////
			fileutils.ThisLine(),
			"match M : A | B",
			ast.AST{
				Matchers: []ast.MatcherDecl{{
					Name: "M",
					Options: ast.MatcherOptions{
						Types: ast.TemplatedNames{
							{Name: "A"},
							{Name: "B"},
						},
					},
				}},
			},
		}, { ///////////////////////////////////////////////////////////////////
			fileutils.ThisLine(),
			"match M : A.B",
			ast.AST{
				Matchers: []ast.MatcherDecl{{
					Name: "M",
					Options: ast.MatcherOptions{
						Enums: ast.MemberNames{
							{Owner: "A", Member: "B"},
						},
					},
				}},
			},
		}, { ///////////////////////////////////////////////////////////////////
			fileutils.ThisLine(),
			"match M : A.B | B.C",
			ast.AST{
				Matchers: []ast.MatcherDecl{{
					Name: "M",
					Options: ast.MatcherOptions{
						Enums: ast.MemberNames{
							{Owner: "A", Member: "B"},
							{Owner: "B", Member: "C"},
						},
					},
				}},
			},
		}, { ///////////////////////////////////////////////////////////////////
			fileutils.ThisLine(),
			"fn F()",
			ast.AST{
				Builtins: []ast.IntrinsicDecl{{
					Kind:       ast.Builtin,
					Name:       "F",
					Parameters: ast.Parameters{},
				}},
			},
		}, { ///////////////////////////////////////////////////////////////////
			fileutils.ThisLine(),
			"@attr fn F()",
			ast.AST{
				Builtins: []ast.IntrinsicDecl{{
					Kind: ast.Builtin,
					Name: "F",
					Attributes: ast.Attributes{
						{Name: "attr", Values: nil},
					},
					Parameters: ast.Parameters{},
				}},
			},
		}, { ///////////////////////////////////////////////////////////////////
			fileutils.ThisLine(),
			"fn F(a)",
			ast.AST{
				Builtins: []ast.IntrinsicDecl{{
					Kind: ast.Builtin,
					Name: "F",
					Parameters: ast.Parameters{
						{Type: ast.TemplatedName{Name: "a"}},
					},
				}},
			},
		}, { ///////////////////////////////////////////////////////////////////
			fileutils.ThisLine(),
			"fn F(a: T)",
			ast.AST{
				Builtins: []ast.IntrinsicDecl{{
					Kind: ast.Builtin,
					Name: "F",
					Parameters: ast.Parameters{
						{Name: "a", Type: ast.TemplatedName{Name: "T"}},
					},
				}},
			},
		}, { ///////////////////////////////////////////////////////////////////
			fileutils.ThisLine(),
			"fn F(a, b)",
			ast.AST{
				Builtins: []ast.IntrinsicDecl{{
					Kind: ast.Builtin,
					Name: "F",
					Parameters: ast.Parameters{
						{Type: ast.TemplatedName{Name: "a"}},
						{Type: ast.TemplatedName{Name: "b"}},
					},
				}},
			},
		}, { ///////////////////////////////////////////////////////////////////
			fileutils.ThisLine(),
			"fn F<A : B<C> >()",
			ast.AST{
				Builtins: []ast.IntrinsicDecl{{
					Kind: ast.Builtin,
					Name: "F",
					ExplicitTemplateParams: []ast.TemplateParam{
						{
							Name: "A", Type: ast.TemplatedName{
								Name: "B",
								TemplateArgs: ast.TemplatedNames{
									{Name: "C"},
								},
							},
						},
					},
					Parameters: ast.Parameters{},
				}},
			},
		}, { ///////////////////////////////////////////////////////////////////
			fileutils.ThisLine(),
			"fn F<T>(a: X, b: Y<T>)",
			ast.AST{
				Builtins: []ast.IntrinsicDecl{{
					Kind: ast.Builtin,
					Name: "F",
					ExplicitTemplateParams: []ast.TemplateParam{
						{Name: "T"},
					},
					Parameters: ast.Parameters{
						{Name: "a", Type: ast.TemplatedName{Name: "X"}},
						{Name: "b", Type: ast.TemplatedName{
							Name:         "Y",
							TemplateArgs: ast.TemplatedNames{{Name: "T"}},
						}},
					},
				}},
			},
		}, { ///////////////////////////////////////////////////////////////////
			fileutils.ThisLine(),
			"implicit(A : B<C>) fn F()",
			ast.AST{
				Builtins: []ast.IntrinsicDecl{{
					Kind: ast.Builtin,
					Name: "F",
					ImplicitTemplateParams: []ast.TemplateParam{
						{
							Name: "A",
							Type: ast.TemplatedName{
								Name: "B",
								TemplateArgs: ast.TemplatedNames{
									{Name: "C"},
								},
							},
						},
					},
					Parameters: ast.Parameters{},
				}},
			},
		}, { ///////////////////////////////////////////////////////////////////
			fileutils.ThisLine(),
			"implicit(T) fn F(a: X, b: Y<T>)",
			ast.AST{
				Builtins: []ast.IntrinsicDecl{{
					Kind: ast.Builtin,
					Name: "F",
					ImplicitTemplateParams: []ast.TemplateParam{
						{Name: "T"},
					},
					Parameters: ast.Parameters{
						{Name: "a", Type: ast.TemplatedName{Name: "X"}},
						{Name: "b", Type: ast.TemplatedName{
							Name:         "Y",
							TemplateArgs: ast.TemplatedNames{{Name: "T"}},
						}},
					},
				}},
			},
		}, { ///////////////////////////////////////////////////////////////////
			fileutils.ThisLine(),
			"implicit(B: C<D>) fn F<A>()",
			ast.AST{
				Builtins: []ast.IntrinsicDecl{{
					Kind: ast.Builtin,
					Name: "F",
					ExplicitTemplateParams: []ast.TemplateParam{
						{Name: "A"},
					},
					ImplicitTemplateParams: []ast.TemplateParam{
						{
							Name: "B",
							Type: ast.TemplatedName{
								Name: "C",
								TemplateArgs: ast.TemplatedNames{
									{Name: "D"},
								},
							},
						},
					},
					Parameters: ast.Parameters{},
				}},
			},
		}, { ///////////////////////////////////////////////////////////////////
			fileutils.ThisLine(),
			"implicit(T) fn F(a: X, b: Y<T>)",
			ast.AST{
				Builtins: []ast.IntrinsicDecl{{
					Kind: ast.Builtin,
					Name: "F",
					ImplicitTemplateParams: []ast.TemplateParam{
						{Name: "T"},
					},
					Parameters: ast.Parameters{
						{Name: "a", Type: ast.TemplatedName{Name: "X"}},
						{Name: "b", Type: ast.TemplatedName{
							Name:         "Y",
							TemplateArgs: ast.TemplatedNames{{Name: "T"}},
						}},
					},
				}},
			},
		}, { ///////////////////////////////////////////////////////////////////
			fileutils.ThisLine(),
			"fn F() -> X",
			ast.AST{
				Builtins: []ast.IntrinsicDecl{{
					Kind:       ast.Builtin,
					Name:       "F",
					ReturnType: &ast.TemplatedName{Name: "X"},
					Parameters: ast.Parameters{},
				}},
			},
		}, { ///////////////////////////////////////////////////////////////////
			fileutils.ThisLine(),
			"fn F() -> X<T>",
			ast.AST{
				Builtins: []ast.IntrinsicDecl{{
					Kind: ast.Builtin,
					Name: "F",
					ReturnType: &ast.TemplatedName{
						Name:         "X",
						TemplateArgs: ast.TemplatedNames{{Name: "T"}},
					},
					Parameters: ast.Parameters{},
				}},
			},
		}, { ///////////////////////////////////////////////////////////////////
			fileutils.ThisLine(),
			"op F()",
			ast.AST{
				Operators: []ast.IntrinsicDecl{{
					Kind:       ast.Operator,
					Name:       "F",
					Parameters: ast.Parameters{},
				}},
			},
		}, { ///////////////////////////////////////////////////////////////////
			fileutils.ThisLine(),
			"@attr op F()",
			ast.AST{
				Operators: []ast.IntrinsicDecl{{
					Kind: ast.Operator,
					Name: "F",
					Attributes: ast.Attributes{
						{Name: "attr", Values: nil},
					},
					Parameters: ast.Parameters{},
				}},
			},
		}, { ///////////////////////////////////////////////////////////////////
			fileutils.ThisLine(),
			"op F(a)",
			ast.AST{
				Operators: []ast.IntrinsicDecl{{
					Kind: ast.Operator,
					Name: "F",
					Parameters: ast.Parameters{
						{Type: ast.TemplatedName{Name: "a"}},
					},
				}},
			},
		}, { ///////////////////////////////////////////////////////////////////
			fileutils.ThisLine(),
			"op F(@blah a)",
			ast.AST{
				Operators: []ast.IntrinsicDecl{{
					Kind: ast.Operator,
					Name: "F",
					Parameters: ast.Parameters{
						{
							Attributes: ast.Attributes{{Name: "blah", Values: nil}},
							Type:       ast.TemplatedName{Name: "a"}},
					},
				}},
			},
		}, { ///////////////////////////////////////////////////////////////////
			fileutils.ThisLine(),
			"op F(a: T)",
			ast.AST{
				Operators: []ast.IntrinsicDecl{{
					Kind: ast.Operator,
					Name: "F",
					Parameters: ast.Parameters{
						{Name: "a", Type: ast.TemplatedName{Name: "T"}},
					},
				}},
			},
		}, { ///////////////////////////////////////////////////////////////////
			fileutils.ThisLine(),
			"op F(a, b)",
			ast.AST{
				Operators: []ast.IntrinsicDecl{{
					Kind: ast.Operator,
					Name: "F",
					Parameters: ast.Parameters{
						{Type: ast.TemplatedName{Name: "a"}},
						{Type: ast.TemplatedName{Name: "b"}},
					},
				}},
			},
		}, { ///////////////////////////////////////////////////////////////////
			fileutils.ThisLine(),
			"op F<A : B<C> >()",
			ast.AST{
				Operators: []ast.IntrinsicDecl{{
					Kind: ast.Operator,
					Name: "F",
					ExplicitTemplateParams: []ast.TemplateParam{
						{
							Name: "A", Type: ast.TemplatedName{
								Name: "B",
								TemplateArgs: ast.TemplatedNames{
									{Name: "C"},
								},
							},
						},
					},
					Parameters: ast.Parameters{},
				}},
			},
		}, { ///////////////////////////////////////////////////////////////////
			fileutils.ThisLine(),
			"op F<T>(a: X, b: Y<T>)",
			ast.AST{
				Operators: []ast.IntrinsicDecl{{
					Kind: ast.Operator,
					Name: "F",
					ExplicitTemplateParams: []ast.TemplateParam{
						{Name: "T"},
					},
					Parameters: ast.Parameters{
						{Name: "a", Type: ast.TemplatedName{Name: "X"}},
						{Name: "b", Type: ast.TemplatedName{
							Name:         "Y",
							TemplateArgs: ast.TemplatedNames{{Name: "T"}},
						}},
					},
				}},
			},
		}, { ///////////////////////////////////////////////////////////////////
			fileutils.ThisLine(),
			"implicit(A : B<C>) op F()",
			ast.AST{
				Operators: []ast.IntrinsicDecl{{
					Kind: ast.Operator,
					Name: "F",
					ImplicitTemplateParams: []ast.TemplateParam{
						{
							Name: "A", Type: ast.TemplatedName{
								Name: "B",
								TemplateArgs: ast.TemplatedNames{
									{Name: "C"},
								},
							},
						},
					},
					Parameters: ast.Parameters{},
				}},
			},
		}, { ///////////////////////////////////////////////////////////////////
			fileutils.ThisLine(),
			"implicit(T) op F(a: X, b: Y<T>)",
			ast.AST{
				Operators: []ast.IntrinsicDecl{{
					Kind: ast.Operator,
					Name: "F",
					ImplicitTemplateParams: []ast.TemplateParam{
						{Name: "T"},
					},
					Parameters: ast.Parameters{
						{Name: "a", Type: ast.TemplatedName{Name: "X"}},
						{Name: "b", Type: ast.TemplatedName{
							Name:         "Y",
							TemplateArgs: ast.TemplatedNames{{Name: "T"}},
						}},
					},
				}},
			},
		}, { ///////////////////////////////////////////////////////////////////
			fileutils.ThisLine(),
			"implicit(D) op F<A : B<C> >()",
			ast.AST{
				Operators: []ast.IntrinsicDecl{{
					Kind: ast.Operator,
					Name: "F",
					ExplicitTemplateParams: []ast.TemplateParam{
						{
							Name: "A", Type: ast.TemplatedName{
								Name: "B",
								TemplateArgs: ast.TemplatedNames{
									{Name: "C"},
								},
							},
						},
					},
					ImplicitTemplateParams: []ast.TemplateParam{
						{Name: "D"},
					},
					Parameters: ast.Parameters{},
				}},
			},
		}, { ///////////////////////////////////////////////////////////////////
			fileutils.ThisLine(),
			"op F() -> X",
			ast.AST{
				Operators: []ast.IntrinsicDecl{{
					Kind:       ast.Operator,
					Name:       "F",
					ReturnType: &ast.TemplatedName{Name: "X"},
					Parameters: ast.Parameters{},
				}},
			},
		}, { ///////////////////////////////////////////////////////////////////
			fileutils.ThisLine(),
			"op F() -> X<T>",
			ast.AST{
				Operators: []ast.IntrinsicDecl{{
					Kind: ast.Operator,
					Name: "F",
					ReturnType: &ast.TemplatedName{
						Name:         "X",
						TemplateArgs: ast.TemplatedNames{{Name: "T"}},
					},
					Parameters: ast.Parameters{},
				}},
			},
		}, { ///////////////////////////////////////////////////////////////////
			fileutils.ThisLine(),
			"ctor F()",
			ast.AST{
				Constructors: []ast.IntrinsicDecl{{
					Kind:       ast.Constructor,
					Name:       "F",
					Parameters: ast.Parameters{},
				}},
			},
		}, { ///////////////////////////////////////////////////////////////////
			fileutils.ThisLine(),
			"@attr ctor F()",
			ast.AST{
				Constructors: []ast.IntrinsicDecl{{
					Kind: ast.Constructor,
					Name: "F",
					Attributes: ast.Attributes{
						{Name: "attr", Values: nil},
					},
					Parameters: ast.Parameters{},
				}},
			},
		}, { ///////////////////////////////////////////////////////////////////
			fileutils.ThisLine(),
			"ctor F(a)",
			ast.AST{
				Constructors: []ast.IntrinsicDecl{{
					Kind: ast.Constructor,
					Name: "F",
					Parameters: ast.Parameters{
						{Type: ast.TemplatedName{Name: "a"}},
					},
				}},
			},
		}, { ///////////////////////////////////////////////////////////////////
			fileutils.ThisLine(),
			"ctor F(a: T)",
			ast.AST{
				Constructors: []ast.IntrinsicDecl{{
					Kind: ast.Constructor,
					Name: "F",
					Parameters: ast.Parameters{
						{Name: "a", Type: ast.TemplatedName{Name: "T"}},
					},
				}},
			},
		}, { ///////////////////////////////////////////////////////////////////
			fileutils.ThisLine(),
			"ctor F(a, b)",
			ast.AST{
				Constructors: []ast.IntrinsicDecl{{
					Kind: ast.Constructor,
					Name: "F",
					Parameters: ast.Parameters{
						{Type: ast.TemplatedName{Name: "a"}},
						{Type: ast.TemplatedName{Name: "b"}},
					},
				}},
			},
		}, { ///////////////////////////////////////////////////////////////////
			fileutils.ThisLine(),
			"ctor F<A : B<C> >()",
			ast.AST{
				Constructors: []ast.IntrinsicDecl{{
					Kind: ast.Constructor,
					Name: "F",
					ExplicitTemplateParams: []ast.TemplateParam{
						{
							Name: "A", Type: ast.TemplatedName{
								Name: "B",
								TemplateArgs: ast.TemplatedNames{
									{Name: "C"},
								},
							},
						},
					},
					Parameters: ast.Parameters{},
				}},
			},
		}, { ///////////////////////////////////////////////////////////////////
			fileutils.ThisLine(),
			"ctor F<T>(a: X, b: Y<T>)",
			ast.AST{
				Constructors: []ast.IntrinsicDecl{{
					Kind: ast.Constructor,
					Name: "F",
					ExplicitTemplateParams: []ast.TemplateParam{
						{Name: "T"},
					},
					Parameters: ast.Parameters{
						{Name: "a", Type: ast.TemplatedName{Name: "X"}},
						{Name: "b", Type: ast.TemplatedName{
							Name:         "Y",
							TemplateArgs: ast.TemplatedNames{{Name: "T"}},
						}},
					},
				}},
			},
		}, { ///////////////////////////////////////////////////////////////////
			fileutils.ThisLine(),
			"ctor F() -> X",
			ast.AST{
				Constructors: []ast.IntrinsicDecl{{
					Kind:       ast.Constructor,
					Name:       "F",
					ReturnType: &ast.TemplatedName{Name: "X"},
					Parameters: ast.Parameters{},
				}},
			},
		}, { ///////////////////////////////////////////////////////////////////
			fileutils.ThisLine(),
			"ctor F() -> X<T>",
			ast.AST{
				Constructors: []ast.IntrinsicDecl{{
					Kind: ast.Constructor,
					Name: "F",
					ReturnType: &ast.TemplatedName{
						Name:         "X",
						TemplateArgs: ast.TemplatedNames{{Name: "T"}},
					},
					Parameters: ast.Parameters{},
				}},
			},
		}, { ///////////////////////////////////////////////////////////////////
			fileutils.ThisLine(),
			"conv F()",
			ast.AST{
				Converters: []ast.IntrinsicDecl{{
					Kind:       ast.Converter,
					Name:       "F",
					Parameters: ast.Parameters{},
				}},
			},
		}, { ///////////////////////////////////////////////////////////////////
			fileutils.ThisLine(),
			"@attr conv F()",
			ast.AST{
				Converters: []ast.IntrinsicDecl{{
					Kind: ast.Converter,
					Name: "F",
					Attributes: ast.Attributes{
						{Name: "attr", Values: nil},
					},
					Parameters: ast.Parameters{},
				}},
			},
		}, { ///////////////////////////////////////////////////////////////////
			fileutils.ThisLine(),
			"conv F(a)",
			ast.AST{
				Converters: []ast.IntrinsicDecl{{
					Kind: ast.Converter,
					Name: "F",
					Parameters: ast.Parameters{
						{Type: ast.TemplatedName{Name: "a"}},
					},
				}},
			},
		}, { ///////////////////////////////////////////////////////////////////
			fileutils.ThisLine(),
			"conv F(a: T)",
			ast.AST{
				Converters: []ast.IntrinsicDecl{{
					Kind: ast.Converter,
					Name: "F",
					Parameters: ast.Parameters{
						{Name: "a", Type: ast.TemplatedName{Name: "T"}},
					},
				}},
			},
		}, { ///////////////////////////////////////////////////////////////////
			fileutils.ThisLine(),
			"conv F(a, b)",
			ast.AST{
				Converters: []ast.IntrinsicDecl{{
					Kind: ast.Converter,
					Name: "F",
					Parameters: ast.Parameters{
						{Type: ast.TemplatedName{Name: "a"}},
						{Type: ast.TemplatedName{Name: "b"}},
					},
				}},
			},
		}, { ///////////////////////////////////////////////////////////////////
			fileutils.ThisLine(),
			"conv F<A : B<C> >()",
			ast.AST{
				Converters: []ast.IntrinsicDecl{{
					Kind: ast.Converter,
					Name: "F",
					ExplicitTemplateParams: []ast.TemplateParam{
						{
							Name: "A", Type: ast.TemplatedName{
								Name: "B",
								TemplateArgs: ast.TemplatedNames{
									{Name: "C"},
								},
							},
						},
					},
					Parameters: ast.Parameters{},
				}},
			},
		}, { ///////////////////////////////////////////////////////////////////
			fileutils.ThisLine(),
			"conv F<T>(a: X, b: Y<T>)",
			ast.AST{
				Converters: []ast.IntrinsicDecl{{
					Kind: ast.Converter,
					Name: "F",
					ExplicitTemplateParams: []ast.TemplateParam{
						{Name: "T"},
					},
					Parameters: ast.Parameters{
						{Name: "a", Type: ast.TemplatedName{Name: "X"}},
						{Name: "b", Type: ast.TemplatedName{
							Name:         "Y",
							TemplateArgs: ast.TemplatedNames{{Name: "T"}},
						}},
					},
				}},
			},
		}, { ///////////////////////////////////////////////////////////////////
			fileutils.ThisLine(),
			"conv F() -> X",
			ast.AST{
				Converters: []ast.IntrinsicDecl{{
					Kind:       ast.Converter,
					Name:       "F",
					ReturnType: &ast.TemplatedName{Name: "X"},
					Parameters: ast.Parameters{},
				}},
			},
		}, { ///////////////////////////////////////////////////////////////////
			fileutils.ThisLine(),
			"conv F() -> X<T>",
			ast.AST{
				Converters: []ast.IntrinsicDecl{{
					Kind: ast.Converter,
					Name: "F",
					ReturnType: &ast.TemplatedName{
						Name:         "X",
						TemplateArgs: ast.TemplatedNames{{Name: "T"}},
					},
					Parameters: ast.Parameters{},
				}},
			}},
	} {
		ast, err := parser.Parse(test.src, test.location, oswrapper.CreateFSTestOSWrapper())
		if err != nil {
			t.Errorf("%v: Parse(%v) error: %v", test.location, test.src, err)
			continue
		}

		if diff := cmp.Diff(test.expect, *ast, ignoreSource); diff != "" {
			t.Errorf("%v: Parse(%v) diff:\n%v", test.location, test.src, diff)
		}
	}
}

func TestParserError(t *testing.T) {
	type test struct {
		src    string
		expect string
	}

	for _, test := range []test{
		{
			"£",
			"test.txt:1:1: unexpected '£'",
		},
		{
			"123",
			"test.txt:1:1 unexpected token 'integer'",
		},
		{
			"@123",
			"test.txt:1:2 expected 'ident' for attribute name, got 'integer'",
		},
	} {
		got, err := parser.Parse(test.src, "test.txt", oswrapper.CreateFSTestOSWrapper())
		gotErr := ""
		if err != nil {
			gotErr = err.Error()
		}
		if test.expect != gotErr {
			t.Errorf(`Parse() returned error "%+v", expected error "%+v"`, gotErr, test.expect)
		}
		if got != nil {
			t.Errorf("Lex() returned non-nil for error")
		}
	}
}

func TestParserWithImports(t *testing.T) {
	type test struct {
		location string
		src      string
		files    map[string]string
		expect   ast.AST
	}

	for _, test := range []test{
		{
			fileutils.ThisLine(),
			`import "foo"`,
			map[string]string{"foo": "type T"},
			ast.AST{
				Types: []ast.TypeDecl{{Name: "T"}},
			},
		},
		{
			fileutils.ThisLine(),
			`import "foo"`,
			map[string]string{
				"foo": `import "bar"`,
				"bar": "type T",
			},
			ast.AST{
				Types: []ast.TypeDecl{{Name: "T"}},
			},
		},
	} {
		fs := oswrapper.CreateFSTestOSWrapper()
		for path, content := range test.files {
			if err := fs.WriteFile(path, []byte(content), 0666); err != nil {
				t.Errorf("%v: WriteFile(%v) error: %v", test.location, path, err)
				continue
			}
		}

		ast, err := parser.Parse(test.src, "root.txt", fs)
		if err != nil {
			t.Errorf("%v: Parse(%v) error: %v", test.location, test.src, err)
			continue
		}

		if diff := cmp.Diff(test.expect, *ast, ignoreSource); diff != "" {
			t.Errorf("%v: Parse(%v) diff:\n%v", test.location, test.src, diff)
		}
	}
}

func TestParserImportErrors(t *testing.T) {
	type test struct {
		src    string
		files  map[string]string
		expect string
	}

	for _, test := range []test{
		{
			`import "missing"`,
			nil,
			"root.txt:1:1 failed to load 'missing': open missing: file does not exist",
		},
		{
			`import "broken"`,
			map[string]string{"broken": "£"},
			"broken:1:1: unexpected '£'",
		},
	} {
		fs := oswrapper.CreateFSTestOSWrapper()
		for path, content := range test.files {
			if err := fs.WriteFile(path, []byte(content), 0666); err != nil {
				t.Errorf("WriteFile(%v) error: %v", path, err)
				continue
			}
		}

		_, err := parser.Parse(test.src, "root.txt", fs)
		if err == nil {
			t.Errorf("Parse(%v) expected error, got nil", test.src)
			continue
		}

		gotErr := err.Error()
		if gotErr != test.expect {
			t.Errorf("Parse(%v) error mismatch:\nGot:  %v\nWant: %v", test.src, gotErr, test.expect)
		}
	}
}
