"""
Parse string to SyntaxNode tree and show as an sexpression
"""
function parse_to_sexpr_str(production, code::AbstractString; v=v"1.6")
    stream = ParseStream(code, version=v)
    production(ParseState(stream))
    JuliaSyntax.validate_tokens(stream)
    t = build_tree(GreenNode, stream)
    source = SourceFile(code)
    s = SyntaxNode(source, t, keep_parens=true)
    return sprint(show, MIME("text/x.sexpression"), s)
end

function test_parse(production, input, output)
    if !(input isa AbstractString)
        opts, input = input
    else
        opts = NamedTuple()
    end
    parsed = parse_to_sexpr_str(production, input; opts...)
    if output isa Regex # Could be AbstractPattern, but that type was added in Julia 1.6.
        @test match(output, parsed) !== nothing
    else
        @test parsed == output
    end
end

function test_parse(inout::Pair)
    test_parse(JuliaSyntax.parse_toplevel, inout...)
end

const PARSE_ERROR = r"\(error-t "

with_version(v::VersionNumber, (i,o)::Pair) = ((;v=v), i) => o

# TODO:
# * Extract the following test cases from the source itself.
# * Use only the green tree to generate the S-expressions
#   (add flag annotations to heads)
tests = [
    JuliaSyntax.parse_toplevel => [
        "a \n b"     =>  "(toplevel a b)"
        "a;b \n c;d" =>  "(toplevel (toplevel-; a b) (toplevel-; c d))"
        "a \n \n"    =>  "(toplevel a)"
        ""           =>  "(toplevel)"
    ],
    JuliaSyntax.parse_block => [
        "a;b;c"   => "(block a b c)"
        "a;;;b;;" => "(block a b)"
        ";a"      => "(block a)"
        "\n a"    => "(block a)"
        "a\nb"    => "(block a b)"
    ],
    JuliaSyntax.parse_stmts => [
        "a;b;c"   => "(toplevel-; a b c)"
        "a;;;b;;" => "(toplevel-; a b)"
        """ "x" a ; "y" b """ =>
            """(toplevel-; (doc (string "x") a) (doc (string "y") b))"""
        "x y"  =>  "(wrapper x (error-t y))"
    ],
    JuliaSyntax.parse_eq => [
        # parse_assignment
        "a = b"       =>  "(= a b)"
        "a .= b"      =>  "(.= a b)"
        "a += b"      =>  "(+= a b)"
        "a .+= b"     =>  "(.+= a b)"
        "a, b = c, d" =>  "(= (tuple a b) (tuple c d))"
        "x, = xs"     =>  "(= (tuple x) xs)"
        "[a ~b]"      =>  "(hcat a (call-pre ~ b))"
        "a ~ b"       =>  "(call-i a ~ b)"
        "a .~ b"      =>  "(dotcall-i a ~ b)"
        "[a ~ b c]"   =>  "(hcat (call-i a ~ b) c)"
        "[a~b]"       =>  "(vect (call-i a ~ b))"
    ],
    JuliaSyntax.parse_pair => [
        "a => b"  =>  "(call-i a => b)"
        "a .=> b" => "(dotcall-i a => b)"
    ],
    JuliaSyntax.parse_cond => [
        "a ? b : c"   => "(? a b c)"
        "a ?\nb : c"  => "(? a b c)"
        "a ? b :\nc"  => "(? a b c)"
        "a ? b : c:d" =>   "(? a b (call-i c : d))"
        # Following are errors but should recover
        "a? b : c"    => "(? a (error-t) b c)"
        "a ?b : c"    => "(? a (error-t) b c)"
        "a ? b: c"    => "(? a b (error-t) c)"
        "a ? b :c"    => "(? a b (error-t) c)"
        "a ? b c"     => "(? a b (error-t) c)"
        "A[x ? y : end]" => "(ref A (? x y end))"
    ],
    JuliaSyntax.parse_arrow => [
        "x → y"     =>  "(call-i x → y)"
        "x <--> y"  =>  "(call-i x <--> y)"
        "x --> y"   =>  "(--> x y)"
        "x .--> y"  =>  "(dotcall-i x --> y)"
        "x -->₁ y"  =>  "(call-i x -->₁ y)"
    ],
    JuliaSyntax.parse_or => [
        "x || y || z" => "(|| x (|| y z))"
        ((v=v"1.6",), "x .|| y") => "(error (.|| x y))"
        ((v=v"1.7",), "x .|| y") => "(.|| x y)"
    ],
    JuliaSyntax.parse_and => [
        "x && y && z" => "(&& x (&& y z))"
        ((v=v"1.6",), "x .&& y") => "(error (.&& x y))"
        ((v=v"1.7",), "x .&& y") => "(.&& x y)"
    ],
    JuliaSyntax.parse_comparison => [
        # Type comparisons are syntactic
        "x <: y"      => "(<: x y)"
        "x >: y"      => "(>: x y)"
        # Normal binary comparisons
        "x < y"       => "(call-i x < y)"
        "x .< y"      => "(dotcall-i x < y)"
        "x .<: y"     => "(dotcall-i x <: y)"
        # Comparison chains
        "x < y < z"   => "(comparison x < y < z)"
        "x == y < z"  => "(comparison x == y < z)"
        "x .< y .< z" => "(comparison x (. <) y (. <) z)"
        "x .< y < z"  => "(comparison x (. <) y < z)"
    ],
    JuliaSyntax.parse_pipe_lt => [
        "x <| y <| z" => "(call-i x <| (call-i y <| z))"
    ],
    JuliaSyntax.parse_pipe_gt => [
        "x |> y |> z" => "(call-i (call-i x |> y) |> z)"
        "x .|> y"     => "(dotcall-i x |> y)"
    ],
    JuliaSyntax.parse_range => [
        "1:2"       => "(call-i 1 : 2)"
        "1:2:3"     => "(call-i 1 : 2 3)"
        "a:b:c:d:e" => "(call-i (call-i a : b c) : d e)"
        "a :< b"    => "(call-i a (error : <) b)"
        "1:\n2"     => "(call-i 1 : (error))"
    ],
    JuliaSyntax.parse_range => [
        "a..b"       => "(call-i a .. b)"
        "a … b"      => "(call-i a … b)"
        "a .… b"     => "(dotcall-i a … b)"
        "[1 :a]"     => "(hcat 1 (quote-: a))"
        "[1 2:3 :a]" =>  "(hcat 1 (call-i 2 : 3) (quote-: a))"
        "x..."     => "(... x)"
        "x:y..."   => "(... (call-i x : y))"
        "x..y..."  => "(... (call-i x .. y))"
    ],
    JuliaSyntax.parse_invalid_ops => [
        "a--b"  =>  "(call-i a (ErrorInvalidOperator) b)"
    ],
    JuliaSyntax.parse_expr => [
        "a - b - c"  => "(call-i (call-i a - b) - c)"
        "a + b + c"  => "(call-i a + b c)"
        "a + b .+ c" => "(dotcall-i (call-i a + b) + c)"
        # parse_with_chains:
        # The following is two elements of a hcat
        "[x +y]"     =>  "(hcat x (call-pre + y))"
        "[x+y +z]"   =>  "(hcat (call-i x + y) (call-pre + z))"
        # Conversely the following are infix calls
        "[x +₁y]"    =>  "(vect (call-i x +₁ y))"
        "[x+y+z]"    =>  "(vect (call-i x + y z))"
        "[x+y + z]"  =>  "(vect (call-i x + y z))"
        # Dotted and normal operators
        "a +₁ b +₁ c" =>  "(call-i (call-i a +₁ b) +₁ c)"
        "a .+ b .+ c" =>  "(dotcall-i (dotcall-i a + b) + c)"
    ],
    JuliaSyntax.parse_term => [
        "a * b * c"  => "(call-i a * b c)"
        "a .* b"     => "(dotcall-i a * b)"
        "-2*x"       => "(call-i -2 * x)"
    ],
    JuliaSyntax.parse_rational => [
        "x // y // z" => "(call-i (call-i x // y) // z)"
    ],
    JuliaSyntax.parse_shift => [
        "x >> y >> z" => "(call-i (call-i x >> y) >> z)"
    ],
    JuliaSyntax.parse_juxtapose => [
        "2x"         => "(juxtapose 2 x)"
        "2x"         => "(juxtapose 2 x)"
        "2(x)"       => "(juxtapose 2 (parens x))"
        "(2)(3)x"    => "(juxtapose (parens 2) (parens 3) x)"
        "(x-1)y"     => "(juxtapose (parens (call-i x - 1)) y)"
        "x'y"        => "(juxtapose (call-post x ') y)"
        "1√x"        =>  "(juxtapose 1 (call-pre √ x))"
        # errors
        "\"a\"\"b\"" => "(juxtapose (string \"a\") (error-t) (string \"b\"))"
        "\"a\"x"     => "(juxtapose (string \"a\") (error-t) x)"
        "\"\$y\"x"   => "(juxtapose (string y) (error-t) x)"
        "\"a\"begin end" => "(juxtapose (string \"a\") (error-t) (block))"
        # Not juxtaposition - parse_juxtapose will consume only the first token.
        "x.3"       =>  "x"
        "f(2)2"     =>  "(call f 2)"
        "x' y"      =>  "(call-post x ')"
        "x 'y"      =>  "x"
        "x@y"       =>  "x"
        "(begin end)x" => "(parens (block))"
    ],
    JuliaSyntax.parse_unary => [
        ":T"       => "(quote-: T)"
        "in::T"    => "(::-i in T)"
        "isa::T"   => "(::-i isa T)"
        "-2^x"     => "(call-pre - (call-i 2 ^ x))"
        "-2[1, 3]" => "(call-pre - (ref 2 1 3))"
        # signed literals
        "-2"       => "-2"
        "+2.0"     => "2.0"
        "-1.0f0"   => "-1.0f0"
        "-0xf.0p0" => "-15.0"
        "+0b10010" => "0x12"
        "+0o22"    => "0x12"
        "+0x12"    => "0x12"
        "-0b10010" => "(call-pre - 0x12)"
        "-0o22"    => "(call-pre - 0x12)"
        "-0x12"    => "(call-pre - 0x12)"
        # Standalone dotted operators are parsed as (|.| op)
        ".+"   =>  "(. +)"
        ".+\n" =>  "(. +)"
        ".+ =" =>  "(. +)"
        ".+)"  =>  "(. +)"
        ".&"   =>  "(. &)"
        # Standalone non-dotted operators
        "+)"   =>  "+"
        # Call with type parameters or non-unary prefix call
        "+{T}(x::T)"  =>  "(call (curly + T) (::-i x T))"
        "*(x)"        =>  "(call * x)"
        ".*(x)"       =>  "(call (. *) x)"
        # Prefix function calls for operators which are both binary and unary
        "+(a,b)"   =>  "(call + a b)"
        ".+(a,)"   =>  "(call (. +) a)"
        "(.+)(a)"  =>  "(call (parens (. +)) a)"
        "+(a=1,)"  =>  "(call + (= a 1))"
        "+(a...)"  =>  "(call + (... a))"
        "+(a;b,c)" =>  "(call + a (parameters b c))"
        "+(;a)"    =>  "(call + (parameters a))"
        "+(;;a)"   =>  "(call + (parameters) (parameters a))"
        "+()"      =>  "(call +)"
        "+(\n;a)"  =>  "(call + (parameters a))"
        "+(;)"     =>  "(call + (parameters))"
        "+(\n;\n)" =>  "(call + (parameters))"
        "+(\n)"    =>  "(call +)"
        # Whitespace not allowed before prefix function call bracket
        "+ (a,b)"  =>  "(call + (error) a b)"
        # Prefix calls have higher precedence than ^
        "+(a,b)^2"  =>  "(call-i (call + a b) ^ 2)"
        "+(a,b)(x)^2"  =>  "(call-i (call (call + a b) x) ^ 2)"
        "<:(a,)"  =>  "(<: a)"
        # Unary function calls with brackets as grouping, not an arglist
        ".+(a)"   =>  "(dotcall-pre + (parens a))"
        "+(a;b)"  =>  "(call-pre + (block-p a b))"
        "+(;;)"   =>  "(call-pre + (block-p))"
        "+(;;)"   =>  "(call-pre + (block-p))"
        "+(a;)"   =>  "(call-pre + (block-p a))"
        "+(a;;)"  =>  "(call-pre + (block-p a))"
        "+(\n;\n;\n)" =>  "(call-pre + (block-p))"
        "+(a=1)"  =>  "(call-pre + (parens (= a 1)))"
        # Unary operators have lower precedence than ^
        "+(a)^2"  =>  "(call-pre + (call-i (parens a) ^ 2))"
        ".+(a)^2" =>  "(dotcall-pre + (call-i (parens a) ^ 2))"
        "+(a)(x,y)^2"  =>  "(call-pre + (call-i (call (parens a) x y) ^ 2))"
        "<:(a)"   =>  "(<:-pre (parens a))"
        # Normal unary calls
        "+x" => "(call-pre + x)"
        "√x" => "(call-pre √ x)"
        ".~x" => "(dotcall-pre ~ x)"
        # Things which are not quite negative literals
        "-0x1"=> "(call-pre - 0x01)"
        "- 2" => "(call-pre - 2)"
        ".-2" => "(dotcall-pre - 2)"
        # Not a unary operator
        "/x"     => "(call-pre (error /) x)"
        "+₁ x"   => "(call-pre (error +₁) x)"
        ".<: x"  => "(dotcall-pre (error (. <:)) x)"
        "?\"str\"" => """(call-pre (error ?) (string "str"))"""
    ],
    JuliaSyntax.parse_factor => [
        "x^y"      =>  "(call-i x ^ y)"
        "x^y^z"    =>  "(call-i x ^ (call-i y ^ z))"
        "x .^ y"   =>  "(dotcall-i x ^ y)"
        "begin x end::T"  =>  "(::-i (block x) T)"
        # parse_decl_with_initial_ex
        "a::b"     =>  "(::-i a b)"
        "a->b"     =>  "(-> a b)"
        "a::b::c"  =>  "(::-i (::-i a b) c)"
        "a::b->c"  =>  "(-> (::-i a b) c)"
    ],
    JuliaSyntax.parse_unary_subtype => [
        "<: )"    =>  "<:"
        "<: \n"   =>  "<:"
        "<: ="    =>  "<:"
        "<:{T}(x::T)"   =>  "(call (curly <: T) (::-i x T))"
        "<:(x::T)"      =>  "(<:-pre (parens (::-i x T)))"
        "<: x"          =>  "(<:-pre x)"
        "<: <: x"       =>  "(<:-pre (<:-pre x))"
        "<: A where B"  =>  "(<:-pre (where A B))"
        # FIXME: The following bizarre precedence seems broken, but is
        # compatible with the reference parser (see #248)
        "+ <: A where B"  =>  "(where (call-pre + (<:-pre A)) B)"
        # Really for parse_where
        "x where \n {T}"  =>  "(where x (braces T))"
        "x where {T,S}"  =>  "(where x (braces T S))"
        "x where {T S}"  =>  "(where x (bracescat (row T S)))"
        "x where {y for y in ys}"  =>  "(where x (braces (generator y (= y ys))))"
        "x where T"  =>  "(where x T)"
        "x where \n T"  =>  "(where x T)"
        "x where T<:S"  =>  "(where x (<: T S))"
        # nested unary and unary-syntactic ops
        "<: + <: + A" => "(<:-pre (call-pre + (<:-pre (call-pre + A))))"
        "* <: A"      => "(call-pre (error *) (<:-pre A))"
    ],
    JuliaSyntax.parse_unary_prefix => [
        "&)"   => "&"
        "\$\n" => "\$"
        "&a"   => "(& a)"
        "::a"  => "(::-pre a)"
        "\$a"  => "(\$ a)"
        "\$\$a"  => "(\$ (\$ a))"
    ],
    JuliaSyntax.parse_call => [
        # parse_call
        "f(x)"    =>  "(call f x)"
        "\$f(x)"  =>  "(call (\$ f) x)"
        ".&(x,y)" =>  "(call (. &) x y)"
        # parse_call_chain
        "f(a).g(b)" => "(call (. (call f a) (quote g)) b)"
        "\$A.@x"    =>  "(macrocall (. (\$ A) (quote @x)))"

        # non-errors in space sensitive contexts
        "[f (x)]"    =>  "(hcat f (parens x))"
        "[f x]"      =>  "(hcat f x)"
        # space separated macro calls
        "@foo a b"     =>  "(macrocall @foo a b)"
        "@foo (x)"     =>  "(macrocall @foo (parens x))"
        "@foo (x,y)"   =>  "(macrocall @foo (tuple-p x y))"
        "A.@foo a b"   =>  "(macrocall (. A (quote @foo)) a b)"
        "@A.foo a b"   =>  "(macrocall (. A (quote @foo)) a b)"
        "[@foo x]"     =>  "(vect (macrocall @foo x))"
        "[@foo]"       =>  "(vect (macrocall @foo))"
        "@var\"#\" a"  =>  "(macrocall (var @#) a)"
        "@(A) x"       =>  "(macrocall (parens @A) x)"
        "A.@x y"       =>  "(macrocall (. A (quote @x)) y)"
        "A.@var\"#\" a"=>  "(macrocall (. A (quote (var @#))) a)"
        "@+x y"        =>  "(macrocall @+ x y)"
        "A.@.x"        =>  "(macrocall (. A (quote @.)) x)"
        # Macro names
        "@! x"  => "(macrocall @! x)"
        "@.. x" => "(macrocall @.. x)"
        "@\$ y"  => "(macrocall @\$ y)"
        "@[x] y z" => "(macrocall (error (vect x)) y z)"
        # Special @doc parsing rules
        "@doc x\ny"    =>  "(macrocall @doc x y)"
        "A.@doc x\ny"  =>  "(macrocall (. A (quote @doc)) x y)"
        "@A.doc x\ny"  =>  "(macrocall (. A (quote @doc)) x y)"
        "@doc x y\nz"  =>  "(macrocall @doc x y)"
        "@doc x\n\ny"  =>  "(macrocall @doc x)"
        "@doc x\nend"  =>  "(macrocall @doc x)"

        # calls with brackets
        "f(a,b)"  => "(call f a b)"
        "f(a=1; b=2)" => "(call f (= a 1) (parameters (= b 2)))"
        "f(a; b; c)" => "(call f a (parameters b) (parameters c))"
        "(a=1)()" =>  "(call (parens (= a 1)))"
        "f (a)" => "(call f (error-t) a)"
        "@x(a, b)"   =>  "(macrocall-p @x a b)"
        "A.@x(y)"    =>  "(macrocall-p (. A (quote @x)) y)"
        "A.@x(y).z"  =>  "(. (macrocall-p (. A (quote @x)) y) (quote z))"
        # do
        "f() do\nend"         =>  "(do (call f) (tuple) (block))"
        "f() do ; body end"   =>  "(do (call f) (tuple) (block body))"
        "f() do x, y\n body end"  =>  "(do (call f) (tuple x y) (block body))"
        "f(x) do y body end"  =>  "(do (call f x) (tuple y) (block body))"

        # square brackets
        "@S[a,b]"  => "(macrocall @S (vect a b))"
        "@S[a b]"  => "(macrocall @S (hcat a b))"
        "@S[a; b]" => "(macrocall @S (vcat a b))"
        "A.@S[a]"  =>  "(macrocall (. A (quote @S)) (vect a))"
        "@S[a].b"  =>  "(. (macrocall @S (vect a)) (quote b))"
        ((v=v"1.7",), "@S[a ;; b]")  =>  "(macrocall @S (ncat-2 a b))"
        ((v=v"1.6",), "@S[a ;; b]")  =>  "(macrocall @S (error (ncat-2 a b)))"
        "a[i]"  =>  "(ref a i)"
        "a [i]"  =>  "(ref a (error-t) i)"
        "a[i,j]"  =>  "(ref a i j)"
        "(a=1)[]" =>  "(ref (parens (= a 1)))"
        "a[end]"  =>  "(ref a end)"
        "a[begin]"  =>  "(ref a begin)"
        "a[:(end)]" => "(typed_hcat a (quote-: (parens (error-t))) (error-t))"
        "T[x   y]"  =>  "(typed_hcat T x y)"
        "T[x ; y]"  =>  "(typed_vcat T x y)"
        "T[a b; c d]"  =>  "(typed_vcat T (row a b) (row c d))"
        "T[x for x in xs]"  =>  "(typed_comprehension T (generator x (= x xs)))"
        ((v=v"1.8",), "T[a ; b ;; c ; d]") => "(typed_ncat-2 T (nrow-1 a b) (nrow-1 c d))"

        # Dotted forms
        # Allow `@` in macrocall only in first and last position
        "A.B.@x"    =>  "(macrocall (. (. A (quote B)) (quote @x)))"
        "@A.B.x"    =>  "(macrocall (. (. A (quote B)) (quote @x)))"
        "A.@B.x"    =>  "(macrocall (. (. A (quote B)) (error-t) (quote @x)))"
        "@M.(x)"    =>  "(macrocall (dotcall @M (error-t) x))"
        "f.(a,b)"   =>  "(dotcall f a b)"
        "f.(a=1; b=2)" => "(dotcall f (= a 1) (parameters (= b 2)))"
        "(a=1).()" =>  "(dotcall (parens (= a 1)))"
        "f. (x)"    =>  "(dotcall f (error-t) x)"
        # Other dotted syntax
        "A.:+"      =>  "(. A (quote-: +))"
        "A.:.+"     =>  "(. A (quote-: (. +)))"
        "A.: +"     =>  "(. A (quote-: (error-t) +))"
        "f.\$x"     =>  "(. f (inert (\$ x)))"
        "f.\$(x+y)" =>  "(. f (inert (\$ (parens (call-i x + y)))))"
        "A.\$B.@x"  =>  "(macrocall (. (. A (inert (\$ B))) (quote @x)))"
        "@A.\$x a"  =>  "(macrocall (. A (inert (error x))) a)"
        "A.@x"      =>  "(macrocall (. A (quote @x)))"
        "A.@x a"    =>  "(macrocall (. A (quote @x)) a)"
        "@A.B.@x a" =>  "(macrocall (. (. A (quote B)) (quote (error-t) @x)) a)"
        # .' discontinued
        "f.'"    =>  "(wrapper f (error-t '))"
        # Field/property syntax
        "f.x.y"  =>  "(. (. f (quote x)) (quote y))"
        "x .y"   =>  "(. x (error-t) (quote y))"
        # Adjoint
        "f'"  => "(call-post f ')"
        "f'ᵀ" => "(call-post f 'ᵀ)"
        # Curly calls
        "S {a}"   => "(curly S (error-t) a)"
        "A.@S{a}" => "(macrocall (. A (quote @S)) (braces a))"
        "@S{a,b}" => "(macrocall @S (braces a b))"
        "A.@S{a}" => "(macrocall (. A (quote @S)) (braces a))"
        "@S{a}.b" => "(. (macrocall @S (braces a)) (quote b))"
        "S{a,b}"  => "(curly S a b)"
        # String macros
        "x\"str\""   => """(macrocall @x_str (string-r "str"))"""
        "x`str`"     => """(macrocall @x_cmd (cmdstring-r "str"))"""
        "x\"\""      => """(macrocall @x_str (string-r ""))"""
        "x``"        => """(macrocall @x_cmd (cmdstring-r ""))"""
        "in\"str\""  => """(macrocall @in_str (string-r "str"))"""
        "outer\"str\"" => """(macrocall @outer_str (string-r "str"))"""
        # Triple quoted procesing for custom strings
        "r\"\"\"\nx\"\"\""        => raw"""(macrocall @r_str (string-s-r "x"))"""
        "r\"\"\"\n x\n y\"\"\""   => raw"""(macrocall @r_str (string-s-r "x\n" "y"))"""
        "r\"\"\"\n x\\\n y\"\"\"" => raw"""(macrocall @r_str (string-s-r "x\\\n" "y"))"""
        # Macro sufficies can include keywords and numbers
        "x\"s\"y"    => """(macrocall @x_str (string-r "s") "y")"""
        "x\"s\"end"  => """(macrocall @x_str (string-r "s") "end")"""
        "x\"s\"in"   => """(macrocall @x_str (string-r "s") "in")"""
        "x\"s\"2"    => """(macrocall @x_str (string-r "s") 2)"""
        "x\"s\"10.0" => """(macrocall @x_str (string-r "s") 10.0)"""
        #
    ],
    JuliaSyntax.parse_resword => [
        # In normal_context
        "begin f() where T = x end" => "(block (= (where (call f) T) x))"
        # block
        "begin end"         =>  "(block)"
        "begin a ; b end"   =>  "(block a b)"
        "begin\na\nb\nend"  =>  "(block a b)"
        # quote
        "quote end"         =>  "(quote (block))"
        "quote body end"    =>  "(quote (block body))"
        # while
        "while cond body end"  =>  "(while cond (block body))"
        "while x < y \n a \n b \n end"  =>  "(while (call-i x < y) (block a b))"
        # for
        "for x in xs end" => "(for (= x xs) (block))"
        "for x in xs, y in ys \n a \n end" => "(for (cartesian_iterator (= x xs) (= y ys)) (block a))"
        # let
        "let x=1\n end"    =>  "(let (block (= x 1)) (block))"
        "let x=1 ; end"    =>  "(let (block (= x 1)) (block))"
        "let x ; end"      =>  "(let (block x) (block))"
        "let x::1 ; end"   =>  "(let (block (::-i x 1)) (block))"
        "let x=1,y=2 end"  =>  "(let (block (= x 1) (= y 2)) (block))"
        "let x+=1 ; end"   =>  "(let (block (+= x 1)) (block))"
        "let ; end"        =>  "(let (block) (block))"
        "let ; body end"   =>  "(let (block) (block body))"
        "let\na\nb\nend"   =>  "(let (block) (block a b))"
        # abstract type
        "abstract type A end"            =>  "(abstract A)"
        "abstract type A ; end"          =>  "(abstract A)"
        "abstract type \n\n A \n\n end"  =>  "(abstract A)"
        "abstract type A <: B end"       =>  "(abstract (<: A B))"
        "abstract type A <: B{T,S} end"  =>  "(abstract (<: A (curly B T S)))"
        "abstract type A < B end"        =>  "(abstract (call-i A < B))"
        # primitive type
        "primitive type A 32 end"   =>  "(primitive A 32)"
        "primitive type A 32 ; end" =>  "(primitive A 32)"
        "primitive type A \$N end"  =>  "(primitive A (\$ N))"
        "primitive type A <: B \n 8 \n end"  =>  "(primitive (<: A B) 8)"
        # struct
        "struct A <: B \n a::X \n end" =>  "(struct (<: A B) (block (::-i a X)))"
        "struct A \n a \n b \n end"    =>  "(struct A (block a b))"
        "mutable struct A end"         =>  "(struct-mut A (block))"
        ((v=v"1.8",), "struct A const a end") => "(struct A (block (const a)))"
        ((v=v"1.7",), "struct A const a end") => "(struct A (block (error (const a))))"
        "struct A end"    =>  "(struct A (block))"
        "struct try end"  =>  "(struct (error try) (block))"
        # return
        "return\nx"   =>  "(return)"
        "return)"     =>  "(return)"
        "return x"    =>  "(return x)"
        "return x,y"  =>  "(return (tuple x y))"
        # break/continue
        "break"    => "(break)"
        "continue" => "(continue)"
        # module/baremodule
        "module A end"      =>  "(module A (block))"
        "baremodule A end"  =>  "(module-bare A (block))"
        "module do \n end"  =>  "(module (error do) (block))"
        "module \$A end"    =>  "(module (\$ A) (block))"
        "module A \n a \n b \n end"  =>  "(module A (block a b))"
        """module A \n "x"\na\n end""" => """(module A (block (doc (string "x") a)))"""
        # export
        "export a"   =>  "(export a)"
        "export @a"  =>  "(export @a)"
        "export @var\"'\"" =>  "(export (var @'))"
        "export a, \n @b"  =>  "(export a @b)"
        "export +, =="     =>  "(export + ==)"
        "export \n a"      =>  "(export a)"
        "export \$a, \$(a*b)"  =>  "(export (\$ a) (\$ (parens (call-i a * b))))"
        "export (x::T)"  =>  "(export (error (parens (::-i x T))))"
        "export outer"  =>  "(export outer)"
        "export (\$f)"  =>  "(export (parens (\$ f)))"
    ],
    JuliaSyntax.parse_if_elseif => [
        "if a xx elseif b yy else zz end" => "(if a (block xx) (elseif b (block yy) (block zz)))"
        "if end"        =>  "(if (error) (block))"
        "if \n end"     =>  "(if (error) (block))"
        "if a end"      =>  "(if a (block))"
        "if a xx end"   =>  "(if a (block xx))"
        "if a \n\n xx \n\n end"   =>  "(if a (block xx))"
        "if a xx elseif b yy end"   =>  "(if a (block xx) (elseif b (block yy)))"
        "if a xx else if b yy end"  =>  "(if a (block xx) (error-t) (elseif b (block yy)))"
        "if a xx else yy end"   =>  "(if a (block xx) (block yy))"
        "if true; x ? true elseif true end"  => "(if true (block (if x true (error-t) (error-t))) (elseif true (block)))"
        "if true; x ? true end"  => "(if true (block (if x true (error-t) (error-t))))"
        "if true; x ? true\nend"  => "(if true (block (if x true (error-t) (error-t))))"
        "if true; x ? true : elseif true end"  => "(if true (block (if x true (error-t))) (elseif true (block)))"
    ],
    JuliaSyntax.parse_resword => [
        "global x"    =>  "(global x)"
        "local x"     =>  "(local x)"
        "global x,y"  =>  "(global x y)"
        "global const x = 1" => "(global (const (= x 1)))"
        "local const x = 1"  => "(local (const (= x 1)))"
        "const global x = 1" => "(const (global (= x 1)))"
        "const local x = 1"  => "(const (local (= x 1)))"
        "const x,y = 1,2"    => "(const (= (tuple x y) (tuple 1 2)))"
        "const x = 1"    =>  "(const (= x 1))"
        "const x .= 1"   => "(error (const (.= x 1)))"
        "global x ~ 1"   =>  "(global (call-i x ~ 1))"
        "global x += 1"  => "(global (+= x 1))"
        "const x"        => "(error (const x))"
        "global const x" => "(global (error (const x)))"
        "const global x" => "(error (const (global x)))"
    ],
    JuliaSyntax.parse_resword => [
        # Macros and functions
        "macro while(ex) end"  =>  "(macro (call (error while) ex) (block))"
        "macro f()     end"    =>  "(macro (call f) (block))"
        "macro (:)(ex) end"    =>  "(macro (call (parens :) ex) (block))"
        "macro (type)(ex) end" =>  "(macro (call (parens type) ex) (block))"
        "macro \$f()    end"   =>  "(macro (call (\$ f)) (block))"
        "macro (\$f)()  end"   =>  "(macro (call (parens (\$ f))) (block))"
        "function (x) body end"=>  "(function (tuple-p x) (block body))"
        "function (x,y) end"   =>  "(function (tuple-p x y) (block))"
        "function (x=1) end"   =>  "(function (tuple-p (= x 1)) (block))"
        "function (;x=1) end"  =>  "(function (tuple-p (parameters (= x 1))) (block))"
        "function ()(x) end"   =>  "(function (call (tuple-p) x) (block))"
        "function (A).f() end" =>  "(function (call (. (parens A) (quote f))) (block))"
        "function (:)() end"   =>  "(function (call (parens :)) (block))"
        "function (x::T)() end"=>  "(function (call (parens (::-i x T))) (block))"
        "function (::g(x))() end" => "(function (call (parens (::-pre (call g x)))) (block))"
        "function (f::T{g(i)})() end" => "(function (call (parens (::-i f (curly T (call g i))))) (block))"
        "function (::T)() end" =>  "(function (call (parens (::-pre T))) (block))"
        "function (:*=(f))() end" => "(function (call (parens (call (quote-: *=) f))) (block))"
        "function begin() end" =>  "(function (call (error begin)) (block))"
        "function f() end"     =>  "(function (call f) (block))"
        "function type() end"  =>  "(function (call type) (block))"
        "function \n f() end"  =>  "(function (call f) (block))"
        "function \$f() end"   =>  "(function (call (\$ f)) (block))"
        "function (::Type{T})(x) end"  =>  "(function (call (parens (::-pre (curly Type T))) x) (block))"
        # Function/macro definition with no methods
        "function f end"      =>  "(function f)"
        "function f \n\n end" =>  "(function f)"
        "function \$f end"    =>  "(function (\$ f))"
        "macro f end"         =>  "(macro f)"
        # Function argument list
        "function f(x,y) end"    =>  "(function (call f x y) (block))"
        "function f{T}() end"    =>  "(function (call (curly f T)) (block))"
        "function A.f()   end"   =>  "(function (call (. A (quote f))) (block))"
        "function f body end"    =>  "(function (error f) (block body))"
        "function f()::T    end" =>  "(function (::-i (call f) T) (block))"
        "function f()::g(T) end" =>  "(function (::-i (call f) (call g T)) (block))"
        "function f() where {T} end"  => "(function (where (call f) (braces T)) (block))"
        "function f() where T   end"  => "(function (where (call f) T) (block))"
        "function f()::S where T end" => "(function (where (::-i (call f) S) T) (block))"
        # Ugly cases for compat where extra parentheses existed and we've
        # already parsed at least the call part of the signature
        "function (f() where T) end" => "(function (parens (where (call f) T)) (block))"
        "function (f()) where T end" => "(function (where (parens (call f)) T) (block))"
        "function (f() where T) where U end" => "(function (where (parens (where (call f) T)) U) (block))"
        "function (f()::S) end"=>  "(function (parens (::-i (call f) S)) (block))"
        "function ((f()::S) where T) end" => "(function (parens (where (parens (::-i (call f) S)) T)) (block))"
        "function (x*y ) end" => "(function (parens (call-i x * y)) (block))"
        # body
        "function f() \n a \n b end"  =>  "(function (call f) (block a b))"
        "function f() end"       =>  "(function (call f) (block))"
        # Errors
        "function"            => "(function (error (error)) (block (error)) (error-t))"
    ],
    JuliaSyntax.parse_try => [
        "try \n x \n catch e \n y \n finally \n z end" =>
            "(try (block x) (catch e (block y)) (finally (block z)))"
        ((v=v"1.8",), "try \n x \n catch e \n y \n else z finally \n w end") =>
            "(try (block x) (catch e (block y)) (else (block z)) (finally (block w)))"
        "try x catch end"       =>  "(try (block x) (catch false (block)))"
        "try x catch ; y end"   =>  "(try (block x) (catch false (block y)))"
        "try x catch \n y end"  =>  "(try (block x) (catch false (block y)))"
        "try x catch e y end"   =>  "(try (block x) (catch e (block y)))"
        "try x catch \$e y end" =>  "(try (block x) (catch (\$ e) (block y)))"
        "try x catch var\"#\" y end" => "(try (block x) (catch (var #) (block y)))"
        "try x catch e+3 y end" =>  "(try (block x) (catch (error (call-i e + 3)) (block y)))"
        "try x finally y end"   =>  "(try (block x) (finally (block y)))"
        # v1.8 only
        ((v=v"1.8",), "try catch ; else end") => "(try (block) (catch false (block)) (else (block)))"
        ((v=v"1.8",), "try else x finally y end") => "(try (block) (else (error (block x))) (finally (block y)))"
        ((v=v"1.7",), "try catch ; else end")  =>  "(try (block) (catch false (block)) (else (error (block))))"
        # finally before catch :-(
        "try x finally y catch e z end"  =>  "(try (block x) (finally (block y)) (catch e (block z)))"
        "try x end" => "(try (block x) (error-t))"
    ],
    JuliaSyntax.parse_imports => [
        "import A as B: x" => "(import (: (error (as (importpath A) B)) (importpath x)))"
        "import A, y"      => "(import (importpath A) (importpath y))"
        "import A: +, =="  => "(import (: (importpath A) (importpath +) (importpath ==)))"
        "import A: x, y"   => "(import (: (importpath A) (importpath x) (importpath y)))"
        "import A: x, B: y" => "(import (: (importpath A) (importpath x) (importpath B) (error-t (importpath y))))"
        "import A: x"      => "(import (: (importpath A) (importpath x)))"
        "using  A"         => "(using (importpath A))"
        "import A"         => "(import (importpath A))"
        # parse_import
        "import A: x, y"   =>  "(import (: (importpath A) (importpath x) (importpath y)))"
        "import A as B"    =>  "(import (as (importpath A) B))"
        "import A: x as y" =>  "(import (: (importpath A) (as (importpath x) y)))"
        "using  A: x as y" =>  "(using (: (importpath A) (as (importpath x) y)))"
        ((v=v"1.5",), "import A as B") =>  "(import (error (as (importpath A) B)))"
        "using A as B"     =>  "(using (error (as (importpath A) B)))"
        "using A, B as C"  =>  "(using (importpath A) (error (as (importpath B) C)))"
        # parse_import_path
        # When parsing import we must split initial dots into nontrivial
        # leading dots for relative paths
        "import .A"     =>  "(import (importpath . A))"
        "import ..A"    =>  "(import (importpath . . A))"
        "import ...A"   =>  "(import (importpath . . . A))"
        "import ....A"  =>  "(import (importpath . . . . A))"
        # Dots with spaces are allowed (a misfeature?)
        "import . .A"   =>  "(import (importpath . . A))"
        # Modules with operator symbol names
        "import .⋆"     =>  "(import (importpath . ⋆))"
        # Expressions allowed in import paths
        "import @x"     =>  "(import (importpath @x))"
        "import \$A"    =>  "(import (importpath (\$ A)))"
        "import \$A.@x" =>  "(import (importpath (\$ A) @x))"
        "import A.B"    =>  "(import (importpath A B))"
        "import A.B.C"  =>  "(import (importpath A B C))"
        "import A.:+"   =>  "(import (importpath A (quote-: +)))"
        "import A.(:+)" =>  "(import (importpath A (parens (quote-: +))))"
        "import A.:(+)" =>  "(import (importpath A (quote-: (parens +))))"
        "import A.=="   =>  "(import (importpath A ==))"
        "import A.⋆.f"  =>  "(import (importpath A ⋆ f))"
        "import A..."   =>  "(import (importpath A ..))"
        "import A; B"   =>  "(import (importpath A))"
    ],
    JuliaSyntax.parse_iteration_spec => [
        "i = rhs"        =>  "(= i rhs)"
        "i in rhs"       =>  "(= i rhs)"
        "i ∈ rhs"        =>  "(= i rhs)"
        "i = 1:10"       =>  "(= i (call-i 1 : 10))"
        "(i,j) in iter"  =>  "(= (tuple-p i j) iter)"
        "outer = rhs"       =>  "(= outer rhs)"
        "outer <| x = rhs"  =>  "(= (call-i outer <| x) rhs)"
        "outer i = rhs"     =>  "(= (outer i) rhs)"
        "outer (x,y) = rhs" =>  "(= (outer (tuple-p x y)) rhs)"
    ],
    JuliaSyntax.parse_paren => [
        # Tuple syntax with commas
        "()"          =>  "(tuple-p)"
        "(x,)"        =>  "(tuple-p x)"
        "(x,y)"       =>  "(tuple-p x y)"
        "(x=1, y=2)"  =>  "(tuple-p (= x 1) (= y 2))"
        # Named tuples with initial semicolon
        "(;)"         =>  "(tuple-p (parameters))"
        "(; a=1)"     =>  "(tuple-p (parameters (= a 1)))"
        # Extra credit: nested parameters and frankentuples
        "(x...; y)"       => "(tuple-p (... x) (parameters y))"
        "(x...;)"         => "(tuple-p (... x) (parameters))"
        "(; a=1; b=2)"    => "(tuple-p (parameters (= a 1)) (parameters (= b 2)))"
        "(a; b; c,d)"     => "(tuple-p a (parameters b) (parameters c d))"
        "(a=1, b=2; c=3)" => "(tuple-p (= a 1) (= b 2) (parameters (= c 3)))"
        # Block syntax
        "(;;)"        =>  "(block-p)"
        "(a=1;)"      =>  "(block-p (= a 1))"
        "(a;b;;c)"    =>  "(block-p a b c)"
        "(a=1; b=2)"  =>  "(block-p (= a 1) (= b 2))"
        # Parentheses used for grouping
        "(a * b)"     =>  "(parens (call-i a * b))"
        "(a=1)"       =>  "(parens (= a 1))"
        "(x)"         =>  "(parens x)"
        "(a...)"      =>  "(parens (... a))"
        # Generators
        "(x for a in as)"       =>  "(parens (generator x (= a as)))"
        "(x \n\n for a in as)"  =>  "(parens (generator x (= a as)))"
        # Range parsing in parens
        "(1:\n2)" => "(parens (call-i 1 : 2))"
        "(1:2)" => "(parens (call-i 1 : 2))"
    ],
    JuliaSyntax.parse_atom => [
        # char literal
        "'a'"           =>  "(char 'a')"
        "'α'"           =>  "(char 'α')"
        "'\\xce\\xb1'"  =>  "(char 'α')"
        "'\\u03b1'"     =>  "(char 'α')"
        "'\\U1D7DA'"    =>  "(char '𝟚')"
        "'a"            =>  "(char 'a' (error-t))"
        "''"            =>  "(char (error))"
        "'"             =>  "(char (error))"
        # symbol/expression quote
        ":foo"   => "(quote-: foo)"
        # Literal colons
        ":)"     => ":"
        ": end"  => ":"
        # Whitespace after quoting colon
        ": foo"  => "(quote-: (error-t) foo)"
        ":\nfoo" => "(quote-: (error-t) foo)"
        # plain equals
        "="      => "(error =)"
        # Identifiers
        "xx"     => "xx"
        "x₁"     => "x₁"
        # var syntax
        """var"x" """   =>  "(var x)"
        # var syntax raw string unescaping
        "var\"\""          =>  "(var )"
        "var\"\\\"\""      =>  "(var \")"
        "var\"\\\\\\\"\""  =>  "(var \\\")"
        "var\"\\\\x\""     =>  "(var \\\\x)"
        # trailing syntax after var
        """var"x"+"""   =>  "(var x)"
        """var"x")"""   =>  "(var x)"
        """var"x"("""   =>  "(var x)"
        """var"x"end""" =>  "(var x (error-t))"
        """var"x"1"""   =>  "(var x (error-t))"
        """var"x"y"""   =>  "(var x (error-t))"
        # Standalone syntactic operators are errors
        "+="  =>  "(error +=)"
        "?"   =>  "(error ?)"
        ".+=" =>  "(error (. +=))"
        # Normal operators
        "+"  =>  "+"
        "~"  =>  "~"
        # Quoted syntactic operators allowed
        ":+="  =>  "(quote-: +=)"
        ":.="  =>  "(quote-: (. =))"
        ":.&&" =>  "(quote-: (. &&))"
        # Special symbols quoted
        ":end" => "(quote-: end)"
        ":(end)" => "(quote-: (parens (error-t)))"
        ":<:"  => "(quote-: <:)"
        # unexpect =
        "="    => "(error =)"
        # parse_cat
        "[]"        =>  "(vect)"
        "[x,]"      =>  "(vect x)"
        "[x\n,,]"   =>  "(vect x (error-t ✘))"
        "[x]"       =>  "(vect x)"
        "[x \n ]"   =>  "(vect x)"
        "[x \n, ]"  =>  "(vect x)"
        "[x"        =>  "(vect x (error-t))"
        "[x \n\n ]" =>  "(vect x)"
        "[x for a in as]"  =>  "(comprehension (generator x (= a as)))"
        "[x \n\n for a in as]"  =>  "(comprehension (generator x (= a as)))"
        # parse_generator
        "(x for a in as for b in bs)" => "(parens (generator x (= a as) (= b bs)))"
        "(x for a in as, b in bs)" => "(parens (generator x (cartesian_iterator (= a as) (= b bs))))"
        "(x for a in as, b in bs if z)" => "(parens (generator x (filter (cartesian_iterator (= a as) (= b bs)) z)))"
        "(x for a in as, b in bs for c in cs, d in ds)" => "(parens (generator x (cartesian_iterator (= a as) (= b bs)) (cartesian_iterator (= c cs) (= d ds))))"
        "(x for a in as for b in bs if z)" => "(parens (generator x (= a as) (filter (= b bs) z)))"
        "(x for a in as if z for b in bs)" => "(parens (generator x (filter (= a as) z) (= b bs)))"
        "[x for a = as for b = bs if cond1 for c = cs if cond2]"  =>  "(comprehension (generator x (= a as) (filter (= b bs) cond1) (filter (= c cs) cond2)))"
        "[x for a = as if begin cond2 end]"  =>  "(comprehension (generator x (filter (= a as) (block cond2))))"
        "[(x)for x in xs]"  =>  "(comprehension (generator (parens x) (error-t) (= x xs)))"
        "(x for a in as if z)" => "(parens (generator x (filter (= a as) z)))"
        # parse_vect
        "[x, y]"        =>  "(vect x y)"
        "[x, y]"        =>  "(vect x y)"
        "[x,\n y]"      =>  "(vect x y)"
        "[x\n, y]"      =>  "(vect x y)"
        "[x\n,, y]"     =>  "(vect x (error-t ✘ y))"
        "[x,y ; z]"     =>  "(vect x y (parameters z))"
        "[x=1, y=2]"    =>  "(vect (= x 1) (= y 2))"
        "[x=1, ; y=2]"  =>  "(vect (= x 1) (parameters (= y 2)))"
        # parse_paren
        ":(=)"  =>  "(quote-: (parens =))"
        ":(::)"  =>  "(quote-: (parens ::))"
        ":(::\n)" => "(quote-: (parens ::))"
        "(function f \n end)" => "(parens (function f))"
        # braces
        "{x y}"      =>  "(bracescat (row x y))"
        ((v=v"1.7",), "{x ;;; y}") =>  "(bracescat (nrow-3 x y))"
        # Macro names can be keywords
        "@end x" => "(macrocall @end x)"
        # __dot__ macro
        "@. x" => "(macrocall @. x)"
        # cmd strings
        "``"         =>  "(macrocall core_@cmd (cmdstring-r \"\"))"
        "`cmd`"      =>  "(macrocall core_@cmd (cmdstring-r \"cmd\"))"
        "```cmd```"  =>  "(macrocall core_@cmd (cmdstring-s-r \"cmd\"))"
        # literals
        "true" => "true"
        "42"   => "42"
        "1.0e-1000"   => "0.0"
        "0x123456789abcdefp+0" => "8.19855292164869e16"
        # closing tokens
        ")"    => "(error)"
    ],
    JuliaSyntax.parse_atom => [
        # Actually parse_array
        # Normal matrix construction syntax
        "[x y ; z w]"  =>  "(vcat (row x y) (row z w))"
        "[x y ; z w ; a b]"  =>  "(vcat (row x y) (row z w) (row a b))"
        "[x ; y ; z]"  =>  "(vcat x y z)"
        "[x;]"  =>  "(vcat x)"
        "[x y]"  =>  "(hcat x y)"
        # Early abort in array parsing
        "[x@y"   =>  "(hcat x (error-t ✘ y))"
        "[x@y]"  =>  "(hcat x (error-t ✘ y))"
        # Mismatched rows
        "[x y ; z]"  =>  "(vcat (row x y) z)"
        # Single elements in rows
        ((v=v"1.7",), "[x ; y ;; z ]")  =>  "(ncat-2 (nrow-1 x y) z)"
        ((v=v"1.7",), "[x  y ;;; z ]")  =>  "(ncat-3 (row x y) z)"
        # Higher dimensional ncat
        # Row major
        ((v=v"1.7",), "[x y ; z w ;;; a b ; c d]")  =>
            "(ncat-3 (nrow-1 (row x y) (row z w)) (nrow-1 (row a b) (row c d)))"
        # Column major
        ((v=v"1.7",), "[x ; y ;; z ; w ;;; a ; b ;; c ; d]")  =>
            "(ncat-3 (nrow-2 (nrow-1 x y) (nrow-1 z w)) (nrow-2 (nrow-1 a b) (nrow-1 c d)))"
        # Array separators
        # Newlines before semicolons are not significant
        "[a \n ;]"  =>  "(vcat a)"
        # Newlines after semicolons are not significant
        "[a ; \n]"  =>  "(vcat a)"
        "[a ; \n\n b]"  =>  "(vcat a b)"
        ((v=v"1.7",), "[a ;; \n b]")  =>  "(ncat-2 a b)"
        # In hcat with spaces as separators, `;;` is a line
        # continuation character
        ((v=v"1.7",), "[a b ;; \n c]")  =>  "(hcat a b c)"
        ((v=v"1.7",), "[a b \n ;; c]")  =>  "(ncat-2 (row a b (error-t)) c)"
        # Can't mix spaces and multiple ;'s
        ((v=v"1.7",), "[a b ;; c]")  =>  "(ncat-2 (row a b (error-t)) c)"
        # Linebreaks not significant before closing `]`
        "[a b\n\n]" =>  "(hcat a b)"
        # Treat a linebreak prior to a value as a semicolon (ie, separator for
        # the first dimension) if no previous semicolons observed
        "[a \n b]"  =>  "(vcat a b)"
        # Can't mix multiple ;'s and spaces
        ((v=v"1.7",), "[a ;; b c]")  =>  "(ncat-2 a (row b (error-t) c))"
        # Empty nd arrays
        ((v=v"1.8",), "[;]")   =>  "(ncat-1)"
        ((v=v"1.8",), "[;;]")  =>  "(ncat-2)"
        ((v=v"1.8",), "[\n  ;; \n ]")  =>  "(ncat-2)"
        ((v=v"1.7",), "[;;]")  =>  "(ncat-2 (error))"
        # parse_string
        "\"\"\"\n\$x\n a\"\"\"" => "(string-s x \"\\n\" \" a\")"
        "\"a \$(x + y) b\""  =>  "(string \"a \" (parens (call-i x + y)) \" b\")"
        "\"hi\$(\"ho\")\""   =>  "(string \"hi\" (parens (string \"ho\")))"
        "\"\$(x,y)\""        =>  "(string (parens (error x y)))"
        "\"\$(x;y)\""        =>  "(string (parens (error x y)))"
        "\"\$(x for y in z)\"" => "(string (parens (error (generator x (= y z)))))"
        "\"\$((x for y in z))\"" => "(string (parens (parens (generator x (= y z)))))"
        "\"\$(xs...)\""  =>  "(string (parens (... xs)))"
        "\"a \$foo b\""  =>  "(string \"a \" foo \" b\")"
        "\"\$var\""      =>  "(string var)"
        "\"\$outer\""    =>  "(string outer)"
        "\"\$in\""       =>  "(string in)"
        # Triple-quoted dedenting:
        "\"\"\"\nx\"\"\""   =>  raw"""(string-s "x")"""
        "\"\"\"\n\nx\"\"\"" =>  raw"""(string-s "\n" "x")"""
        "```\n x\n y```"    =>  raw"""(macrocall core_@cmd (cmdstring-s-r "x\n" "y"))"""
        # Various newlines (\n \r \r\n) and whitespace (' ' \t)
        "\"\"\"\n x\n y\"\"\""  =>  raw"""(string-s "x\n" "y")"""
        "\"\"\"\r x\r y\"\"\""  =>  raw"""(string-s "x\n" "y")"""
        "\"\"\"\r\n x\r\n y\"\"\""  =>  raw"""(string-s "x\n" "y")"""
        # Spaces or tabs or mixtures acceptable
        "\"\"\"\n\tx\n\ty\"\"\""  =>  raw"""(string-s "x\n" "y")"""
        "\"\"\"\n \tx\n \ty\"\"\""  =>  raw"""(string-s "x\n" "y")"""
        # Mismatched tab vs space not deindented
        # Find minimum common prefix in mismatched whitespace
        "\"\"\"\n\tx\n y\"\"\""  =>  raw"""(string-s "\tx\n" " y")"""
        "\"\"\"\n x\n  y\"\"\""  =>  raw"""(string-s "x\n" " y")"""
        "\"\"\"\n  x\n y\"\"\""  =>  raw"""(string-s " x\n" "y")"""
        "\"\"\"\n \tx\n  y\"\"\""  =>  raw"""(string-s "\tx\n" " y")"""
        "\"\"\"\n  x\n \ty\"\"\""  =>  raw"""(string-s " x\n" "\ty")"""
        # Empty lines don't affect dedenting
        "\"\"\"\n x\n\n y\"\"\""  =>  raw"""(string-s "x\n" "\n" "y")"""
        # Non-empty first line doesn't participate in deindentation
        "\"\"\" x\n y\"\"\""  =>  raw"""(string-s " x\n" "y")"""
        # Dedenting and interpolations
        "\"\"\"\n  \$a\n  \$b\"\"\""  =>  raw"""(string-s a "\n" b)"""
        "\"\"\"\n  \$a \n  \$b\"\"\""  =>  raw"""(string-s a " \n" b)"""
        "\"\"\"\n  \$a\n  \$b\n\"\"\""  =>  raw"""(string-s "  " a "\n" "  " b "\n")"""
        # Empty chunks after dedent are removed
        "\"\"\"\n \n \"\"\""  =>  "(string-s \"\\n\")"
        # Newline at end of string
        "\"\"\"\n x\n y\n\"\"\""  =>  raw"""(string-s " x\n" " y\n")"""
        # Empty strings, or empty after triple quoted processing
        "\"\""              =>  "(string \"\")"
        "\"\"\"\n  \"\"\""  =>  "(string-s \"\")"
        # Missing delimiter
        "\"str"  =>  "(string \"str\" (error-t))"
        # String interpolations
        "\"\$x\$y\$z\""  =>  "(string x y z)"
        "\"\$(x)\""  =>  "(string (parens x))"
        "\"\$x\""  =>  "(string x)"
        # Strings with embedded whitespace trivia
        "\"a\\\nb\""     =>  raw"""(string "a" "b")"""
        "\"a\\\rb\""     =>  raw"""(string "a" "b")"""
        "\"a\\\r\nb\""   =>  raw"""(string "a" "b")"""
        "\"a\\\n \tb\""  =>  raw"""(string "a" "b")"""
        # Strings with only a single valid string chunk
        "\"str\""     => "(string \"str\")"
        "\"a\\\n\""   => "(string \"a\")"
        "\"a\\\r\""   => "(string \"a\")"
        "\"a\\\r\n\"" => "(string \"a\")"
    ],
    JuliaSyntax.parse_atom => [
        # errors in literals
        "\"\\xqqq\""  =>  "(string (ErrorInvalidEscapeSequence))"
        "'\\xq'"      =>  "(char (ErrorInvalidEscapeSequence))"
        "'ab'"        =>  "(char (ErrorOverLongCharacter))"
        "\"\xf5\""    =>  "(string (ErrorInvalidUTF8))"
        "'\xf5'"      =>  "(char (ErrorInvalidUTF8))"
        "`\xf5`"      =>  "(macrocall core_@cmd (cmdstring-r (ErrorInvalidUTF8)))"
        "10.0e1000'"  =>  "(ErrorNumericOverflow)"
        "10.0f100'"   =>  "(ErrorNumericOverflow)"
    ],
    JuliaSyntax.parse_stmts => with_version.(v"1.11", [
        "function f(public)\n    public + 3\nend"       => "(function (call f public) (block (call-i public + 3)))"
        "public A, B"                                   => "(public A B)"
        "if true \n public *= 4 \n end"                 => "(if true (block (*= public 4)))"
        "module Mod\n public A, B \n end"               => "(module Mod (block (public A B)))"
        "module Mod2\n a = 3; b = 6; public a, b\n end" => "(module Mod2 (block (= a 3) (= b 6) (public a b)))"
        "a = 3; b = 6; public a, b"                     => "(toplevel-; (= a 3) (= b 6) (public a b))"
        "begin \n public A, B \n end"                   => PARSE_ERROR
        "if true \n public A, B \n end"                 => PARSE_ERROR
        "public export=true foo, bar"                   => PARSE_ERROR # but these may be
        "public experimental=true foo, bar"             => PARSE_ERROR # supported soon ;)
        "public(x::String) = false"                     => "(= (call public (::-i x String)) false)"
        "module M; export @a; end"                      => "(module M (block (export @a)))"
        "module M; public @a; end"                      => "(module M (block (public @a)))"
        "module M; export ⤈; end"                       => "(module M (block (export ⤈)))"
        "module M; public ⤈; end"                       => "(module M (block (public ⤈)))"
        "public = 4"                                    => "(= public 4)"
        "public[7] = 5"                                 => "(= (ref public 7) 5)"
        "public() = 6"                                  => "(= (call public) 6)"
    ]),
    JuliaSyntax.parse_docstring => [
        """ "notdoc" ]        """ => "(string \"notdoc\")"
        """ "notdoc" \n]      """ => "(string \"notdoc\")"
        """ "notdoc" \n\n foo """ => "(string \"notdoc\")"
        """ "doc" \n foo      """ => """(doc (string "doc") foo)"""
        """ "doc" foo         """ => """(doc (string "doc") foo)"""
        """ "doc \$x" foo     """ => """(doc (string "doc " x) foo)"""
        # Allow docstrings with embedded trailing whitespace trivia
        "\"\"\"\n doc\n \"\"\" foo"  => """(doc (string-s "doc\\n") foo)"""
    ],
]

@testset "Inline test cases" begin
    @testset "$production" for (production, test_specs) in tests
        @testset "$(repr(input))" for (input, output) in test_specs
            test_parse(production, input, output)
        end
    end
end

parsestmt_test_specs = [
    # whitespace before keywords in space-insensitive mode
    "(y::\nif x z end)" => "(parens (::-i y (if x (block z))))"
    # Contextual keyword pairs inside parentheses
    "(abstract type X end)" => "(parens (abstract X))"
    "(mutable struct X end)" => "(parens (struct-mut X (block)))"
    # parsing of tricky primes
    "x in'c'"   => "(call-i x in (char 'c'))"
    "1where'c'" => "(where 1 (char 'c'))"
    ":+'y'"     => "(juxtapose (call-post (quote-: +) ') (call-post y '))"
    # Empty character consumes trailing ' delimiter (ideally this could be
    # tested above but we don't require the input stream to be consumed in the
    # unit tests there.
    "''" => "(char (error))"

    # The following may not be ideal error recovery! But at least the parser
    # shouldn't crash
    "@(x y)" => "(macrocall (parens @x (error-t y)))"
    "|(&\nfunction" => "(call | (& (function (error (error)) (block (error)) (error-t))) (error-t))"
    "@(" => "(macrocall (parens (error-t)))"
    "x = @(" => "(= x (macrocall (parens (error-t))))"
    "function(where" => "(function (tuple-p where (error-t)) (block (error)) (error-t))"
    # Contextual keyword pairs must not be separated by newlines even within parens
    "(abstract\ntype X end)" => "(wrapper (parens abstract (error-t type X)) (error-t end ✘))"
    "(mutable\nstruct X end)" => "(wrapper (parens mutable (error-t struct X)) (error-t end ✘))"

    # The following is currently broken but at least the parser shouldn't
    # crash.
    "x in' '" => "(call-i x in (char (error)))"
]

@testset "Parser does not crash on broken code" begin
    @testset "$(repr(input))" for (input, output) in parsestmt_test_specs
        test_parse(JuliaSyntax.parse_stmts, input, output)
    end
end

@testset "Trivia attachment" begin
    # TODO: Need to expand this greatly to cover as many forms as possible!
    @test show_green_tree("f(a;b)") == """
         1:6      │[toplevel]
         1:6      │  [call]
         1:1      │    Identifier           ✔   "f"
         2:2      │    (                        "("
         3:3      │    Identifier           ✔   "a"
         4:5      │    [parameters]
         4:4      │      ;                      ";"
         5:5      │      Identifier         ✔   "b"
         6:6      │    )                        ")"
    """
end

@testset "Unicode normalization in tree conversion" begin
    # ɛµ normalizes to εμ
    @test parse_to_sexpr_str(JuliaSyntax.parse_eq, "\u025B\u00B5()") == "(call \u03B5\u03BC)"
    @test parse_to_sexpr_str(JuliaSyntax.parse_eq, "@\u025B\u00B5") == "(macrocall @\u03B5\u03BC)"
    @test parse_to_sexpr_str(JuliaSyntax.parse_eq, "\u025B\u00B5\"\"") == "(macrocall @\u03B5\u03BC_str (string-r \"\"))"
    @test parse_to_sexpr_str(JuliaSyntax.parse_eq, "\u025B\u00B5``") == "(macrocall @\u03B5\u03BC_cmd (cmdstring-r \"\"))"
    # · and · normalize to ⋅
    @test parse_to_sexpr_str(JuliaSyntax.parse_eq, "a \u00B7 b") == "(call-i a \u22C5 b)"
    @test parse_to_sexpr_str(JuliaSyntax.parse_eq, "a \u0387 b") == "(call-i a \u22C5 b)"
    # − normalizes to -
    @test parse_to_sexpr_str(JuliaSyntax.parse_expr, "a \u2212 b")  == "(call-i a - b)"
    @test parse_to_sexpr_str(JuliaSyntax.parse_eq, "a \u2212= b") == "(-= a b)"
    @test parse_to_sexpr_str(JuliaSyntax.parse_eq, "a .\u2212= b") == "(.-= a b)"
end

@testset "Unbalanced bidirectional unicode" begin
    # https://trojansource.codes
    @test_throws JuliaSyntax.ParseError parsestmt(GreenNode, """
    function checkUserAccess(u::User)
        if u.accessLevel != "user\u202e \u2066# users are not allowed\u2069\u2066"
            return true
        end
        return false
    end
    """)

    @test_throws JuliaSyntax.ParseError parsestmt(GreenNode, """
    function checkUserAccess(u::User)
        #=\u202e \u2066if (u.isAdmin)\u2069 \u2066 begin admins only =#
            return true
        #= end admin only \u202e \u2066end\u2069 \u2066=#
        return false
    end
    """)
end
