diff --git a/src/JuliaLowering.jl b/src/JuliaLowering.jl index d8ff05d..ee1308c 100644 --- a/src/JuliaLowering.jl +++ b/src/JuliaLowering.jl @@ -37,6 +37,7 @@ _include("syntax_macros.jl") _include("eval.jl") _include("compat.jl") _include("hooks.jl") +_include("validation.jl") function __init__() _register_kinds() diff --git a/src/desugaring.jl b/src/desugaring.jl index 53aa67b..5aaa915 100644 --- a/src/desugaring.jl +++ b/src/desugaring.jl @@ -4595,6 +4595,7 @@ end @fzone "JL: desugar" function expand_forms_2(ctx::MacroExpansionContext, ex::SyntaxTree) ctx1 = DesugaringContext(ctx, ctx.expr_compat_mode) + valid_st1_or_throw(ex) ex1 = expand_forms_2(ctx1, reparent(ctx1, ex)) ctx1, ex1 end diff --git a/src/syntax_graph.jl b/src/syntax_graph.jl index 767643b..40a9c51 100644 --- a/src/syntax_graph.jl +++ b/src/syntax_graph.jl @@ -817,3 +817,227 @@ end # end # out # end + +# TODO: forgot to support vcat (i.e. newlines in patterns currently require a +# double-semicolon continuation) + +# TODO: SyntaxList pattern matching could take similar syntax and use most of +# the same machinery + +raw""" +Simple SyntaxTree pattern matching + +``` +@stm syntax_tree begin + pattern1 -> result1 + (pattern2, when=cond2) -> result2 + (pattern3, when=cond3, run=run3, when=cond4) -> result3 + ... +end +``` + +Returns the first result where its corresponding pattern matches `syntax_tree` +and each extra `cond` is true. Throws an error if no match is found. + +This macro (especially `when` and `run`) takes heavy inspiration from [Racket's +pattern matching](https://docs.racket-lang.org/reference/match.html), but with a +significantly less featureful pattern language. + +## Patterns + +A pattern is used as both a conditional (does this syntax tree have a certain +structure?) and a `let` (bind trees to these names if so). Each pattern uses a +limited version of the @ast syntax: + +``` + = + | [K"" *] + | [K"" * ... *] + +# note "*" is the meta-operator meaning one or more, and "..." is literal +``` + +where a `[K"k" p1 p2 ps...]` form matches any tree with kind `k` and >=2 +children (bound to `p1` and `p2`), and `ps` is bound to the possibly-empty +SyntaxList of children `3:end`. Identifiers (except `_`) can't be re-used, but +may check for some form of tree equivalence in a future implementation. + +## Extra conditions: `when`, `run` + +Like an escape hatch to the structure-matching mechanism. `when=cond` requires +`cond`'s value be `true` for this pattern to match. `run=code` simply evaluates +`code`, usually to bind variables or debug the matching process. + +`when` and `run` clauses may appear multiple times in any order after the +pattern. They are executed left-to-right, stopping if any `when=cond` evaluates +to false. These may not mutate the object being matched. + +## Scope of variables + +Every `(pattern, extras...) -> result` introduces a local scope. Identifiers in +the pattern are let-bound when evaluating `extras` and `result`. Any `extra` can +introduce variables for use in later `extras` and `result`. User code in +`extras` and `result` can refer to outer variables. + +## Example + +``` +julia> st = JuliaSyntax.parsestmt(JuliaLowering.SyntaxTree, "function foo(x,y,z); x; end") +julia> JuliaLowering.@stm st begin + [K"function" [K"call" fname args... [K"parameters" kws...]] body] -> + "has kwargs: $(kws)" + [K"function" fname] -> + "zero-method function $fname" + [K"function" [K"call" fname args...] body] -> + "normal function $fname" + ([K"=" [K"call" _...] _...], when=(args=if_valid_get_args(st[1]); !isnothing(args))) -> + "deprecated call-equals form with args $args" + (_, run=show("printf debugging is great")) -> "something else" + _ -> "something else" +end +"normal function foo" +``` +""" +macro stm(st, pats) + _stm(__source__, st, pats; debug=false) +end + +"Like `@stm`, but prints a trace during matching." +macro stm_debug(st, pats) + _stm(__source__, st, pats; debug=true) +end + +function _stm(line::LineNumberNode, st, pats; debug=false) + _stm_check_usage(pats) + # We leave most code untouched, so the user probably wants esc(output) + st_gs, result_gs = gensym("st"), gensym("result") + out_blk = Expr(:let, + Expr(:block, :($st_gs = $st::SyntaxTree), :($result_gs = nothing)), + Expr(:if, false, nothing)) + needs_else = out_blk.args[2].args + for per in pats.args + per isa LineNumberNode && (line = per; continue) + p, extras, result = _stm_destruct_pat(Base.remove_linenums!(per)) + # We need to let-bind patvars in both extras and the result, so result + # needs to live in the first argument of :if with the extra conditions. + e_check = Expr(:&&) + for (ek, ev) in extras + push!(e_check.args, ek === :when ? ev : Expr(:block, ev, true)) + end + # final arg to e_check: successful match + push!(e_check.args, Expr(:block, line, :($result_gs = $result), true)) + case = Expr(:elseif, + Expr(:&&, :(JuliaLowering._stm_matches($(Expr(:quote, p)), $st_gs, $debug)), + Expr(:let, _stm_assigns(p, st_gs), e_check)), + result_gs) + push!(needs_else, case) + needs_else = needs_else[3].args + end + push!(needs_else, :(throw("No match found for $($st_gs) at $($(string(line)))"))) + return esc(out_blk) +end + +function _stm_destruct_pat(per) + pe, r = per.args[1:2] + return !Meta.isexpr(pe, :tuple) ? (pe, Tuple[], r) : + let extras = pe.args[2:end] + (pe.args[1], Tuple[(e.args[1], e.args[2]) for e in extras], r) + end +end + +function _stm_matches(p::Union{Symbol, Expr}, st, debug=false, indent="") + if p isa Symbol + debug && printstyled(indent, "$p = $st\n"; color=:yellow) + return true + elseif Meta.isexpr(p, (:vect, :hcat)) + p_kind = Kind(p.args[1].args[3]) + kind_ok = p_kind === kind(st) + if !kind_ok + debug && printstyled(indent, "[kind]: $(kind(st))!=$p_kind\n"; color=:red) + return false + end + p_args = filter(e->!(e isa LineNumberNode), p.args)[2:end] + dots_i = findfirst(x->Meta.isexpr(x, :(...)), p_args) + dots_start = something(dots_i, length(p_args) + 1) + n_after = length(p_args) - dots_start + npats = dots_start + n_after + n_ok = (isnothing(dots_i) ? numchildren(st) === npats : numchildren(st) >= npats - 1) + if !n_ok + debug && printstyled(indent, "[numc]: $(numchildren(st))!=$npats\n"; color=:red) + return false + end + all_ok = all(i->_stm_matches(p_args[i], st[i], debug, indent*" "), 1:dots_start-1) && + all(i->_stm_matches(p_args[end-i], st[end-i], debug, indent*" "), n_after-1:-1:0) + debug && printstyled(indent, st, all_ok ? " matched\n" : " not matched\n"; + color=(all_ok ? :green : :red)) + return all_ok + end + @assert false +end + +# Assuming _stm_matches, construct an Expr that assigns syms to SyntaxTrees. +# Note st_rhs_expr is a ref-expr with a SyntaxTree/List value (in context). +function _stm_assigns(p, st_rhs_expr; assigns=Expr(:block)) + if p isa Symbol && p != :_ + push!(assigns.args, Expr(:(=), p, st_rhs_expr)) + elseif p isa Expr + p_args = filter(e->!(e isa LineNumberNode), p.args)[2:end] + dots_i = findfirst(x->Meta.isexpr(x, :(...)), p_args) + dots_start = something(dots_i, length(p_args) + 1) + n_after = length(p_args) - dots_start + for i in 1:dots_start-1 + _stm_assigns(p_args[i], :($st_rhs_expr[$i]); assigns) + end + if !isnothing(dots_i) + _stm_assigns(p_args[dots_i].args[1], + :($st_rhs_expr[$dots_i:end-$n_after]); assigns) + for i in n_after-1:-1:0 + _stm_assigns(p_args[end-i], :($st_rhs_expr[end-$i]); assigns) + end + end + end + return assigns + @assert false +end + +# Check for correct pattern syntax. Not needed outside of development. +function _stm_check_usage(pats) + function _stm_check_pattern(p; syms=Set{Symbol}()) + if Meta.isexpr(p, :(...), 1) + p = p.args[1] + @assert(p isa Symbol, "Expected symbol before `...` in $p") + end + if p isa Symbol + # No support for duplicate syms for now (user is either looking for + # some form of equality we don't implement, or they made a mistake) + dup = p in syms && p !== :_ + push!(syms, p) + return !dup || @assert(false, "invalid duplicate non-underscore identifier $p") + end + return (Meta.isexpr(p, :vect, 1) || + (Meta.isexpr(p, :hcat) && + isnothing(@assert(count(x->Meta.isexpr(x, :(...)), p.args[2:end]) <= 1, + "Multiple `...` in a pattern is ambiguous")) && + all(x->_stm_check_pattern(x; syms), p.args[2:end])) && + # This exact syntax is not necessary since the kind can't be + # provided by a variable, but requiring [K"kinds"] is consistent. + Meta.isexpr(p.args[1], :macrocall, 3) && + p.args[1].args[1] === Symbol("@K_str") && p.args[1].args[3] isa String) + end + + @assert Meta.isexpr(pats, :block) "Usage: @st_match st begin; ...; end" + for per in filter(e->!isa(e, LineNumberNode), pats.args) + @assert(Meta.isexpr(per, :(->), 2), "Expected pat -> res, got malformed pair: $per") + if Meta.isexpr(per.args[1], :tuple) + @assert length(per.args[1].args) >= 2 "Unnecessary tuple in $(per.args[1])" + for e in per.args[1].args[2:end] + @assert(Meta.isexpr(e, :(=), 2) && e.args[1] in (:when, :run), + "Expected `when=` or `run=`, got $e") + end + p = per.args[1].args[1] + else + p = per.args[1] + end + @assert _stm_check_pattern(p) "Malformed pattern: $p" + end +end diff --git a/src/validation.jl b/src/validation.jl new file mode 100644 index 0000000..bc82b3c --- /dev/null +++ b/src/validation.jl @@ -0,0 +1,972 @@ +struct ValidationDiagnostic <: Exception + sts::SyntaxList + msg::String +end +ValidationDiagnostic(st::SyntaxTree, msg::String) = + ValidationDiagnostic(SyntaxList(syntax_graph(st), NodeId[st._id]), msg) + +""" +This context contains: +- The error vector (shared per invocation of valid_st1) +- Recursive flags that would otherwise require keyword arguments to all + validation functions, usually to remember the kinds of structures we're in. + +By default, assume we are validating a usual lowering input (top-level) that has +been macroexpanded. +""" +Base.@kwdef struct Validation1Context + errors::Vector{ValidationDiagnostic}=ValidationDiagnostic[] + # warnings::Vector{ValidationDiagnostic} + speculative::Bool=false # match not required; disable errors and return a bool + toplevel::Bool=true # not in any lambda body + in_gscope::Bool=true # not in any scope; implies toplevel + in_loop::Bool=false # break/continue allowed + inner_cond::Bool=false # inner methods not allowed. true in ? (args 2-3), &&, and || + return_ok::Bool=true # yes usually (even outside of functions), no in comprehensions/generators + # syntax TODO: no return in finally? type declarations? + # We currently always parse to K"=", but Expr(:kw) is valid here and Expr(:(=)) is not + # :assign_ok => true # no in vect, curly, [typed_]h/v/ncat + # :beginend_ok => false # once this is different from the identifier + + # vst0 shares this context type since macro expansion doesn't recurse + # into some forms, and most parts of the AST are the same. + unexpanded::Bool=false + quote_level::Int=0 +end + +function with(vcx::Validation1Context; + errors =vcx.errors, + speculative =vcx.speculative, + toplevel =vcx.toplevel, + in_gscope =vcx.in_gscope, + in_loop =vcx.in_loop, + inner_cond =vcx.inner_cond, + return_ok =vcx.return_ok, + unexpanded =vcx.unexpanded, + quote_level =vcx.quote_level) + Validation1Context(errors, speculative, toplevel, in_gscope, in_loop, + inner_cond, return_ok, unexpanded, quote_level) +end + +""" +Executable grammar of the input language to lowering (post-macro-expansion). + +This should serve three purposes: +(1) A readable reference for the julia AST structure. +(2) A set of assumptions we can use in lowering (a guard against many forms of + invalid input). If `valid_st1(st)` returns true, lowering is expected to + produce correct output given `st` (possibly by throwing a LoweringError). +(3) The place we throw helpful user-facing errors given malformed ASTs. + +Only AST structure is checked. Roughly, this means "kinds and child counts in +context". A tree `t` has valid structure if, given the kinds and child count of +all its ancestors, and the position within its parent of `t` and all its +ancestors, we know how to lower `t` to IR. + +We don't check some other things: +- This pass assumes that required attributes exist, that leaf-only (or not) + kinds are leaves (or not), and that syntax flags are valid per kind; these can + be checked beforehand in a linear pass over the nodes. +- Scope issues are caught later in lowering, e.g. declaring something local and + global. + +""" +function valid_st1(st::SyntaxTree) + vcx = Validation1Context() + valid = valid_st1(vcx, st) + for (i, e) in enumerate(vcx.errors) + printstyled("error $i:\n"; color=:red) + showerror(stdout, LoweringError(e.sts[1], e.msg)) + for st_ in e.sts[2:end]; show(st_); end + printstyled("---------------------------------\n"; color=:red) + end + return valid +end + +function valid_st1(vcx::Validation1Context, st::SyntaxTree) + pass = vst1_stmt(vcx, st) + !pass && isempty(vcx.errors) && + fail(vcx, st, "invalid syntax: unknown kind `$(kind(st))` or number of arguments ($(numchildren(st)))") + return pass +end + +# temp function to keep LoweringErrors the same as before, producing only one +# error with only one associated tree +function valid_st1_or_throw(st::SyntaxTree) + vcx = Validation1Context() + valid = valid_st1(vcx, st) + if !isempty(vcx.errors) & !valid + e1 = vcx.errors[1] + throw(LoweringError(e1.sts[1], e1.msg)) + elseif isempty(vcx.errors) & valid + return nothing + else + throw(LoweringError(st, "validator bug: returned $valid but with $(length(vcx.errors)) errors")) + end +end + +vst1_value(vcx::Validation1Context, st::SyntaxTree; need_value=true) = @stm st begin + (leaf, when=kind(leaf) in KSet"""Identifier Value Symbol Integer Float + String Char Bool CmdString HexInt OctInt BinInt""") -> true + + # Container nodes that may or may not be values depending on their contents; + # callers of vst1_value can specify that they don't need a value. Most + # other forms are always a value. + [K"block" _...] -> vst1_block(vcx, st; need_value) + [K"let" [K"block" decls...] body] -> allv(vst1_symdecl_or_assign, vcx, decls) & + vst1_block(with(vcx; in_gscope=false), body; need_value) + [K"if" cond t] -> vst1_value(vcx, cond) & + vst1_value(vcx.toplevel ? vcx : with(vcx; inner_cond=true), t; need_value) + [K"if" cond t f] -> vst1_value(vcx, cond) & + vst1_value(vcx.toplevel ? vcx : with(vcx; inner_cond=true), t; need_value) & + vst1_else(vcx.toplevel ? vcx : with(vcx; inner_cond=true), f; need_value) + + # op-assign will perform the op, but fail to assign with a bad lhs, so we + # disallow it here + [K"=" l r] -> vst1_assign_lhs(vcx, l) & vst1_value(vcx, r) + [K"op=" l op r] -> vst1_assign_lhs(vcx, l) & vst1_ident(vcx, op) & vst1_value(vcx, r) + [K".=" l r] -> vst1_dotassign_lhs(vcx, l) & vst1_value(vcx, r) + [K".op=" l op r] -> vst1_dotassign_lhs(vcx, l) & vst1_ident(vcx, op) & vst1_value(vcx, r) + + [K"function" _...] -> !vcx.inner_cond ? vst1_function(vcx, st) : + # fail(vcx, st, "conditional inner method definitions are not supported; use `()->()` syntax instead") + # lowering TODO: conditional nested function definitions are known to be + # broken, but are not disallowed, and can be found in stdlibs. + vst1_function(vcx, st) + [K"call" _...] -> vst1_call(vcx, st) + [K"dotcall" _...] -> vst1_dotcall(vcx, st) + [K"return"] -> vcx.return_ok || fail(vcx, st, "`return` not allowed inside comprehension or generator") + [K"return" val] -> vcx.return_ok ? vst1_value(vcx, val) : + fail(vcx, st, "`return` not allowed inside comprehension or generator") + [K"continue"] -> vcx.in_loop || fail(vcx, st, "`continue` must be used inside a `while` or `for` loop") + [K"break"] -> vcx.in_loop || fail(vcx, st, "`break` must be used inside a `while` or `for` loop") + [K"?" cond t f] -> vst1_value(vcx, cond) & + vst1_value(vcx.toplevel ? vcx : with(vcx; inner_cond=true), t) & + vst1_value(vcx.toplevel ? vcx : with(vcx; inner_cond=true), f) + [K"for" [K"iteration" is...] [K"block" body...]] -> + allv(vst1_iter, vcx, is) & + allv(vst1_stmt, with(vcx; in_loop=true, in_gscope=false), body) + [K"while" cond [K"block" body...]] -> + vst1_value(vcx, cond) & + allv(vst1_stmt, with(vcx; in_loop=true, in_gscope=false), body) + [K"try" _...] -> vst1_try(vcx, st) + [K"macrocall" _...] -> vcx.unexpanded ? vst0_macrocall(vcx, st) : + fail(vcx, st, "macrocall not valid in AST after macro expansion") + [K"quote" x] -> vcx.unexpanded ? vst0_quoted(with(vcx; quote_level=1), x) : + fail(vcx, st, "interpolating quote not valid in AST after macro expansion") + [K"$" x] -> fail(vcx, st, raw"`$` expression outside string or quote") + [K"tuple" _...] -> vst1_value_tuple(vcx, st) + [K"." x y] -> vst1_value(vcx, x) & vst1_dot_rhs(vcx, y) + [K"." x] -> vst1_value(vcx, x) + [K":"] -> true # ast TODO: `a:b` is identifier, lone `:` should be identifier, `a[:]` should be this kind + [K"curly" t xs...] -> vst1_value(vcx, t) & allv(vst1_value_curly_typevar, vcx, xs) + [K"where" lhs rhs] -> vst1_value(vcx, lhs) & vst1_where_rhs(vcx, rhs) + [K"string" xs...] -> allv(vst1_value, vcx, xs) + [K"->" _...] -> vst1_lam(vcx, st) + [K"generator" _...] -> vst1_generator(vcx, st) + [K"comprehension" g] -> vst1_generator(vcx, g) + [K"typed_comprehension" t g] -> vst1_value(with(vcx; return_ok=false), t) & vst1_generator(vcx, g) + [K"comparison" xs...] -> length(xs) < 3 || iseven(length(xs)) ? + fail(vcx, st, "`comparison` expects n>=3 args and odd n") : + # TODO: xs[2:2:end] should just be identifier or (. identifier) + allv(vst1_value, vcx, xs[2:2:end]) & allv(vst1_value, vcx, xs[1:2:end]) + [K"doc" [K"string" strs...] x] -> allv(vst1_value, vcx, strs) & vst1_documented(vcx, x) + [K"<:" x y] -> vst1_value(vcx, x) & vst1_value(vcx, y) + [K">:" x y] -> vst1_value(vcx, x) & vst1_value(vcx, y) + [K"-->" x y] -> vst1_value(vcx, x) & vst1_value(vcx, y) + [K"::" x y] -> vst1_value(vcx, x) & vst1_value(vcx, y) + [K"&&" xs...] -> allv(vst1_value, vcx, xs) + [K"||" xs...] -> allv(vst1_value, vcx, xs) + [K".&&" x y] -> vst1_value(vcx, x) & vst1_value(vcx, y) + [K".||" x y] -> vst1_value(vcx, x) & vst1_value(vcx, y) + (_, when=(kp=pass_or_err(vst1_arraylike, vcx, st); kp.known)) -> kp.pass + # value-producing const, global-= + [K"const" [K"global" x]] -> vst1_const_assign(vcx, x) + [K"global" [K"const" x]] -> vst1_const_assign(vcx, x) + [K"const" x] -> !vcx.in_gscope ? + fail(vcx, st, "unsupported `const` declaration on local variable") : + vst1_const_assign(vcx, x) + [K"global" [K"=" l r]] -> + vst1_assign_lhs(vcx, l; disallow_type=!vcx.toplevel) & + vst1_value(vcx, r) + # syntax TODO: local is always OK as a value, but non-assigning should probably not be + # TODO: fail if immediate toplevel? + [K"local" xs...] -> allv(vst1_symdecl_or_assign, vcx, xs) + + # Forms not produced by the parser + [K"inert" _] -> true + [K"core"] -> true + [K"top"] -> true + [K"meta" _...] -> true # TODO documented forms, once we figure out how we'll deal with this + [K"extension" [K"Symbol"] _...] -> + (st[1].name_val in ("locals", "islocal", "isglobal") || fail(vcx, st, "unknown extension kind")) + [K"toplevel" xs...] -> # contents will be unexpanded + allv(valid_st0, with(vcx; toplevel=true, in_gscope=true), xs) + [K"opaque_closure" argt lb ub [K"Bool"] lam] -> + allv(vst1_value, vcx, [argt, lb, ub]) & vst1_lam(vcx, lam) + [K"gc_preserve" x ids...] -> vst1_value(vcx, x) & allv(vst1_ident, vcx, ids) + [K"gc_preserve_begin" ids...] -> allv(vst1_ident, vcx, ids) + [K"gc_preserve_end" ids...] -> allv(vst1_ident, vcx, ids) + # [K"thisfunction"] -> !vcx.toplevel && vcx.return_ok || + # fail(vcx, st, "`thisfunction` is not defined in generators or outside of functions") + [K"isdefined" [K"Identifier"]] -> true + [K"lambda" [K"block" b1...] [K"block" b2...] [K"->" _...]] -> + allv(vst1_ident, vcx, b1) & + allv(vst1_ident, vcx, b2) & + vst1_lam(vcx, st[3]) + [K"scope_block" x] -> vst1_value(vcx, x; need_value) + [K"generated"] -> true + # TODO: could probably be validated more + [K"foreigncall" f [K"static_eval" rt] [K"static_eval" argt_svec] args...] -> + vst1_foreigncall_fname(vcx, f) & vst1_value(vcx, rt) & + vst1_value(vcx, argt_svec) & allv(vst1_value, vcx, args) + [K"cfunction" t [K"static_eval" fptr] [K"static_eval" rt] [K"static_eval" argt_svec] [K"Symbol"]] -> + vst1_value(vcx, t) & vst1_value(vcx, fptr) & + vst1_value(vcx, rt) & vst1_value(vcx, argt_svec) + [K"cfunction" t fptr [K"static_eval" rt] [K"static_eval" argt_svec] [K"Symbol"]] -> + vst1_value(vcx, t) & vst1_value(vcx, fptr) & + vst1_value(vcx, rt) & vst1_value(vcx, argt_svec) + # syntax TODO: lowering-internal form used to remove scope from try blocks. + # probably shouldn't be allowed. + [K"tryfinally" t f _...] -> vst1_value(vcx, t) & vst1_value(vcx, f) + + # Only from macro expansions producing Expr(:toplevel, ...). We don't want + # to recurse on the contained expression since `K"escape"` can wrap nearly + # any node. This is OK since (1) if we're running `valid_st1` + # pre-desugaring, this form is not recognized by desugaring and wrapped in a + # `toplevel` anyway, so we'll see the expanded version later. (2) If we're + # running `valid_st0`, macros are not expanded, so this form won't appear. + ([K"hygienic_scope" x [K"Value"] [K"Value"]]) -> + vcx.unexpanded || fail(vcx, st, "`hygienic_scope` not valid after macro expansion") + [K"hygienic_scope" x [K"Value"]] -> + vcx.unexpanded || fail(vcx, st, "`hygienic_scope` not valid after macro expansion") + + # forms normalized by expand_forms_1, so not valid in st1. TODO: we should + # consider doing each of these normalizations before macro expansion rather + # than after. + ([K"juxtapose" xs...], when=vcx.unexpanded) -> allv(vst1_value, vcx, xs) + ([K"cmdstring" x], when=vcx.unexpanded) -> vst1_value(vcx, x) + ([K"char" [K"Char"]], when=vcx.unexpanded) -> true + ([K"var" [K"Identifier"]], when=vcx.unexpanded) -> true + + # Invalid forms for which we want to produce detailed errors + [K"..." _...] -> fail(vcx, st, "unexpected splat not in `call`, `tuple`, `curly`, or array expression") + [K"parameters" _...] -> fail(vcx, st, "unexpected keyword-separating semicolon outside of call or tuple") + [K"braces" _...] -> fail(vcx, st, "`braces` outside of `where` is reserved for future use") + [K"bracescat" _...] -> fail(vcx, st, "`bracescat` is reserved for future use") + [K"do" _...] -> fail(vcx, st, "unexpected `do` outside of `call`") + [K"Placeholder"] -> fail(vcx, st, "all-underscore identifiers are write-only and their values cannot be used in expressions") + [K"atomic" _...] -> fail(vcx, st, "unimplemented or unsupported `atomic` declaration") + [K"::" x] -> fail(vcx, st, "`::` must be written `value::type` outside function argument lists") + (_, when=need_value && kind(st) in KSet"symbolic_label symbolic_goto") -> + fail(vcx, st, "misplaced `$(kind(st))` in value position") + ([K"global" _...], when=need_value) -> + fail(vcx, st, "global declaration doesn't read the variable and can't return a value") + + (_, when=(kp=pass_or_err(vst1_toplevel_only_value, vcx, st); kp.known)) -> + kp.pass && (vcx.toplevel || fail(vcx, st, "this syntax is only allowed in top level code")) + _ -> false +end + +vst1_toplevel_only_value(vcx::Validation1Context, st::SyntaxTree) = @stm st begin + [K"module" name [K"block" xs...]] -> ( + vst1_ident(vcx, name) & allv(valid_st0, with(vcx; toplevel=true, in_gscope=true), xs)) + [K"macro" _...] -> vst1_macro(vcx, st) + # The following return nothing, but are allowed as values + [K"struct" sig [K"block" body...]] -> vst1_typesig(vcx, sig) & + allv(vst1_struct_field, vcx, body) + [K"abstract" sig] -> vst1_typesig(vcx, sig) + [K"primitive" sig [K"Integer"]] -> vst1_typesig(vcx, sig) + [K"primitive" sig n] -> vst1_typesig(vcx, sig) & vst1_value(vcx, n) # allowed? + _ -> false +end + +vst1_stmt(vcx::Validation1Context, st::SyntaxTree) = @stm st begin + (_, when=(kp=pass_or_err(vst1_value, vcx, st; need_value=false); kp.known)) -> kp.pass + ([K"global" xs...], when=vcx.toplevel) -> allv(vst1_symdecl_or_assign, vcx, xs) + ([K"global" xs...], when=!vcx.toplevel) -> allv(vst1_inner_global_decl, vcx, xs) + [K"symbolic_label"] -> true + [K"symbolic_goto"] -> true + [K"latestworld_if_toplevel"] -> true + + (_, when=(kp=pass_or_err(vst1_toplevel_only_stmt, vcx, st); kp.known)) -> + kp.pass && (vcx.toplevel || fail(vcx, st, "this syntax is only allowed in top level code")) + _ -> fail(vcx, st, "invalid syntax: unknown kind `$(kind(st))` or number of arguments ($(numchildren(st)))") +end + +vst1_inner_global_decl(vcx, st) = @stm st begin + (_, when=cst1_ident(vcx, st)) -> true + [K"=" l r] -> vst1_assign_lhs(vcx, l; disallow_type=true) & vst1_value(vcx, r) + [K"function" _...] -> vst1_function(vcx, st) && + JuliaSyntax.has_flags(st, JuliaSyntax.SHORT_FORM_FUNCTION_FLAG) + + [K"::" _...] -> fail(vcx, st, "type declarations for global variables must be at top level, not inside a function") + _ -> fail(vcx, st, "invalid global declaration: expected identifier or assignment") +end + +vst1_toplevel_only_stmt(vcx::Validation1Context, st::SyntaxTree) = @stm st begin + # Parsing is stricter (no "as" in no-colon using) + [K"import" [K":" p1 ps...]] -> + (vst1_importpath(vcx, p1; dots_ok=true) & + allv(vst1_importpath, vcx, ps; dots_ok=false)) + [K"using" [K":" p1 ps...]] -> + (vst1_importpath(vcx, p1; dots_ok=true) & + allv(vst1_importpath, vcx, ps; dots_ok=false)) + [K"import" ps...] -> allv(vst1_importpath, vcx, ps; dots_ok=true) + [K"using" ps...] -> allv(vst1_importpath, vcx, ps; dots_ok=true) + [K"public" xs...] -> allv(vst1_ident, vcx, xs) + [K"export" xs...] -> allv(vst1_ident, vcx, xs) + [K"latestworld"] -> true + _ -> false +end + +# @stm doesn't work so well with n dots and m identifiers +# one of: +# (as (importpath . . . x y z) ident) +# (importpath . . . x y z) +# where y, z may be quoted (syntax TODO: require var"" for odd identifiers?) +function vst1_importpath(vcx, st; dots_ok) + ok = true + path_components = @stm st begin + [K"as" [K"importpath" xs...] as2] -> ( + if !vst1_ident(vcx, as2) + fail(vcx, as2, "expected identifier after `as`") + ok = false + end; xs) + [K"importpath" xs...] -> xs + end + seen_first = false + for c in path_components + if kind(c) === K"." + if !dots_ok || seen_first + ok = false + fail(vcx, c, "unexpected `.` in import path") + end + continue + end + ok = ok && vst1_ident(vcx, seen_first && kind(c) === K"quote" ? c[1] : c) + seen_first = true + end + !seen_first && fail(vcx, st, "expected identifier in `importpath`") + return ok && seen_first +end + +vst1_value_tuple(vcx, st) = @stm st begin + [K"tuple" [K"parameters" kws...]] -> allv(vst1_call_arg, vcx, kws) + [K"tuple" _ _... [K"parameters" _ _...]] -> + fail(vcx, st[end], "cannot mix tuple `(a,b,c)` and named tuple `(;a,b,c)` syntax") + [K"tuple" xs...] -> allv(vst1_splat_or_val, vcx, xs) + _ -> fail(vcx, st, "malformed tuple") +end + +vst1_block(vcx, st; need_value=true) = @stm st begin + [K"block"] -> true + ([K"block" xs...], when=!need_value) -> allv(vst1_stmt, vcx, xs) + [K"block" xs... val] -> allv(vst1_stmt, vcx, xs) & vst1_value(vcx, val) + _ -> fail(vcx, st, "expected `block`") +end + +# Usually block-wrapped, but not always. +vst1_else(vcx, st; need_value) = @stm st begin + [K"elseif" cond t] -> vst1_value(vcx, cond) & vst1_value(vcx, t; need_value) + [K"elseif" cond t f] -> vst1_value(vcx, cond) & + vst1_value(vcx, t; need_value) & vst1_else(vcx, f; need_value) + _ -> vst1_value(vcx, st; need_value) +end + +# TODO: catch and else are in value position +# ast TODO: While the AST is more readable than Expr's try-catch, it's difficult +# enough to unpack that we should consider changing it, especially if we intend +# to support multiple catch-blocks at some point +vst1_try(vcx, st) = @stm st begin + [K"try" _] -> fail(vcx, st, "try without catch or finally") + [K"try" b1 [K"catch" err body2...]] -> + vst1_block(vcx, b1) & + vst1_ident(vcx, err; lhs=true) & + allv(vst1_stmt, vcx, body2) + [K"try" b1 [K"else" body3...]] -> + fail(vcx, st, "try without catch or finally") + [K"try" b1 [K"finally" body4...]] -> + vst1_block(vcx, b1) & + allv(vst1_stmt, vcx, body4) + [K"try" b1 [K"catch" err body2...] [K"else" body3...]] -> + vst1_block(vcx, b1) & + vst1_ident(vcx, err; lhs=true) & + allv(vst1_stmt, vcx, body2) & + allv(vst1_stmt, vcx, body3) + [K"try" b1 [K"catch" err body2...] [K"finally" body4...]] -> + vst1_block(vcx, b1) & + vst1_ident(vcx, err; lhs=true) & + allv(vst1_stmt, vcx, body2) & + allv(vst1_stmt, vcx, body4) + [K"try" b1 [K"else" body3...] [K"finally" body4...]] -> + vst1_block(vcx, b1) & + allv(vst1_stmt, vcx, body4) + [K"try" b1 [K"catch" err body2...] [K"else" body3...] [K"finally" body4...]] -> + vst1_block(vcx, b1) & + vst1_ident(vcx, err; lhs=true) & + allv(vst1_stmt, vcx, body2) & + allv(vst1_stmt, vcx, body3) & + allv(vst1_stmt, vcx, body4) + _ -> fail(vcx, st, "malformed `try` expression") +end + +# syntax TODO: +# - const is inoperative in the function case +# - single-arg const with no value (presumably to poison this name) was likely +# not intended to work, and can only be produced by macros +vst1_const_assign(vcx, st) = @stm st begin + (_, when=!vcx.toplevel) -> fail(vcx, st, "`const` declaration not allowed inside function") + [K"=" l r] -> vst1_assign_lhs(vcx, l; in_const=true) & vst1_value(vcx, r) + [K"function" _...] -> vst1_function(vcx, st) && + JuliaSyntax.has_flags(st, JuliaSyntax.SHORT_FORM_FUNCTION_FLAG) + [K"Identifier"] -> true + + [K"local" _...] -> fail(vcx, st, "locals cannot be declarated `const`") + _ -> fail(vcx, st, "expected assignment after `const`") +end + +# We can't validate A.B in general (usually lowers to getproperty), but it shows +# up in a number of syntax special cases where we can. +vst1_dotpath(vcx, st) = @stm st begin + [K"." l r] -> vst1_dotpath(vcx, l) & vst1_dot_rhs(vcx, r) + [K"Value"] -> typeof(st.value) in (Module, GlobalRef) + lhs -> vst1_ident(vcx, lhs) +end + +# syntax TODO: all-underscore variables may be read from with dot syntax +vst1_dot_rhs(vcx, st; lhs=false) = @stm st begin + [K"Symbol"] -> true + [K"Identifier"] -> vcx.unexpanded || + fail(vcx, st, "Identifier not valid on dot rhs after macro expansion") + [K"string" _...] -> true # syntax TODO: disallow + [K"core"] -> fail(vcx, st, "this is a reserved identifier") + _ -> fail(vcx, st, "`a.b` requires symbol `b`") +end + +cst1_assign(vcx, st) = @stm st begin + [K"=" l r] -> vst1_assign_lhs(vcx, l) & vst1_value(vcx, r) + [K"function" _...] -> vst1_function(vcx, st) && + JuliaSyntax.has_flags(st, JuliaSyntax.SHORT_FORM_FUNCTION_FLAG) + _ -> false +end + +vst1_symdecl_or_assign(vcx, st) = cst1_assign(vcx, st) || vst1_symdecl(vcx, st) + +vst1_symdecl(vcx, st) = @stm st begin + (_, when=cst1_ident(vcx, st; lhs=true)) -> true + [K"::" id t] -> vst1_ident(vcx, t; lhs=true) & vst1_value(vcx, t) + _ -> fail(vcx, st, "expected identifier or `::`") +end + +# ast TODO: Omit the `var` container at the parser level. This would let us +# delete these entirely and just match [K"Identifier"] instead. +# TODO: A K"Value" globalref is often OK in place of an identifier; check usage +# of this function +vst1_ident(vcx, st; lhs=false) = + cst1_ident(vcx, st; lhs) || fail(vcx, st, "expected identifier, got `$(kind(st))`") +cst1_ident(vcx, st; lhs=false) = @stm st begin + [K"Identifier"] -> true + [K"var" [K"Identifier"]] -> vcx.unexpanded ? true : + fail(vcx, st, "`var` container not valid after macro expansion") + [K"Placeholder"] -> lhs || + fail(vcx, st, "all-underscore identifiers are write-only and their values cannot be used in expressions") + _ -> false +end + +# no kws in macrocalls, but `do` is OK. +# TODO: we move `do` between st0 and st1... +vst1_call(vcx, st) = @stm st begin + ([K"call" a0 op a1], when=(vcx.unexpanded && is_infix_op_call(st))) -> + vst1_value(vcx, a0) & + vst1_ident(vcx, op) & + vst1_value(vcx, a1) + ([K"call" a0 op], when=(vcx.unexpanded && is_postfix_op_call(st))) -> + vst1_value(vcx, a0) & + vst1_ident(vcx, op) + [K"call" f [K"do" _...] args... [K"parameters" kwargs...]] -> + vst1_value(vcx, f) & + allv(vst1_call_arg, vcx, args) & + allv(vst1_call_kwarg, vcx, kwargs) & + vst1_do(vcx, st[2]) + [K"call" f args... [K"parameters" kwargs...]] -> + vst1_value(vcx, f) & + allv(vst1_call_arg, vcx, args) & + allv(vst1_call_kwarg, vcx, kwargs) + [K"call" f [K"do" _...] args...] -> + vst1_value(vcx, f) & + allv(vst1_call_arg, vcx, args) & + vst1_do(vcx, st[2]) + [K"call" f args...] -> + vst1_value(vcx, f) & + allv(vst1_call_arg, vcx, args) + _ -> fail(vcx, st, "invalid `call` form") +end + +# unfortunate duplicate of the above +vst1_dotcall(vcx, st) = @stm st begin + ([K"dotcall" a0 op a1], when=(vcx.unexpanded && is_infix_op_call(st))) -> + vst1_value(vcx, a0) & + vst1_ident(vcx, op) & + vst1_value(vcx, a1) + ([K"dotcall" a0 op], when=(vcx.unexpanded && is_postfix_op_call(st))) -> + vst1_value(vcx, a0) & + vst1_ident(vcx, op) + [K"dotcall" f [K"do" _...] args... [K"parameters" kwargs...]] -> + vst1_value(vcx, f) & + allv(vst1_call_arg, vcx, args) & + allv(vst1_call_kwarg, vcx, kwargs) & + vst1_do(vcx, st[2]) + [K"dotcall" f args... [K"parameters" kwargs...]] -> + vst1_value(vcx, f) & + allv(vst1_call_arg, vcx, args) & + allv(vst1_call_kwarg, vcx, kwargs) + [K"dotcall" f [K"do" _...] args...] -> + vst1_value(vcx, f) & + allv(vst1_call_arg, vcx, args) & + vst1_do(vcx, st[2]) + [K"dotcall" f args...] -> + vst1_value(vcx, f) & + allv(vst1_call_arg, vcx, args) + _ -> fail(vcx, st, "invalid `call` form") +end + +# Arg to call (not function decl), pre-semicolon. This can be anything, but +# assignments (interpreted as kwargs) must have a simple LHS, and splat is OK. +vst1_call_arg(vcx, st) = @stm st begin + [K"=" id val] -> vst1_ident(vcx, id) & vst1_value(vcx, val) + _ -> vst1_splat_or_val(vcx, st) # complex assignment in `...` is OK +end + +# Arg to `parameters` (post-semicolon) in a call (not function decl) can be: +# - ident +# - ident=value +# - :ident=>value +# - value... +vst1_call_kwarg(vcx, st) = @stm st begin + (_, when=cst1_ident(vcx, st)) -> true + [K"=" id val] -> vst1_ident(vcx, id) & vst1_value(vcx, val) + # TODO: this call isn't necessarily infix, and we should check name_val is => + [K"call" [K"quote" id] arrow v] -> vst1_ident(vcx, id) & + vst1_ident(vcx, arrow) & vst1_value(v) + [K"..." x] -> vst1_value(vcx, x) + _ -> fail(vcx, st, "malformed `parameters`; expected identifier, `=`, or, `...`") +end + +vst1_lam(vcx, st) = @stm st begin + [K"->" l r] -> + vst1_lam_lhs(with(vcx; return_ok=false, toplevel=false, in_gscope=false), l) & + vst1_stmt(with(vcx; return_ok=true, toplevel=false, in_gscope=false), r) + _ -> false +end + +vst1_lam_lhs(vcx, st) = @stm st begin + [K"tuple" ps... [K"parameters" _...]] -> + _calldecl_positionals(vcx, ps) & vst1_calldecl_kws(vcx, st[end]) + [K"tuple" ps...] -> _calldecl_positionals(vcx, ps) + [K"where" ps t] -> vst1_lam_lhs(vcx, ps) & vst1_where_rhs(vcx, t) + # ast TODO: This is handled badly in the parser + [K"block" ps...] -> allv(vst1_param_kw, vcx, ps) + _ -> fail(vcx, st, "malformed `->` expression") +end + +vst1_do(vcx, st) = @stm st begin + [K"do" [K"tuple" ps...] b] -> + allv(vst1_param_req, with(vcx; return_ok=false, toplevel=false, in_gscope=false), ps) & + vst1_block(with(vcx; return_ok=true, toplevel=false, in_gscope=false), b) + _ -> fail(vcx, st, "malformed `do` expression") +end + +vst1_function(vcx, st) = @stm st begin + [K"function" name] -> vst1_ident(vcx, name) + [K"function" callex body] -> + vst1_function_calldecl(with(vcx; return_ok=false, toplevel=false, in_gscope=false), callex) & + # usually a block, but not in the function-= case + # TODO: should body be a value? + vst1_stmt(with(vcx; return_ok=true, toplevel=false, in_gscope=false), body) + _ -> fail(vcx, st, "malformed `function`") +end + +# Note that we consistently refer to children of a declaring call as +# "parameters" rather than arguments (and children of a K"parameters" block as +# "keyword args/params") so we don't mix them up with children to a real call, +# whose valid forms are subtly different. + +vst1_function_calldecl(vcx, st) = @stm st begin + [K"where" callex tvs] -> + vst1_function_calldecl(vcx, callex) & vst1_where_rhs(vcx, tvs) + [K"::" callex rt] -> + vst1_simple_calldecl(vcx, callex) & vst1_value(vcx, rt) + _ -> vst1_simple_calldecl(vcx, st) +end + +vst1_simple_calldecl(vcx, st; in_macro=false) = @stm st begin + [K"call" f ps... [K"parameters" _...]] -> vst1_calldecl_name(vcx, f) & + _calldecl_positionals(vcx, ps) & + vst1_calldecl_kws(vcx, st[end]) + [K"call" f ps...] -> vst1_calldecl_name(vcx, f) & + _calldecl_positionals(vcx, ps) + + # anonymous function syntax `function (x)` ?! + [K"tuple" ps... [K"parameters" _...]] -> _calldecl_positionals(vcx, ps) & + vst1_calldecl_kws(vcx, st[end]) + [K"tuple" ps...] -> _calldecl_positionals(vcx, ps) + + [K"dotcall" _...] -> fail(vcx, st, "cannot define function using `.` broadcast syntax") + _ -> fail(vcx, st, "malformed `call` in function decl") +end + +vst1_macro(vcx, st) = @stm st begin + (_, when=!vcx.in_gscope) -> fail(vcx, st, "macro definition is not allowed in a local scope") + [K"macro" m] -> vst1_ident(vcx, m) + [K"macro" [K"call" _... [K"parameters" _...]] _...] -> + fail(vcx, st[1][end], "macros cannot accept keyword arguments") + [K"macro" [K"call" m ps...] body] -> + let vcx = with(vcx; return_ok=false, toplevel=false, in_gscope=false) + vst1_macro_calldecl_name(vcx, m) & + _calldecl_positionals(vcx, ps) & + vst1_block(with(vcx; return_ok=true), body) + end + [K"macro" [K"where" _...] _...] -> + fail(vcx, st[1], "`where` not allowed in macro signatures") +end + +vst1_macro_calldecl_name(vcx, st) = @stm st begin + (_, when=cst1_ident(vcx, st)) -> true + [K"." l r] -> vst1_dotpath(vcx, l) & vst1_dot_rhs(vcx, r) + _ -> fail(vcx, st, "invalid macro name") +end + +vst1_calldecl_name(vcx, st) = @stm st begin + (_, when=cst1_ident(vcx, st)) -> true + [K"." l r] -> vst1_dotpath(vcx, l) & vst1_dot_rhs(vcx, r) + [K"curly" t tvs...] -> vst1_calldecl_name(vcx, t) & allv(vst1_value, vcx, tvs) + [K"Value"] -> true # GlobalRef works. Function? Type? + # callable type + [K"::" t] -> vst1_value(vcx, t) + [K"::" x t] -> vst1_ident(vcx, x) & vst1_value(vcx, t) + _ -> fail(vcx, st, "invalid function name") +end + +# Check mandatory and optional positional params. Another case of @stm being too +# limited: `[required_param* opt_param* (= (... required_param) val)|(... required_param)?]` +function _calldecl_positionals(vcx, params) + require_assign = false + ok = true + if !isempty(params) + maybe_va = params[end] + # (= (... required_param) val) + if kind(maybe_va) === K"=" && numchildren(maybe_va) === 2 + ok &= vst1_splat_or_val(vcx, maybe_va[2]) + maybe_va = maybe_va[1] + end + # (... required_param) + if kind(maybe_va) === K"..." + numchildren(maybe_va) !== 1 && fail(vcx, maybe_va, "invalid `...` form") + # TODO: can this actually be a tuple? + ok &= vst1_param_req_or_tuple(vcx, maybe_va[1]) + params = params[1:end-1] + end + end + for p in params + if kind(p) === K"=" + require_assign = true + ok &= vst1_param_opt(vcx, p) + elseif kind(p) === K"..." + ok = fail(vcx, p, "`...` may only be used on the final parameter") + elseif require_assign # TODO: multi-syntaxtree error + ok = fail(vcx, p, "all function parameters after an optional parameter must also be optional") + else + ok &= vst1_param_req_or_tuple(vcx, p) + end + end + return ok +end + +# destructuring args: function f(a, (x, y)) ... +vst1_param_req_or_tuple(vcx, st) = @stm st begin + [K"::" [K"tuple" _...] t] -> + vst1_simple_tuple_lhs(vcx, st[1]) & vst1_value(vcx, t) + [K"tuple" _...] -> vst1_simple_tuple_lhs(vcx, st) + _ -> vst1_param_req(vcx, st) +end + +vst1_simple_tuple_lhs(vcx, st) = @stm st begin + [K"tuple" [K"parameters" kws...]] -> allv(vst1_ident, vcx, kws; lhs=true) + [K"tuple" xs...] -> allv(vst1_simple_tuple_lhs, vcx, xs) + _ -> vst1_ident(vcx, st; lhs=true) +end + +vst1_param_req(vcx, st) = @stm st begin + (_, when=vst1_ident(with(vcx; speculative=true), st; lhs=true)) -> true + [K"::" x t] -> vst1_ident(vcx, x; lhs=true) & vst1_value(vcx, t) + [K"::" t] -> vst1_value(vcx, t) + _ -> fail(vcx, st, "expected identifier or `::`") +end + +vst1_param_opt(vcx, st) = @stm st begin + [K"=" id val] -> vst1_param_req_or_tuple(vcx, id) & vst1_value(vcx, val) + _ -> fail(vcx, st, "malformed optional positional parameter; expected `=`") +end + +vst1_calldecl_kws(vcx, st) = @stm st begin + [K"parameters" kws... [K"..." varkw]] -> + allv(vst1_param_kw, vcx, kws) & vst1_param_req(vcx, varkw) + [K"parameters" kws...] -> allv(vst1_param_kw, vcx, kws) +end + +vst1_param_kw(vcx, st) = @stm st begin + [K"=" id val] -> vst1_param_req(vcx, id) & vst1_value(vcx, val) + (_, when=vst1_param_req(with(vcx; speculative=true), st)) -> true + [K"..." _...] -> fail(vcx, st, "`...` may only be used for the last keyword parameter") + _ -> fail(vcx, st, "malformed keyword parameter; expected identifier, `=`, or `::`") +end + +vst1_where_rhs(vcx, st) = @stm st begin + [K"braces" tvs...] -> allv(vst1_typevar_decl, vcx, tvs) + _ -> vst1_typevar_decl(vcx, st) +end + +vst1_typevar_decl(vcx, st) = @stm st begin + (_, when=cst1_ident(vcx, st)) -> true + [K"<:" [K"Identifier"] old] -> vst1_value(vcx, old) + [K">:" [K"Identifier"] old] -> vst1_value(vcx, old) + ([K"comparison" old1 [K"Identifier"] [K"Identifier"] [K"Identifier"] old2], + when=st[2].name_val===st[4].name_val && st[2].name_val in ("<:", ">:")) -> + vst1_value(vcx, old1) & vst1_value(vcx, old2) + + [K"<:" x _] -> fail(vcx, x, "expected type name") + [K">:" x _] -> fail(vcx, x, "expected type name") + [K"comparison" _...] -> fail(vcx, st, "expected `lb <: type_name <: ub` or `ub >: type_name >: lb`") + _ -> fail(vcx, st, "expected type name or type bounds") +end + +vst1_typesig(vcx, st) = @stm st begin + (_, when=cst1_ident(vcx, st)) -> true + [K"<:" [K"curly" name tvs...] super] -> + vst1_ident(vcx, name) & + vst1_value(vcx, super) & + allv(vst1_typevar_decl, vcx, tvs) + [K"curly" name tvs...] -> vst1_ident(vcx, name) & allv(vst1_typevar_decl, vcx, tvs) + [K"<:" name super] -> vst1_ident(vcx, name) & vst1_value(vcx, super) + _ -> fail(vcx, st, "invalid type signature") +end + +# normal, non-lhs curly may have implicit `(<: t)` +vst1_value_curly_typevar(vcx, st) = @stm st begin + [K"<:" t] -> vst1_splat_or_val(vcx, t) + [K">:" t] -> vst1_splat_or_val(vcx, t) + _ -> vst1_splat_or_val(vcx, st) +end + +vst1_struct_field(vcx, st) = @stm st begin + (_, when=cst1_ident(vcx, st)) -> true + [K"block" x] -> vst1_struct_field(vcx, x) + [K"::" x t] -> vst1_struct_field(vcx, x) & vst1_value(vcx, t) + [K"const" x] -> vst1_struct_field(vcx, x) + [K"atomic" x] -> vst1_struct_field(vcx, x) + _ -> vst1_stmt(vcx, st) +end + +# TODO count(kind(x)===k for x in xs) +# working around a revise bug that fails with anonymous functions +function _num_of_kind(xs, k) + n = 0 + for x in xs + kind(x) === k && (n += 1) + end + return n +end + +# syntax TODO: (local/global (= lhs rhs)) forms should probably reject the same lhss as const (ref and .) +# TODO: We can do some destructuring checks here (e.g. fail `(a,b,c) = (1,2)`) +# compat: +# - call (only within a tuple using JuliaSyntax) can declare a function with +# arguments, but can't use them on the rhs +# - in curly, typevars are checked for structure, but not used. +vst1_assign_lhs(vcx, st; in_const=false, in_tuple=false, disallow_type=false) = @stm st begin + [K"tuple" [K"parameters" xs...]] -> allv(vst1_symdecl, vcx, xs) + [K"tuple" xs...] -> + allv(vst1_assign_lhs, vcx, xs; in_const, in_tuple=true) & + (_num_of_kind(xs, K"...") <= 1 || + fail(vcx, st, "multiple `...` in destructuring assignment are ambiguous")) + # Parser produces this in too many places + # [K"call" _...] -> + # fail(vcx, st, "`(= (call ...) ...)` syntax is deprecated, use `(function (call ...) ...)`") + # type-annotated tuple segfaults, haha + # [K"::" [K"tuple" _...] t] -> ??? + _ -> vst1_assign_lhs_nontuple(vcx, st; in_const, in_tuple) +end +vst1_assign_lhs_nontuple(vcx, st; in_const=false, in_tuple=false, disallow_type=false) = @stm st begin + (_, when=vst1_ident(with(vcx; speculative=true), st; lhs=true)) -> true + (_, when=(in_const && kind(st) in (K".", K"ref"))) -> + fail(vcx, st, "cannot declare this form constant") + ([K"Value"], when=st.value isa GlobalRef) -> true + ([K"::" x t], when=!disallow_type) -> + vst1_assign_lhs(vcx, x; in_const, in_tuple) & vst1_value(vcx, t) + [K"call" _...] -> vst1_function_calldecl(vcx, st) + [K"." x y] -> vst1_value(vcx, x) & vst1_dot_rhs(vcx, y) + [K"..." x] -> !in_tuple ? + fail(vcx, st, "splat on left side of assignment must be in a tuple") : + vst1_assign_lhs(vcx, x; in_const, in_tuple) + [K"ref" x is...] -> vst1_assign_lhs_nontuple(vcx, x; in_const, in_tuple) & + allv(vst1_splat_or_val, vcx, is) + [K"curly" t tvs...] -> vst1_ident(vcx, t; lhs=true) & + allv(vst1_typevar_decl, vcx, tvs) + + # errors + ([K"::" _...], when=disallow_type) -> + fail(vcx, st, "type declarations for global variables must be at top level, not inside a function") + [K"typed_hcat" _...] -> fail(vcx, st, "invalid spacing on left side of indexed assignment") + [K"typed_vcat" _...] -> fail(vcx, st, "unexpected `;` in left side of indexed assignment") + [K"typed_ncat" _...] -> fail(vcx, st, "unexpected `;` in left side of indexed assignment") + ([K"parameters" _...], when=in_tuple) -> + fail(vcx, st, "property destructuring must use a single `;` before the property names, eg `(; a, b) = rhs`") + (_, when=(kind(st) in KSet"vect hcat vcat ncat")) -> fail(vcx, st, "use `(a, b) = ...` to assign multiple values") + _ -> fail(vcx, st, "invalid `$(kind(st))` on left side of assignment") +end + +# dot-assign with placeholders in an arraylike lhs throws a syntax error +vst1_dotassign_lhs(vcx, st) = + vst1_arraylike(with(vcx; speculative=true), st) || vst1_assign_lhs(vcx, st) + +vst1_arraylike(vcx, st) = @stm st begin + # TODO: more validation is possible here, e.g. when row/nrow can/can't show up in ncat + [K"vect" xs...] -> allv(vst1_splat_or_val, vcx, xs) + [K"hcat" xs...] -> allv(vst1_splat_or_val, vcx, xs) + [K"vcat" xs...] -> allv(vst1_splat_or_val, vcx, xs) + [K"ncat" xs...] -> allv(vst1_splat_or_val, vcx, xs) + [K"ref" x is...] -> vst1_value(vcx, x) & allv(vst1_splat_or_val, vcx, is) + [K"typed_hcat" t xs...] -> vst1_value(vcx, t) & allv(vst1_splat_or_val, vcx, xs) + [K"typed_vcat" t xs...] -> vst1_value(vcx, t) & allv(vst1_splat_or_val, vcx, xs) + [K"typed_ncat" t xs...] -> vst1_value(vcx, t) & allv(vst1_splat_or_val, vcx, xs) + [K"row" xs...] -> allv(vst1_splat_or_val, vcx, xs) + [K"nrow" xs...] -> allv(vst1_splat_or_val, vcx, xs) + _ -> false +end + +vst1_splat_or_val(vcx, st) = @stm st begin + [K"..." x] -> vst1_splat_or_val(vcx, x) + [K"..." _...] -> fail("expected one argument to `...`") + _ -> vst1_value(vcx, st) +end + +function vst1_generator(vcx, st) + vcx = with(vcx; return_ok=false, toplevel=false, in_gscope=false) + return @stm st begin + [K"generator" val its...] -> + vst1_value(vcx, val) & + allv(vst1_iterspec, vcx, its) + _ -> fail(vcx, st, "malformed `generator`") + end +end + +vst1_iterspec(vcx, st) = @stm st begin + [K"filter" [K"iteration" is...] cond] -> allv(vst1_iter, vcx, is) & vst1_value(vcx, cond) + [K"iteration" is...] -> allv(vst1_iter, vcx, is) + _ -> fail(vcx, st, "malformed `iteration`") +end + +vst1_iter(vcx, st) = @stm st begin + [K"in" [K"outer" i] v] -> vst1_assign_lhs(vcx, i) & vst1_value(vcx, v) + [K"in" i v] -> vst1_assign_lhs(vcx, i) & vst1_value(vcx, v) + _ -> fail(vcx, st, "malformed `in`") +end + +vst1_documented(vcx, st) = @stm st begin + (_, when=kind(st) in KSet"function macro = struct abstract primitive const global Symbol inert module") -> + vst1_stmt(vcx, st) + # doc-only cases + (_, when=kind(st) in KSet". Identifier tuple") -> vst1_stmt(vcx, st) + (callex, when=vst1_function_calldecl(with(vcx; speculative=true), st)) -> true + _ -> fail(vcx, st, "`$(kind(st))` cannot be annotated with a docstring") +end + +vst1_foreigncall_fname(vcx, st) = @stm st begin + [K"Symbol"] -> true + [K"Identifier"] -> true + [K"static_eval" x] -> vst1_foreigncall_fname(vcx, x) + [K"tuple" xs...] -> allv(vst1_value, vcx, xs) # TODO + _ -> fail(vcx, st, "invalid foreigncall function name") +end + +""" +For convenience, much of the validation code for st0 (non-macro-expanded trees) is +shared with that for st1 (macro-expanded trees). The main differences: +- quote/unquote is valid in st0 but not st1 +- macrocalls are valid in st0 but not st1 +- any of the ad-hoc pre-desugaring we do in expand_forms_1 + +Even though st0 should usually be limited to parser output, `valid_st0` allows a +superset of `vst1_stmt` to allow for validation of partially-expanded trees. +""" +function valid_st0(st::SyntaxTree) + vcx = with(Validation1Context(), unexpanded=true) + valid = vst1_stmt(vcx, st) + for e in vcx.errors + showerror(stdout, LoweringError(e.sts[1], e.msg)) + for st_ in e.sts[2:end]; show(st_); end + printstyled("---------------------------------\n"; color=:red) + end + return valid +end +valid_st0(vcx, st) = @stm st begin + _ -> vst1_stmt(with(vcx; unexpanded=true), st) +end + +""" +TODO: While we can't validate any arguments to a macrocall in general, it would +make sense to check usage for things like @ccall. +""" +vst0_macrocall(vcx, st) = @stm st begin + [K"macrocall" name args...] -> vst0_macro_name(vcx, name) + _ -> false +end + +vst0_quoted(vcx, st) = @stm st begin + ([K"$" x], when=vcx.quote_level===1) -> valid_st0(with(vcx; quote_level=0), x) + [K"$" x] -> vst0_quoted(with(vcx; quote_level=vcx.quote_level-1), x) + [K"quote" x] -> vst0_quoted(with(vcx; quote_level=vcx.quote_level+1), x) + _ -> allv(vst0_quoted, vcx, children(st)) +end + +# Currently julia allows arbitrary top-level code in the first argument of a +# macrocall. It's probably OK to restrict this. +vst0_macro_name(vcx, st) = @stm st begin + [K"." modpath [K"macro_name" mname]] -> (vst1_dotpath(vcx, modpath) & vst1_ident(vcx, mname)) + [K"macro_name" [K"." modpath mname]] -> (vst1_dotpath(vcx, modpath) & vst1_ident(vcx, mname)) + [K"Value"] -> true + _ -> fail(vcx, st, "invalid macro name") +end + +# Non-lazy `&` to fetch errors from all subtrees in the iterable +function allv(f, vcx, itr; kws...) + ok = true + for i in itr + ok &= f(vcx, i; kws...) + end + return ok +end + +# `known` is true if validation passes or knows what's wrong; false otherwise. +# Allows for a "match if `vst1_foo` knows what this is supposed to be" pattern: +# +# (_, when=(kp=pass_or_err(vst1_foo, vcx, st); kp.known)) -> kp.pass +# +# which is similar to splicing in all cases from `vst1_foo` that either pass or +# produce an error (i.e. `_ -> false` cases are ignored). +function pass_or_err(f, vcx, st; kws...) + n_err = length(vcx.errors) + pass = f(vcx, st; kws...) + known = pass || length(vcx.errors) > n_err + return (; known, pass) +end + +function fail(vcx::Validation1Context, st::SyntaxTree, msg="invalid syntax") + if !vcx.speculative + push!(vcx.errors, ValidationDiagnostic(st, msg)) + end + return false +end + +vtodo(vcx, st, line=0) = (push!(vcx.errors, ValidationDiagnostic(st, "TODO: line $line")); false) diff --git a/test/arrays_ir.jl b/test/arrays_ir.jl index 4595603..a184e65 100644 --- a/test/arrays_ir.jl +++ b/test/arrays_ir.jl @@ -34,7 +34,7 @@ #--------------------- LoweringError: [10, 20; 30] -# └──┘ ── unexpected semicolon in array expression +# └──┘ ── unexpected keyword-separating semicolon outside of call or tuple ######################################## # Error: vect syntax with embedded assignments @@ -390,7 +390,7 @@ a[i, j; w=1] #--------------------- LoweringError: a[i, j; w=1] -# └───┘ ── unexpected semicolon in array expression +# └───┘ ── unexpected keyword-separating semicolon outside of call or tuple ######################################## # simple setindex! diff --git a/test/assignments_ir.jl b/test/assignments_ir.jl index 2b002fb..a27d181 100644 --- a/test/assignments_ir.jl +++ b/test/assignments_ir.jl @@ -169,7 +169,7 @@ a.(b) = rhs #--------------------- LoweringError: a.(b) = rhs -└───┘ ── invalid dot call syntax on left hand side of assignment +└───┘ ── invalid `dotcall` on left side of assignment ######################################## # Error: Invalid lhs in `=` @@ -177,7 +177,7 @@ T[x y] = rhs #--------------------- LoweringError: T[x y] = rhs -└────┘ ── invalid spacing in left side of indexed assignment +└────┘ ── invalid spacing on left side of indexed assignment ######################################## # Error: Invalid lhs in `=` @@ -233,7 +233,7 @@ LoweringError: #--------------------- LoweringError: 1 = rhs -╙ ── invalid assignment location +╙ ── invalid `Integer` on left side of assignment ######################################## # Basic updating assignment @@ -358,4 +358,4 @@ f() += y #--------------------- LoweringError: (if false end, b) += 2 -└───────────────┘ ── invalid multiple assignment location +#└──────────┘ ── invalid `if` on left side of assignment diff --git a/test/branching_ir.jl b/test/branching_ir.jl index f7a63f4..eeaca60 100644 --- a/test/branching_ir.jl +++ b/test/branching_ir.jl @@ -236,4 +236,4 @@ x = @label foo #--------------------- LoweringError: x = @label foo -# └─┘ ── misplaced label in value position +# └─┘ ── misplaced `symbolic_label` in value position diff --git a/test/decls_ir.jl b/test/decls_ir.jl index 1092b4d..4b25186 100644 --- a/test/decls_ir.jl +++ b/test/decls_ir.jl @@ -85,7 +85,7 @@ y = global x #--------------------- LoweringError: y = global x -# ╙ ── global declaration doesn't read the variable and can't return a value +# └──────┘ ── global declaration doesn't read the variable and can't return a value ######################################## # const @@ -224,7 +224,7 @@ const local x = 1 #--------------------- LoweringError: const local x = 1 -└───────────────┘ ── unsupported `const local` declaration +# └──────────┘ ── locals cannot be declarated `const` ######################################## # Error: Const not supported on locals @@ -235,7 +235,7 @@ end LoweringError: let const x = 1 -# └────┘ ── unsupported `const` declaration on local variable +# └─────────┘ ── unsupported `const` declaration on local variable end ######################################## diff --git a/test/destructuring_ir.jl b/test/destructuring_ir.jl index 990096a..231135c 100644 --- a/test/destructuring_ir.jl +++ b/test/destructuring_ir.jl @@ -87,7 +87,7 @@ end #--------------------- LoweringError: (xs..., ys...) = x -# └────┘ ── multiple `...` in destructuring assignment are ambiguous +└────────────┘ ── multiple `...` in destructuring assignment are ambiguous ######################################## # Recursive destructuring @@ -376,7 +376,7 @@ end #--------------------- LoweringError: (x ; a, b) = rhs -└────────┘ ── Property destructuring must use a single `;` before the property names, eg `(; a, b) = rhs` +# └─────┘ ── property destructuring must use a single `;` before the property names, eg `(; a, b) = rhs` ######################################## # Error: Property destructuring with values for properties @@ -384,4 +384,4 @@ LoweringError: #--------------------- LoweringError: (; a=1, b) = rhs -# └─┘ ── invalid assignment location +# └─┘ ── expected identifier or `::` diff --git a/test/function_calls_ir.jl b/test/function_calls_ir.jl index f2772a6..dbcdb7a 100644 --- a/test/function_calls_ir.jl +++ b/test/function_calls_ir.jl @@ -559,7 +559,7 @@ cglobal = 10 #--------------------- LoweringError: cglobal = 10 -└─────┘ ── invalid assignment location +└─────┘ ── invalid `core` on left side of assignment ######################################## # Error: assigning to `ccall` @@ -567,7 +567,7 @@ ccall = 10 #--------------------- LoweringError: ccall = 10 -└───┘ ── invalid assignment location +└───┘ ── invalid `core` on left side of assignment ######################################## # Error: assigning to `var"ccall"` @@ -575,7 +575,7 @@ var"ccall" = 10 #--------------------- LoweringError: var"ccall" = 10 -# └───┘ ── invalid assignment location +# └───┘ ── invalid `core` on left side of assignment ######################################## # Error: Invalid function name ccall @@ -584,7 +584,7 @@ end #--------------------- LoweringError: function ccall() -# └───┘ ── Invalid function name +# └───┘ ── invalid function name end ######################################## @@ -594,7 +594,7 @@ end #--------------------- LoweringError: function A.ccall() -# └─────┘ ── Invalid function name +# └───┘ ── this is a reserved identifier end ######################################## @@ -650,4 +650,4 @@ tuple(((xs...)...)...) #--------------------- LoweringError: (xs...) -#└───┘ ── `...` expression outside call +#└───┘ ── unexpected splat not in `call`, `tuple`, `curly`, or array expression diff --git a/test/functions_ir.jl b/test/functions_ir.jl index a537757..ffca6f4 100644 --- a/test/functions_ir.jl +++ b/test/functions_ir.jl @@ -132,7 +132,7 @@ end #--------------------- LoweringError: function f(xs..., y) -# └───┘ ── `...` may only be used for the last positional argument +# └───┘ ── `...` may only be used on the final parameter body end @@ -390,7 +390,7 @@ end #--------------------- LoweringError: function (.+)(x,y) -# └───────┘ ── Cannot define function using `.` broadcast syntax +# └───────┘ ── cannot define function using `.` broadcast syntax end ######################################## @@ -400,7 +400,7 @@ end #--------------------- LoweringError: function f[](x,y) -# └─┘ ── Invalid function name +# └─┘ ── invalid function name end ######################################## @@ -746,7 +746,7 @@ end #--------------------- LoweringError: function f(x=1, ys, z=2) -# └─┘ ── optional positional arguments must occur at end +# └┘ ── all function parameters after an optional parameter must also be optional ys end @@ -1469,7 +1469,7 @@ end #--------------------- LoweringError: function f_kw_destruct(; (x,y)=10) -# └───┘ ── Invalid keyword name +# └───┘ ── expected identifier or `::` end ######################################## @@ -1479,7 +1479,7 @@ end #--------------------- LoweringError: function f_kw_slurp_default(; kws...=def) -# └────────┘ ── keyword argument with `...` cannot have a default value +# └────┘ ── expected identifier or `::` end ######################################## @@ -1499,7 +1499,7 @@ end #--------------------- LoweringError: function f_kw_slurp_not_last(; kws..., x=1) -# └────┘ ── `...` may only be used for the last keyword argument +# └────┘ ── `...` may only be used for the last keyword parameter end ######################################## diff --git a/test/generators_ir.jl b/test/generators_ir.jl index 28f0241..6e7ecd5 100644 --- a/test/generators_ir.jl +++ b/test/generators_ir.jl @@ -128,9 +128,9 @@ LoweringError: #--------------------- 1 (call core.svec) 2 (call core.svec) -3 (call JuliaLowering.eval_closure_type TestMod :#->##5 %₁ %₂) +3 (call JuliaLowering.eval_closure_type TestMod :#->##4 %₁ %₂) 4 latestworld -5 TestMod.#->##5 +5 TestMod.#->##4 6 (call core.svec %₅ core.Any) 7 (call core.svec) 8 SourceLocation::1:2 @@ -150,7 +150,7 @@ LoweringError: 11 TestMod.body 12 (return %₁₁) 11 latestworld -12 TestMod.#->##5 +12 TestMod.#->##4 13 (new %₁₂) 14 TestMod.iter 15 (call top.Generator %₁₃ %₁₄) @@ -162,9 +162,9 @@ LoweringError: #--------------------- 1 (call core.svec) 2 (call core.svec) -3 (call JuliaLowering.eval_closure_type TestMod :#->##6 %₁ %₂) +3 (call JuliaLowering.eval_closure_type TestMod :#->##5 %₁ %₂) 4 latestworld -5 TestMod.#->##6 +5 TestMod.#->##5 6 (call core.svec %₅ core.Any) 7 (call core.svec) 8 SourceLocation::1:4 @@ -174,7 +174,7 @@ LoweringError: 1 (call JuliaLowering.interpolate_ast SyntaxTree (inert (return x))) 2 (return %₁) 11 latestworld -12 TestMod.#->##6 +12 TestMod.#->##5 13 (new %₁₂) 14 TestMod.iter 15 (call top.Generator %₁₃ %₁₄) @@ -194,7 +194,7 @@ LoweringError: #--------------------- 1 (call core.svec) 2 (call core.svec) -3 (call JuliaLowering.eval_closure_type TestMod :#->##7 %₁ %₂) +3 (call JuliaLowering.eval_closure_type TestMod :#->##6 %₁ %₂) 4 latestworld 5 (call core.svec) 6 (call core.svec) @@ -212,7 +212,7 @@ LoweringError: 3 slot₃/x 4 (return %₃) 15 latestworld -16 TestMod.#->##7 +16 TestMod.#->##6 17 (call core.svec %₁₆ core.Any) 18 (call core.svec) 19 SourceLocation::1:2 @@ -226,7 +226,7 @@ LoweringError: 5 (call top.Generator %₂ %₄) 6 (return %₅) 22 latestworld -23 TestMod.#->##7 +23 TestMod.#->##6 24 (new %₂₃) 25 TestMod.: 26 (call %₂₅ 1 3) diff --git a/test/loops_ir.jl b/test/loops_ir.jl index 709322a..8c5f5a9 100644 --- a/test/loops_ir.jl +++ b/test/loops_ir.jl @@ -119,7 +119,7 @@ break #--------------------- LoweringError: break -└───┘ ── break must be used inside a `while` or `for` loop +└───┘ ── `break` must be used inside a `while` or `for` loop ######################################## # Error: continue outside for/while @@ -127,7 +127,7 @@ continue #--------------------- LoweringError: continue -└──────┘ ── continue must be used inside a `while` or `for` loop +└──────┘ ── `continue` must be used inside a `while` or `for` loop ######################################## # Error: `outer` without outer local variable diff --git a/test/macros_ir.jl b/test/macros_ir.jl index 183dce3..481ca96 100644 --- a/test/macros_ir.jl +++ b/test/macros_ir.jl @@ -123,7 +123,7 @@ let # ┌──────────── macro foo(ex) end -#─────┘ ── macro is only allowed in global scope +#─────┘ ── macro definition is not allowed in a local scope end ######################################## @@ -138,7 +138,7 @@ function f() # ┌────────── macro foo() end -#─────┘ ── macro is only allowed in global scope +#─────┘ ── macro definition is not allowed in a local scope end ######################################## diff --git a/test/misc_ir.jl b/test/misc_ir.jl index ffffd6e..ca79da0 100644 --- a/test/misc_ir.jl +++ b/test/misc_ir.jl @@ -34,7 +34,7 @@ x."b" @ast_ [K"." "x"::K"Identifier" "a"::K"Identifier" 3::K"Integer"] #--------------------- LoweringError: -#= line 1 =# - `.` form requires either one or two children +#= line 1 =# - invalid syntax: unknown kind `.` or number of arguments (3) ######################################## # Error: Placeholder value used @@ -148,7 +148,7 @@ LoweringError: #--------------------- LoweringError: (a=1; b=2, c=3) -# └────────┘ ── unexpected semicolon in tuple - use `,` to separate tuple elements +# └────────┘ ── cannot mix tuple `(a,b,c)` and named tuple `(;a,b,c)` syntax ######################################## # Error: Named tuple field dots in rhs @@ -156,7 +156,7 @@ LoweringError: #--------------------- LoweringError: (; a=xs...) -# └───┘ ── `...` cannot be used in a value for a named tuple field +# └───┘ ── unexpected splat not in `call`, `tuple`, `curly`, or array expression ######################################## # Error: Named tuple field invalid lhs @@ -164,7 +164,7 @@ LoweringError: #--------------------- LoweringError: (; a[]=1) -# └─┘ ── invalid named tuple field name +# └─┘ ── expected identifier, got `ref` ######################################## # Error: Named tuple element with weird dot syntax @@ -209,7 +209,7 @@ function f() # ┌─────── module C end -#─────┘ ── `module` is only allowed at top level +#─────┘ ── this syntax is only allowed in top level code end ######################################## @@ -235,7 +235,7 @@ LoweringError: #--------------------- LoweringError: {x, y} -└────┘ ── { } syntax is reserved for future use +└────┘ ── `braces` outside of `where` is reserved for future use ######################################## # Error: braces matrix syntax @@ -243,7 +243,7 @@ LoweringError: #--------------------- LoweringError: {x y; y z} -└────────┘ ── { } syntax is reserved for future use +└────────┘ ── `bracescat` is reserved for future use ######################################## # Error: Test AST which has no source form and thus must have been constructed @@ -251,7 +251,7 @@ LoweringError: @ast_ [K"if"] #--------------------- LoweringError: -#= line 1 =# - expected `numchildren(ex) >= 2` +#= line 1 =# - invalid syntax: unknown kind `if` or number of arguments (0) ######################################## # Error: @atomic in wrong position @@ -262,7 +262,7 @@ end LoweringError: let @atomic x -# └───────┘ ── unimplemented or unsupported atomic declaration +# └───────┘ ── unimplemented or unsupported `atomic` declaration end ######################################## @@ -486,7 +486,7 @@ MacroExpansionError while expanding @ccall in module Main.TestMod: #--------------------- LoweringError: &x -└┘ ── invalid syntax +└┘ ── invalid syntax: unknown kind `&` or number of arguments (1) ######################################## # Error: $ outside quote/string @@ -494,7 +494,7 @@ $x #--------------------- LoweringError: $x -└┘ ── `$` expression outside string or quote block +└┘ ── `$` expression outside string or quote ######################################## # Error: splat outside call @@ -502,7 +502,7 @@ x... #--------------------- LoweringError: x... -└──┘ ── `...` expression outside call +└──┘ ── unexpected splat not in `call`, `tuple`, `curly`, or array expression ######################################## # `include` should increment world age diff --git a/test/quoting_ir.jl b/test/quoting_ir.jl index 5b323c1..f740139 100644 --- a/test/quoting_ir.jl +++ b/test/quoting_ir.jl @@ -40,5 +40,5 @@ end LoweringError: quote $$x + 1 -# └┘ ── `$` expression outside string or quote block +# └┘ ── `$` expression outside string or quote end diff --git a/test/typedefs_ir.jl b/test/typedefs_ir.jl index 260e3c2..8d303fe 100644 --- a/test/typedefs_ir.jl +++ b/test/typedefs_ir.jl @@ -92,7 +92,7 @@ A where X < Y < Z #--------------------- LoweringError: A where X < Y < Z -# └───────┘ ── invalid type bounds +# └───────┘ ── expected `lb <: type_name <: ub` or `ub >: type_name >: lb` ######################################## # Error: bad type bounds @@ -100,7 +100,7 @@ A where X <: f() <: Z #--------------------- LoweringError: A where X <: f() <: Z -# └─┘ ── expected type name +# └───────────┘ ── expected `lb <: type_name <: ub` or `ub >: type_name >: lb` ######################################## # Error: bad type bounds @@ -173,7 +173,7 @@ X{S, T; W} #--------------------- LoweringError: X{S, T; W} -# └─┘ ── unexpected semicolon in type parameter list +# └─┘ ── unexpected keyword-separating semicolon outside of call or tuple ######################################## # Error: assignment in type application @@ -269,7 +269,7 @@ abstract type A(){T} end #--------------------- LoweringError: abstract type A(){T} end -# └────┘ ── invalid type signature +# └─┘ ── expected identifier, got `call` ######################################## # Error: Abstract type definition with bad signature @@ -277,7 +277,7 @@ abstract type A() <: B end #--------------------- LoweringError: abstract type A() <: B end -# └───────┘ ── invalid type signature +# └─┘ ── expected identifier, got `call` ######################################## # Error: Abstract type definition in function scope