From 077f2fa9bac9de7897e938456b0da42b4a6e94d0 Mon Sep 17 00:00:00 2001 From: Em Chu Date: Wed, 24 Dec 2025 13:30:28 -0800 Subject: [PATCH 1/7] Add `@stm` "SyntaxTree match" macro --- JuliaSyntax/src/porcelain/syntax_graph.jl | 235 ++++++++++++++++++++++ JuliaSyntax/test/syntax_graph.jl | 177 +++++++++++++++- 2 files changed, 411 insertions(+), 1 deletion(-) diff --git a/JuliaSyntax/src/porcelain/syntax_graph.jl b/JuliaSyntax/src/porcelain/syntax_graph.jl index fb37e2581e248..08346f8c9046c 100644 --- a/JuliaSyntax/src/porcelain/syntax_graph.jl +++ b/JuliaSyntax/src/porcelain/syntax_graph.jl @@ -740,6 +740,241 @@ function _copy_ast(graph2::SyntaxGraph, graph1::SyntaxGraph, return id2 end +#------------------------------------------------------------------------------- +# AST destruction utilities + +raw""" +Simple `SyntaxTree` pattern matching + +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. + +## 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 in order, 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(JuliaSyntax.SyntaxTree, "function foo(x,y,z); x; end") + +julia> JuliaSyntax.@stm st begin + [K"function" [K"call" fname [K"parameters" kws...]] body] -> + "no positional args, only 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" +``` + +See [Racket `match`](https://docs.racket-lang.org/reference/match.html) for the +inspiration for this macro and an example of a much more featureful pattern +language. +""" +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 + +# 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 + +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(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 e::Expr in extras + (ek, ev) = e.args[1:2] + 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(:&&, :(JuliaSyntax._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(ErrorException(string( + "No match found for `", $st_gs, "` at ", $(string(line))))))) + return esc(out_blk) +end + +function _stm_destruct_pat(per::Expr) + pe, r = per.args[1:2] + Base.remove_linenums!(pe) + return Meta.isexpr(pe, :tuple) ? (pe.args[1], pe.args[2:end], r) : + (pe, Expr[], r) +end + +function _stm_matches(p::Union{Symbol, Expr}, st, debug=false, indent="") + if p isa Symbol + debug && printstyled(string(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( + string(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_dots = length(p_args) - dots_start # -1 if no dots + npats = dots_start + n_after_dots + n_ok = isnothing(dots_i) ? numchildren(st) === npats : + numchildren(st) >= npats - 1 + if !n_ok + debug && printstyled(string( + indent, "[numc]: ", numchildren(st), "!=", npats, "\n"); color=:red) + return false + end + all_ok = true + for i in 1:dots_start-1 + if !_stm_matches(p_args[i], st[i], debug, indent*" ") + all_ok = false; break + end + end + all_ok && for i in n_after_dots-1:-1:0 + if !_stm_matches(p_args[end-i], st[end-i], debug, indent*" ") + all_ok = false; break + end + end + debug && printstyled(string( + indent, all_ok ? "matched " : "nomatch ", st, "\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::Expr) + 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) && length(p.args) >= 1 && + 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 + #------------------------------------------------------------------------------- # RawGreenNode->SyntaxTree # WIP: expr_structure param will be deleted diff --git a/JuliaSyntax/test/syntax_graph.jl b/JuliaSyntax/test/syntax_graph.jl index 42583bab76bd7..8b2991340d9d4 100644 --- a/JuliaSyntax/test/syntax_graph.jl +++ b/JuliaSyntax/test/syntax_graph.jl @@ -1,4 +1,4 @@ -using .JuliaSyntax: SyntaxGraph, SyntaxTree, SyntaxList, freeze_attrs, unfreeze_attrs, ensure_attributes, ensure_attributes!, delete_attributes, copy_ast, attrdefs +using .JuliaSyntax: SyntaxGraph, SyntaxTree, SyntaxList, freeze_attrs, unfreeze_attrs, ensure_attributes, ensure_attributes!, delete_attributes, copy_ast, attrdefs, @stm @testset "SyntaxGraph attrs" begin st = parsestmt(SyntaxTree, "function foo end") @@ -100,3 +100,178 @@ end @test_throws ErrorException copy_ast(new_g, st; copy_source=false) end end + +@testset "@stm SyntaxTree pattern-matching" begin + st = parsestmt(SyntaxTree, "foo(a,b=1,c(d=2))") + # (call foo a (kw b 1) (call c (kw d 2))) + + @testset "basic functionality" begin + @test @stm st begin + _ -> true + end + + @test @stm st begin + x -> x isa SyntaxTree + end + + @test @stm st begin + [K"function" f a b c] -> false + [K"call" f a b c] -> true + end + + @test @stm st begin + [K"function" _ _ _ _] -> false + [K"call" _ _ _ _] -> true + end + + @test @stm st begin + [K"call" f a b] -> false + [K"call" f a b c d] -> false + [K"call" f a b c] -> true + end + + @test @stm st begin + [K"call" f a b c] -> + kind(f) === K"Identifier" && + kind(b) === K"kw" && + kind(c) === K"call" + end + end + + @testset "errors" begin + # no match + @test_throws ErrorException @stm st begin + [K"Identifier"] -> false + end + + # assuming we run this checker by default + @testset "_stm_check_usage" begin + bad = Expr[ + :(@stm st begin + [K"None" a a] -> false + end) + :(@stm st begin + x + end) + :(@stm st begin + x() -> false + end) + :(@stm st begin + (a, b=1) -> false + end) + :(@stm st begin + [K"None" a... b...] -> false + end) + ] + for e in bad + Base.remove_linenums!(e) + @testset "$(string(e))" begin + @test_throws AssertionError macroexpand(@__MODULE__, e) + end + end + end + + end + + @testset "nested patterns" begin + @test 1 === @stm st begin + [K"call" [K"Identifier"] [K"Identifier"] [K"kw" [K"Identifier"] k1] [K"call" [K"Identifier"] [K"kw" [K"Identifier"] k2]]] -> 1 + [K"call" [K"Identifier"] [K"Identifier"] [K"kw" [K"Identifier"] k1] [K"call" [K"Identifier"] [K"kw" _ k2]]] -> 2 + [K"call" [K"Identifier"] [K"Identifier"] [K"kw" _ k1] [K"call" _ _]] -> 3 + [K"call" [K"Identifier"] [K"Identifier"] _ _ ] -> 4 + [K"call" _ _ _ _] -> 5 + end + @test 1 === @stm st begin + [K"call" _ _ [K"None" [K"Identifier"] k1] [K"None" [K"Identifier"] [K"None" [K"None"] k2]]] -> 5 + [K"call" _ _ [K"kw" [K"Identifier"] k1] [K"None" [K"Identifier"] [K"None" [K"None"] k2]]] -> 4 + [K"call" _ _ [K"kw" [K"Identifier"] k1] [K"call" [K"Identifier"] [K"None" [K"None"] k2]]] -> 3 + [K"call" _ _ [K"kw" [K"Identifier"] k1] [K"call" [K"Identifier"] [K"kw" [K"None"] k2]]] -> 2 + [K"call" _ _ [K"kw" [K"Identifier"] k1] [K"call" [K"Identifier"] [K"kw" [K"Identifier"] k2]]] -> 1 + end + @test 1 === @stm st begin + [K"call" _ _ [K"kw" [K"Identifier"] k1] [K"call" [K"Identifier"] [K"kw" [K"Identifier"] k2] bad]] -> 4 + [K"call" _ _ [K"kw" [K"Identifier"] k1] [K"call" [K"Identifier"] [K"kw" [K"Identifier"] k2 bad]]] -> 3 + [K"call" _ _ [K"kw" [K"Identifier"] k1] [K"call" [K"Identifier"] [K"kw" [K"Identifier" bad] k2]]] -> 2 + [K"call" _ _ [K"kw" [K"Identifier"] k1] [K"call" [K"Identifier"] [K"kw" [K"Identifier"] k2]]] -> 1 + end + end + + @testset "SyntaxList splat matching" begin + # trailing splat + @test @stm st begin + [K"call" f _...] -> true + end + @test @stm st begin + [K"call" f args...] -> kind(f) === K"Identifier" + end + @test @stm st begin + [K"call" f args...] -> args isa SyntaxList && length(args) === 3 + end + @test @stm st begin + [K"call" f args...] -> kind(args[1]) === K"Identifier" && + kind(args[2]) === K"kw" && + kind(args[3]) === K"call" + end + @test @stm st begin + [K"call" f a b c empty...] -> empty isa SyntaxList && length(empty) === 0 + end + + # binds after splat + @test @stm st begin + [K"call" f args... last] -> + args isa SyntaxList && + length(args) === 2 + end + @test @stm st begin + [K"call" f args... last] -> + kind(f) === K"Identifier" && + kind(args[1]) === K"Identifier" && + kind(args[2]) === K"kw" && + kind(last) === K"call" + end + @test @stm st begin + [K"call" empty... f a b c] -> empty isa SyntaxList && length(empty) === 0 + end + end + + @testset "`when` clauses affect matching" begin + @test @stm st begin + (_, when=false) -> false + (_, when=false, when=false) -> false + (_, when=false, when=true) -> false + (_, when=true, when=true) -> true + end + @test @stm st begin + ([K"call" _...], when=false) -> false + ([K"call" _...], when=false, when=false) -> false + ([K"call" _...], when=false, when=true) -> false + ([K"call" _...], when=true, when=true) -> true + end + @test @stm st begin + ([K"call" _ _...], when=kind(st[1])===K"Identifier") -> true + end + @test @stm st begin + ([K"call" f _...], when=kind(f)===K"Identifier") -> true + end + end + + @testset "effects of when/`run` clauses" begin + let x = Int[] + @test @stm st begin + (_, when=(push!(x, 1); true), run=push!(x, 2)) -> x == [1,2] + end + @test @stm st begin + (_, when=(push!(x, 3); true), + when=(push!(x, 4); false), + when=(push!(x, 5); true)) -> false + _ -> x == [1,2,3,4] + end + @test @stm st begin + (x_pat, + when=((x_when = x_pat); true), + run=(x_run = x_pat)) -> + x_pat == x_when == x_run + end + end + end +end From 11f35ab496a3707639f0f4848cf0ab0b7ca91caa Mon Sep 17 00:00:00 2001 From: Em Chu Date: Fri, 2 Jan 2026 11:47:34 -0800 Subject: [PATCH 2/7] Support `vcat` form (newline in patterns) in `@stm` --- JuliaSyntax/src/porcelain/syntax_graph.jl | 57 ++++++++++++++++------- JuliaSyntax/test/syntax_graph.jl | 37 ++++++++++++++- 2 files changed, 77 insertions(+), 17 deletions(-) diff --git a/JuliaSyntax/src/porcelain/syntax_graph.jl b/JuliaSyntax/src/porcelain/syntax_graph.jl index 08346f8c9046c..219c3604c4cf2 100644 --- a/JuliaSyntax/src/porcelain/syntax_graph.jl +++ b/JuliaSyntax/src/porcelain/syntax_graph.jl @@ -858,14 +858,24 @@ function _stm(line::LineNumberNode, st, pats; debug=false) return esc(out_blk) end +function _stm_vcat_to_hcat(p::Expr) + Meta.isexpr(p, :vcat) || return p + out = Expr(:hcat) + for a in p.args + Meta.isexpr(a, :row) ? append!(out.args, a.args) : push!(out.args, a) + end + return out +end + function _stm_destruct_pat(per::Expr) pe, r = per.args[1:2] - Base.remove_linenums!(pe) + Base.remove_linenums!(pe) # errors in lhs of `->` are caught in usage check return Meta.isexpr(pe, :tuple) ? (pe.args[1], pe.args[2:end], r) : (pe, Expr[], r) end function _stm_matches(p::Union{Symbol, Expr}, st, debug=false, indent="") + p = Meta.isexpr(p, :vcat) ? _stm_vcat_to_hcat(p) : p if p isa Symbol debug && printstyled(string(indent, p, "=", st, "\n"); color=:yellow) return true @@ -905,14 +915,16 @@ function _stm_matches(p::Union{Symbol, Expr}, st, debug=false, indent="") color=(all_ok ? :green : :red)) return all_ok end - @assert false + @assert false "unexpected syntax; enable or fix `_stm_check_usage`" 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)) + p = Meta.isexpr(p, :vcat) ? _stm_vcat_to_hcat(p) : p + if p isa Symbol + p != :_ && push!(assigns.args, Expr(:(=), p, st_rhs_expr)) + return assigns elseif p isa Expr p_args = filter(e->!(e isa LineNumberNode), p.args)[2:end] dots_i = findfirst(x->Meta.isexpr(x, :(...)), p_args) @@ -928,9 +940,9 @@ function _stm_assigns(p, st_rhs_expr; assigns=Expr(:block)) _stm_assigns(p_args[end-i], :($st_rhs_expr[end-$i]); assigns) end end + return assigns end - return assigns - @assert false + @assert false "unexpected syntax; enable or fix `_stm_check_usage`" end # Check for correct pattern syntax. Not needed outside of development. @@ -945,17 +957,30 @@ function _stm_check_usage(pats::Expr) # 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") + @assert(!dup, "invalid duplicate non-underscore identifier $p") + return true + elseif Meta.isexpr(p, :vect) + @assert(length(p.args) === 1, + "use spaces, not commas, in @stm []-patterns") + elseif Meta.isexpr(p, :hcat) + @assert(length(p.args) >= 2) + elseif Meta.isexpr(p, :vcat) + p = _stm_vcat_to_hcat(p) + @assert(length(p.args) >= 2) + else + @assert(false, "malformed pattern $p") end - return (Meta.isexpr(p, :vect, 1) || - (Meta.isexpr(p, :hcat) && length(p.args) >= 1 && - 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) + @assert(count(x->Meta.isexpr(x, :(...)), p.args[2:end]) <= 1, + "Multiple `...` in a pattern is ambiguous") + + # This exact syntax is not necessary since the kind can't be provided by + # a variable, but requiring [K"kinds"] is consistent and allows us to + # implement list matching later. + @assert(Meta.isexpr(p.args[1], :macrocall, 3) && + p.args[1].args[1] === Symbol("@K_str") && + p.args[1].args[3] isa String, "first pattern elt must be K\"\"") + + return all(x->_stm_check_pattern(x; syms), p.args[2:end]) end @assert Meta.isexpr(pats, :block) "Usage: @st_match st begin; ...; end" diff --git a/JuliaSyntax/test/syntax_graph.jl b/JuliaSyntax/test/syntax_graph.jl index 8b2991340d9d4..641dd50b3edfb 100644 --- a/JuliaSyntax/test/syntax_graph.jl +++ b/JuliaSyntax/test/syntax_graph.jl @@ -147,6 +147,12 @@ end # assuming we run this checker by default @testset "_stm_check_usage" begin bad = Expr[ + :(@stm st begin + [a] -> false + end) + :(@stm st begin + [K"None",a] -> false + end) :(@stm st begin [K"None" a a] -> false end) @@ -170,7 +176,6 @@ end end end end - end @testset "nested patterns" begin @@ -196,6 +201,36 @@ end end end + @testset "vcat form (newlines in pattern)" begin + @test @stm st begin + [K"call" + f + a + b + c] -> true + end + @test @stm st begin + [K"call" + f a b c] -> true + end + @test @stm st begin + [K"call" + + + f a b c] -> true + end + @test @stm st begin + [K"call" + [K"Identifier"] [K"Identifier"] + [K"kw" [K"Identifier"] k1] + [K"call" + [K"Identifier"] + [K"kw" + [K"Identifier"] + k2]]] -> true + end + end + @testset "SyntaxList splat matching" begin # trailing splat @test @stm st begin From 526525df178458b12bd80d1970f2dbcaa2be77cd Mon Sep 17 00:00:00 2001 From: Em Chu Date: Fri, 2 Jan 2026 11:50:54 -0800 Subject: [PATCH 3/7] rm todo --- JuliaSyntax/src/porcelain/syntax_graph.jl | 3 --- 1 file changed, 3 deletions(-) diff --git a/JuliaSyntax/src/porcelain/syntax_graph.jl b/JuliaSyntax/src/porcelain/syntax_graph.jl index 219c3604c4cf2..998080af3a50b 100644 --- a/JuliaSyntax/src/porcelain/syntax_graph.jl +++ b/JuliaSyntax/src/porcelain/syntax_graph.jl @@ -818,9 +818,6 @@ macro stm_debug(st, pats) _stm(__source__, st, pats; debug=true) 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 From c340209a7e9c86c08c20f77963b56d0fa08bf175 Mon Sep 17 00:00:00 2001 From: Em Chu Date: Mon, 5 Jan 2026 14:50:02 -0800 Subject: [PATCH 4/7] Remove multi-`when`, generate match expression, simplify trace --- JuliaSyntax/src/porcelain/syntax_graph.jl | 215 +++++++++++----------- JuliaSyntax/test/syntax_graph.jl | 34 ++-- 2 files changed, 128 insertions(+), 121 deletions(-) diff --git a/JuliaSyntax/src/porcelain/syntax_graph.jl b/JuliaSyntax/src/porcelain/syntax_graph.jl index 998080af3a50b..d04f6de403b2e 100644 --- a/JuliaSyntax/src/porcelain/syntax_graph.jl +++ b/JuliaSyntax/src/porcelain/syntax_graph.jl @@ -771,24 +771,23 @@ 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 in order, stopping if any `when=cond` evaluates to -false. These may not mutate the object being matched. +`cond` to evaluate to `true` for this branch to be taken. `cond` may also bind +variables or printf-debug the matching process, as it runs only when its pattern +matches and no previous branch was taken. `cond` 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. +Every `(pattern, when=cond) -> result` introduces a local scope. Identifiers in +the pattern are let-bound when evaluating `cond` and `result`. `cond` can +introduce variables for use in `result`. User code in `cond` and `result` (but +not `pattern`) can refer to outer variables. ## Example ``` -julia> st = JuliaSyntax.parsestmt(JuliaSyntax.SyntaxTree, "function foo(x,y,z); x; end") +julia> st = JuliaSyntax.parsestmt( + JuliaSyntax.SyntaxTree, "function foo(x,y,z); x; end") julia> JuliaSyntax.@stm st begin [K"function" [K"call" fname [K"parameters" kws...]] body] -> @@ -799,8 +798,8 @@ julia> JuliaSyntax.@stm st begin "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" + (_, when=(show("printf debugging is great"); true)) -> "something else" + _ -> "unreachable due to the case above" end "normal function foo" ``` @@ -824,28 +823,26 @@ 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)), + st_gs, result_gs, k_gs, nc_gs = gensym.("st", "result", "k", "nc") + out_blk = Expr(:let, Expr(:block, :($st_gs = $st::SyntaxTree), + :($result_gs = nothing), + :($k_gs = $kind($st_gs)), + :($nc_gs = $numchildren($st_gs))), 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(per) - # We need to let-bind patvars in both extras and the result, so result + for pcr in pats.args + pcr isa LineNumberNode && (line = pcr; continue) + p, cond, result = _stm_destruct_pat(pcr) + pat_ok = p isa Symbol ? true : _stm_matches(p, st_gs, k_gs, nc_gs, debug) + # We need to let-bind patvars in both cond and the result, so result # needs to live in the first argument of :if with the extra conditions. - e_check = Expr(:&&) - for e::Expr in extras - (ek, ev) = e.args[1:2] - 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(:&&, :(JuliaSyntax._stm_matches($(Expr(:quote, p)), $st_gs, $debug)), - Expr(:let, _stm_assigns(p, st_gs), e_check)), - result_gs) + Expr(:&&, pat_ok, + Expr(:let, _stm_assigns(p, st_gs), + Expr(:&&, cond, + Expr(:block, line, + :($result_gs = $result), true)))), + result_gs) push!(needs_else, case) needs_else = needs_else[3].args end @@ -855,85 +852,91 @@ function _stm(line::LineNumberNode, st, pats; debug=false) return esc(out_blk) end +# recursively flatten `vcat` expressions function _stm_vcat_to_hcat(p::Expr) - Meta.isexpr(p, :vcat) || return p - out = Expr(:hcat) - for a in p.args - Meta.isexpr(a, :row) ? append!(out.args, a.args) : push!(out.args, a) + if Meta.isexpr(p, :vcat) + out = Expr(:hcat) + for a in p.args + Meta.isexpr(a, :row) ? append!(out.args, a.args) : push!(out.args, a) + end + else + out = Expr(p.head, p.args...) + end + for i in eachindex(out.args) + out.args[i] = _stm_vcat_to_hcat(out.args[i]) end return out end - -function _stm_destruct_pat(per::Expr) - pe, r = per.args[1:2] - Base.remove_linenums!(pe) # errors in lhs of `->` are caught in usage check - return Meta.isexpr(pe, :tuple) ? (pe.args[1], pe.args[2:end], r) : - (pe, Expr[], r) -end - -function _stm_matches(p::Union{Symbol, Expr}, st, debug=false, indent="") - p = Meta.isexpr(p, :vcat) ? _stm_vcat_to_hcat(p) : p - if p isa Symbol - debug && printstyled(string(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( - string(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_dots = length(p_args) - dots_start # -1 if no dots - npats = dots_start + n_after_dots - n_ok = isnothing(dots_i) ? numchildren(st) === npats : - numchildren(st) >= npats - 1 - if !n_ok - debug && printstyled(string( - indent, "[numc]: ", numchildren(st), "!=", npats, "\n"); color=:red) - return false - end - all_ok = true - for i in 1:dots_start-1 - if !_stm_matches(p_args[i], st[i], debug, indent*" ") - all_ok = false; break - end - end - all_ok && for i in n_after_dots-1:-1:0 - if !_stm_matches(p_args[end-i], st[end-i], debug, indent*" ") - all_ok = false; break - end - end - debug && printstyled(string( - indent, all_ok ? "matched " : "nomatch ", st, "\n"); - color=(all_ok ? :green : :red)) - return all_ok +_stm_vcat_to_hcat(x) = x + +# return (pat_expr, when_expr|nothing, res_expr) +function _stm_destruct_pat(pcr::Expr) + pc, r = pcr.args[1:2] + Base.remove_linenums!(pc) # errors in lhs of `->` are caught in usage check + (p_vcat, c) = Meta.isexpr(pc, :tuple) ? + (pc.args[1], pc.args[2].args[2]) : (pc, true) + return (_stm_vcat_to_hcat(p_vcat), c, r) +end + +function _stm_matches_wrapper(p::Expr, st_ex, debug) + st_gs, k_gs, nc_gs = gensym.("st", "k", "nc") + Expr(:let, Expr(:block, :($st_gs = $st_ex), + :($k_gs = $kind($st_gs)), + :($nc_gs = $numchildren($st_gs))), + _stm_matches(p, st_gs, k_gs, nc_gs, debug)) +end + +function _stm_matches(p::Expr, st_gs::Symbol, k_gs::Symbol, nc_gs::Symbol, debug) + pat_k = Kind(p.args[1].args[3]) + out = Expr(:&&, :($pat_k === $k_gs)) + debug && push!(out.args, Expr(:block, :(printstyled( + string("[kind]: ", $k_gs, "\n"); color=:yellow)), true)) + + p_args = p.args[2:end] + dots_i = findfirst(x->Meta.isexpr(x, :(...)), p_args) + dots_start = something(dots_i, length(p_args) + 1) + n_after_dots = length(p_args) - dots_start # -1 if no dots + + push!(out.args, isnothing(dots_i) ? + :($nc_gs === $(length(p_args))) : + :($nc_gs >= $(length(p_args) - 1))) + debug && push!(out.args, Expr(:block, :(printstyled( + string("[numc]: ", $nc_gs, "\n"); color=:yellow)), true)) + + for i in 1:dots_start-1 + p_args[i] isa Symbol && continue + push!(out.args, + _stm_matches_wrapper(p_args[i], :($st_gs[$i]), debug)) end - @assert false "unexpected syntax; enable or fix `_stm_check_usage`" + for i in n_after_dots-1:-1:0 + p_args[end-i] isa Symbol && continue + push!(out.args, + _stm_matches_wrapper(p_args[end-i], :($st_gs[end-$i]), debug)) + end + debug && push!(out.args, Expr(:block, :(printstyled( + string("matched: ", $st_gs, " with ", $(QuoteNode(p)), "\n"); + color=:green)), true)) + return out 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)) - p = Meta.isexpr(p, :vcat) ? _stm_vcat_to_hcat(p) : p if p isa Symbol p != :_ && push!(assigns.args, Expr(:(=), p, st_rhs_expr)) return assigns elseif p isa Expr - p_args = filter(e->!(e isa LineNumberNode), p.args)[2:end] + p_args = 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 + n_after_dots = 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 + :($st_rhs_expr[$dots_i:end-$n_after_dots]); assigns) + for i in n_after_dots-1:-1:0 _stm_assigns(p_args[end-i], :($st_rhs_expr[end-$i]); assigns) end end @@ -955,7 +958,7 @@ function _stm_check_usage(pats::Expr) dup = p in syms && p !== :_ push!(syms, p) @assert(!dup, "invalid duplicate non-underscore identifier $p") - return true + return nothing elseif Meta.isexpr(p, :vect) @assert(length(p.args) === 1, "use spaces, not commas, in @stm []-patterns") @@ -970,30 +973,32 @@ function _stm_check_usage(pats::Expr) @assert(count(x->Meta.isexpr(x, :(...)), p.args[2:end]) <= 1, "Multiple `...` in a pattern is ambiguous") - # This exact syntax is not necessary since the kind can't be provided by - # a variable, but requiring [K"kinds"] is consistent and allows us to - # implement list matching later. + # This exact `K"kind"` syntax is not necessary since the kind can't be + # provided by a variable, but requiring [K"kinds"] is consistent with + # `@ast` and allows us to implement list matching later. @assert(Meta.isexpr(p.args[1], :macrocall, 3) && p.args[1].args[1] === Symbol("@K_str") && p.args[1].args[3] isa String, "first pattern elt must be K\"\"") - return all(x->_stm_check_pattern(x; syms), p.args[2:end]) + for subp in p.args[2:end] + _stm_check_pattern(subp; syms) + end 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] + for pcr in filter(e->!isa(e, LineNumberNode), pats.args) + @assert(Meta.isexpr(pcr, :(->), 2), "Expected pat -> res, got malformed case: $pcr") + if Meta.isexpr(pcr.args[1], :tuple) + @assert(length(pcr.args[1].args) === 2, + "Expected `pat` or `(pat, when=cond)`, got $(pcr.args[1])") + p = pcr.args[1].args[1] + c = pcr.args[1].args[2] + @assert(Meta.isexpr(c, :(=), 2) && c.args[1] === :when, + "Expected `(when=cond)` in tuple pattern, got $(c)") else - p = per.args[1] + p = pcr.args[1] end - @assert _stm_check_pattern(p) "Malformed pattern: $p" + _stm_check_pattern(p) end end diff --git a/JuliaSyntax/test/syntax_graph.jl b/JuliaSyntax/test/syntax_graph.jl index 641dd50b3edfb..7d7a6776a70ab 100644 --- a/JuliaSyntax/test/syntax_graph.jl +++ b/JuliaSyntax/test/syntax_graph.jl @@ -272,15 +272,11 @@ end @testset "`when` clauses affect matching" begin @test @stm st begin (_, when=false) -> false - (_, when=false, when=false) -> false - (_, when=false, when=true) -> false - (_, when=true, when=true) -> true + (_, when=true) -> true end @test @stm st begin ([K"call" _...], when=false) -> false - ([K"call" _...], when=false, when=false) -> false - ([K"call" _...], when=false, when=true) -> false - ([K"call" _...], when=true, when=true) -> true + ([K"call" _...], when=true) -> true end @test @stm st begin ([K"call" _ _...], when=kind(st[1])===K"Identifier") -> true @@ -290,22 +286,28 @@ end end end - @testset "effects of when/`run` clauses" begin + @testset "effects of when=cond" begin let x = Int[] @test @stm st begin - (_, when=(push!(x, 1); true), run=push!(x, 2)) -> x == [1,2] + (_, when=(push!(x, 1); true)) -> x == [1] end + empty!(x) + @test @stm st begin - (_, when=(push!(x, 3); true), - when=(push!(x, 4); false), - when=(push!(x, 5); true)) -> false - _ -> x == [1,2,3,4] + (_, when=(push!(x, 1); false)) -> false + (_, when=(push!(x, 2); false)) -> false + (_, when=(push!(x, 3); true)) -> x == [1, 2, 3] end + empty!(x) + + @test @stm st begin + ([K"block"], when=(push!(x, 123); false)) -> false + (_, when=(push!(x, 1); true)) -> x == [1] + end + empty!(x) + @test @stm st begin - (x_pat, - when=((x_when = x_pat); true), - run=(x_run = x_pat)) -> - x_pat == x_when == x_run + (x_pat, when=((x_when = x_pat); true)) -> x_pat == x_when end end end From 33a79022010588b844466eb9a77bf5d5d4de8b01 Mon Sep 17 00:00:00 2001 From: Em Chu Date: Tue, 6 Jan 2026 10:42:43 -0800 Subject: [PATCH 5/7] Tweaks Co-authored-by: Claire Foster --- JuliaSyntax/src/porcelain/syntax_graph.jl | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/JuliaSyntax/src/porcelain/syntax_graph.jl b/JuliaSyntax/src/porcelain/syntax_graph.jl index d04f6de403b2e..d47ce59b77176 100644 --- a/JuliaSyntax/src/porcelain/syntax_graph.jl +++ b/JuliaSyntax/src/porcelain/syntax_graph.jl @@ -741,7 +741,7 @@ function _copy_ast(graph2::SyntaxGraph, graph1::SyntaxGraph, end #------------------------------------------------------------------------------- -# AST destruction utilities +# AST destructuring utilities raw""" Simple `SyntaxTree` pattern matching @@ -768,7 +768,7 @@ 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` +## Extra condition: `when` Like an escape hatch to the structure-matching mechanism. `when=cond` requires `cond` to evaluate to `true` for this branch to be taken. `cond` may also bind @@ -829,7 +829,7 @@ function _stm(line::LineNumberNode, st, pats; debug=false) :($k_gs = $kind($st_gs)), :($nc_gs = $numchildren($st_gs))), Expr(:if, false, nothing)) - needs_else = out_blk.args[2].args + case_list_tail = out_blk.args[2].args for pcr in pats.args pcr isa LineNumberNode && (line = pcr; continue) p, cond, result = _stm_destruct_pat(pcr) @@ -843,10 +843,10 @@ function _stm(line::LineNumberNode, st, pats; debug=false) Expr(:block, line, :($result_gs = $result), true)))), result_gs) - push!(needs_else, case) - needs_else = needs_else[3].args + push!(case_list_tail, case) + case_list_tail = case_list_tail[3].args end - push!(needs_else, + push!(case_list_tail, :(throw(ErrorException(string( "No match found for `", $st_gs, "` at ", $(string(line))))))) return esc(out_blk) @@ -985,7 +985,7 @@ function _stm_check_usage(pats::Expr) end end - @assert Meta.isexpr(pats, :block) "Usage: @st_match st begin; ...; end" + @assert Meta.isexpr(pats, :block) "Usage: @stm st begin; ...; end" for pcr in filter(e->!isa(e, LineNumberNode), pats.args) @assert(Meta.isexpr(pcr, :(->), 2), "Expected pat -> res, got malformed case: $pcr") if Meta.isexpr(pcr.args[1], :tuple) From 23e6283bffbdefa6b0f43243616532e3577debe1 Mon Sep 17 00:00:00 2001 From: Em Chu Date: Tue, 6 Jan 2026 12:47:59 -0800 Subject: [PATCH 6/7] Use `x::K"kind"` instead of `[K"kind"]` for leaf nodes Co-authored-by: Claire Foster --- JuliaSyntax/src/porcelain/syntax_graph.jl | 177 +++++++++++++--------- JuliaSyntax/test/syntax_graph.jl | 54 ++++--- 2 files changed, 141 insertions(+), 90 deletions(-) diff --git a/JuliaSyntax/src/porcelain/syntax_graph.jl b/JuliaSyntax/src/porcelain/syntax_graph.jl index d47ce59b77176..5387a6477d472 100644 --- a/JuliaSyntax/src/porcelain/syntax_graph.jl +++ b/JuliaSyntax/src/porcelain/syntax_graph.jl @@ -756,17 +756,24 @@ structure?) and a `let` (bind trees to these names if so). Each pattern uses a limited version of the @ast syntax: ``` - = + = + | ::K"" | [K"" *] - | [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. +where a `[K"k" p1 p2 ps...]` form matches any non-leaf 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`. + +Note that the syntax for matching leaves is different: `someleaf::K"kind"`, even +though the K"kind" determines whether it's a leaf anyway. This is for +consistency with `@ast`. + +Identifiers (except `_`) can't be re-used, but reuse may check for some form of +tree equivalence in a future implementation. ## Extra condition: `when` @@ -790,7 +797,7 @@ julia> st = JuliaSyntax.parsestmt( JuliaSyntax.SyntaxTree, "function foo(x,y,z); x; end") julia> JuliaSyntax.@stm st begin - [K"function" [K"call" fname [K"parameters" kws...]] body] -> + [K"function" [K"call" fname::K"Identifier" [K"parameters" kws...]] body] -> "no positional args, only kwargs: $(kws)" [K"function" fname] -> "zero-method function $fname" @@ -887,36 +894,50 @@ function _stm_matches_wrapper(p::Expr, st_ex, debug) end function _stm_matches(p::Expr, st_gs::Symbol, k_gs::Symbol, nc_gs::Symbol, debug) - pat_k = Kind(p.args[1].args[3]) - out = Expr(:&&, :($pat_k === $k_gs)) - debug && push!(out.args, Expr(:block, :(printstyled( - string("[kind]: ", $k_gs, "\n"); color=:yellow)), true)) - - p_args = p.args[2:end] - dots_i = findfirst(x->Meta.isexpr(x, :(...)), p_args) - dots_start = something(dots_i, length(p_args) + 1) - n_after_dots = length(p_args) - dots_start # -1 if no dots - - push!(out.args, isnothing(dots_i) ? - :($nc_gs === $(length(p_args))) : - :($nc_gs >= $(length(p_args) - 1))) - debug && push!(out.args, Expr(:block, :(printstyled( - string("[numc]: ", $nc_gs, "\n"); color=:yellow)), true)) - - for i in 1:dots_start-1 - p_args[i] isa Symbol && continue - push!(out.args, - _stm_matches_wrapper(p_args[i], :($st_gs[$i]), debug)) - end - for i in n_after_dots-1:-1:0 - p_args[end-i] isa Symbol && continue - push!(out.args, - _stm_matches_wrapper(p_args[end-i], :($st_gs[end-$i]), debug)) + if Meta.isexpr(p, :(::)) + pat_k = Kind(p.args[2].args[3]) + if !debug + Expr(:&&, :($pat_k === $k_gs), :($nc_gs === 0)) + else + Expr(:&&, :($pat_k === $k_gs), + Expr(:block, :(printstyled( + string("[kind]: ", $k_gs, "\n"); color=:yellow)), true), + :($nc_gs === 0), + Expr(:block, :(printstyled( + string("[numc]: ", $nc_gs, "\n"); color=:green)), true)) + end + else + pat_k = Kind(p.args[1].args[3]) + out = Expr(:&&, :($pat_k === $k_gs)) + debug && push!(out.args, Expr(:block, :(printstyled( + string("[kind]: ", $k_gs, "\n"); color=:yellow)), true)) + + p_args = p.args[2:end] + dots_i = findfirst(x->Meta.isexpr(x, :(...)), p_args) + dots_start = something(dots_i, length(p_args) + 1) + n_after_dots = length(p_args) - dots_start # -1 if no dots + + push!(out.args, isnothing(dots_i) ? + :($nc_gs === $(length(p_args))) : + :($nc_gs >= $(length(p_args) - 1))) + debug && push!(out.args, Expr(:block, :(printstyled( + string("[numc]: ", $nc_gs, "\n"); color=:yellow)), true)) + + for i in 1:dots_start-1 + p_args[i] isa Symbol && continue + push!(out.args, + _stm_matches_wrapper(p_args[i], :($st_gs[$i]), debug)) + end + for i in n_after_dots-1:-1:0 + p_args[end-i] isa Symbol && continue + push!(out.args, + _stm_matches_wrapper(p_args[end-i], :($st_gs[end-$i]), debug)) + end + debug && push!(out.args, Expr(:block, :(printstyled( + string("matched: ", $st_gs, " with ", $(QuoteNode(p)), "\n"); + color=:green)), true)) + return out end - debug && push!(out.args, Expr(:block, :(printstyled( - string("matched: ", $st_gs, " with ", $(QuoteNode(p)), "\n"); - color=:green)), true)) - return out end # Assuming _stm_matches, construct an Expr that assigns syms to SyntaxTrees. @@ -925,6 +946,8 @@ function _stm_assigns(p, st_rhs_expr; assigns=Expr(:block)) if p isa Symbol p != :_ && push!(assigns.args, Expr(:(=), p, st_rhs_expr)) return assigns + elseif Meta.isexpr(p, :(::)) + return _stm_assigns(p.args[1], st_rhs_expr; assigns) elseif p isa Expr p_args = p.args[2:end] dots_i = findfirst(x->Meta.isexpr(x, :(...)), p_args) @@ -947,54 +970,64 @@ end # Check for correct pattern syntax. Not needed outside of development. function _stm_check_usage(pats::Expr) - function _stm_check_pattern(p; syms=Set{Symbol}()) + # This exact `K"kind"` syntax is not necessary since the kind can't be + # provided by a variable, but requiring [K"kinds"] is consistent with `@ast` + # and allows us to implement list matching later. + function _stm_check_k_str(p) + @assert(Meta.isexpr(p, :macrocall, 3) && + p.args[1] === Symbol("@K_str") && + p.args[3] isa String, "expected `K\"kind\"`, got $p") + end + function _stm_check_pattern_sym(p::Symbol, syms=Set{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) + @assert(!dup, "invalid duplicate identifier $p") + end + 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) - @assert(!dup, "invalid duplicate non-underscore identifier $p") - return nothing - elseif Meta.isexpr(p, :vect) - @assert(length(p.args) === 1, - "use spaces, not commas, in @stm []-patterns") - elseif Meta.isexpr(p, :hcat) - @assert(length(p.args) >= 2) - elseif Meta.isexpr(p, :vcat) - p = _stm_vcat_to_hcat(p) - @assert(length(p.args) >= 2) + @assert(p.args[1] isa Symbol, "expected symbol before `...` in $p") + _stm_check_pattern_sym(p.args[1], syms) + elseif p isa Symbol + _stm_check_pattern_sym(p, syms) + elseif Meta.isexpr(p, :(::)) + @assert(length(p.args) === 2, "expected two args to `::` in $p") + @assert(p.args[1] isa Symbol, "expected symbol before `::` in $p") + _stm_check_pattern_sym(p.args[1], syms) + _stm_check_k_str(p.args[2]) + elseif Meta.isexpr(p, (:vect, :hcat, :vcat)) + # may have subpatterns + if Meta.isexpr(p, :vect) + @assert(length(p.args) === 1, + "use spaces, not commas, in @stm []-patterns") + elseif Meta.isexpr(p, :hcat) + @assert(length(p.args) >= 2) + elseif Meta.isexpr(p, :vcat) + p = _stm_vcat_to_hcat(p) + @assert(length(p.args) >= 2) + end + @assert(count(x->Meta.isexpr(x, :(...)), p.args[2:end]) <= 1, + "multiple `...` in a pattern is ambiguous") + _stm_check_k_str(p.args[1]) + for subp in p.args[2:end] + _stm_check_pattern(subp, syms) + end else @assert(false, "malformed pattern $p") end - @assert(count(x->Meta.isexpr(x, :(...)), p.args[2:end]) <= 1, - "Multiple `...` in a pattern is ambiguous") - - # This exact `K"kind"` syntax is not necessary since the kind can't be - # provided by a variable, but requiring [K"kinds"] is consistent with - # `@ast` and allows us to implement list matching later. - @assert(Meta.isexpr(p.args[1], :macrocall, 3) && - p.args[1].args[1] === Symbol("@K_str") && - p.args[1].args[3] isa String, "first pattern elt must be K\"\"") - - for subp in p.args[2:end] - _stm_check_pattern(subp; syms) - end end - @assert Meta.isexpr(pats, :block) "Usage: @stm st begin; ...; end" + @assert Meta.isexpr(pats, :block) "usage: @stm st begin; ...; end" for pcr in filter(e->!isa(e, LineNumberNode), pats.args) - @assert(Meta.isexpr(pcr, :(->), 2), "Expected pat -> res, got malformed case: $pcr") + @assert(Meta.isexpr(pcr, :(->), 2), "expected pat -> res, got malformed case: $pcr") if Meta.isexpr(pcr.args[1], :tuple) @assert(length(pcr.args[1].args) === 2, - "Expected `pat` or `(pat, when=cond)`, got $(pcr.args[1])") + "expected `pat` or `(pat, when=cond)`, got $(pcr.args[1])") p = pcr.args[1].args[1] c = pcr.args[1].args[2] @assert(Meta.isexpr(c, :(=), 2) && c.args[1] === :when, - "Expected `(when=cond)` in tuple pattern, got $(c)") + "expected `(when=cond)` in tuple pattern, got $(c)") else p = pcr.args[1] end diff --git a/JuliaSyntax/test/syntax_graph.jl b/JuliaSyntax/test/syntax_graph.jl index 7d7a6776a70ab..f30560945b8c0 100644 --- a/JuliaSyntax/test/syntax_graph.jl +++ b/JuliaSyntax/test/syntax_graph.jl @@ -141,7 +141,7 @@ end @testset "errors" begin # no match @test_throws ErrorException @stm st begin - [K"Identifier"] -> false + _::K"Identifier" -> false end # assuming we run this checker by default @@ -168,6 +168,18 @@ end :(@stm st begin [K"None" a... b...] -> false end) + :(@stm st begin + ([K"None"]...) -> false + end) + :(@stm st begin + [K"None" [K"None"]...] -> false + end) + :(@stm st begin + [K"None" x::K"Identifier"...] -> false + end) + :(@stm st begin + [[K"None"]::K"Identifier"] -> false + end) ] for e in bad Base.remove_linenums!(e) @@ -180,24 +192,30 @@ end @testset "nested patterns" begin @test 1 === @stm st begin - [K"call" [K"Identifier"] [K"Identifier"] [K"kw" [K"Identifier"] k1] [K"call" [K"Identifier"] [K"kw" [K"Identifier"] k2]]] -> 1 - [K"call" [K"Identifier"] [K"Identifier"] [K"kw" [K"Identifier"] k1] [K"call" [K"Identifier"] [K"kw" _ k2]]] -> 2 - [K"call" [K"Identifier"] [K"Identifier"] [K"kw" _ k1] [K"call" _ _]] -> 3 - [K"call" [K"Identifier"] [K"Identifier"] _ _ ] -> 4 + [K"call" _::K"Identifier" _::K"Identifier" [K"kw" _::K"Identifier" k1] [K"call" _::K"Identifier" [K"kw" _::K"Identifier" k2]]] -> 1 + [K"call" _::K"Identifier" _::K"Identifier" [K"kw" _::K"Identifier" k1] [K"call" _::K"Identifier" [K"kw" _ k2]]] -> 2 + [K"call" _::K"Identifier" _::K"Identifier" [K"kw" _ k1] [K"call" _ _]] -> 3 + [K"call" _::K"Identifier" _::K"Identifier" _ _ ] -> 4 [K"call" _ _ _ _] -> 5 end @test 1 === @stm st begin - [K"call" _ _ [K"None" [K"Identifier"] k1] [K"None" [K"Identifier"] [K"None" [K"None"] k2]]] -> 5 - [K"call" _ _ [K"kw" [K"Identifier"] k1] [K"None" [K"Identifier"] [K"None" [K"None"] k2]]] -> 4 - [K"call" _ _ [K"kw" [K"Identifier"] k1] [K"call" [K"Identifier"] [K"None" [K"None"] k2]]] -> 3 - [K"call" _ _ [K"kw" [K"Identifier"] k1] [K"call" [K"Identifier"] [K"kw" [K"None"] k2]]] -> 2 - [K"call" _ _ [K"kw" [K"Identifier"] k1] [K"call" [K"Identifier"] [K"kw" [K"Identifier"] k2]]] -> 1 + [K"call" _ _ [K"None" _::K"Identifier" k1] [K"None" _::K"Identifier" [K"None" [K"None"] k2]]] -> 5 + [K"call" _ _ [K"kw" _::K"Identifier" k1] [K"None" _::K"Identifier" [K"None" [K"None"] k2]]] -> 4 + [K"call" _ _ [K"kw" _::K"Identifier" k1] [K"call" _::K"Identifier" [K"None" [K"None"] k2]]] -> 3 + [K"call" _ _ [K"kw" _::K"Identifier" k1] [K"call" _::K"Identifier" [K"kw" [K"None"] k2]]] -> 2 + [K"call" _ _ [K"kw" _::K"Identifier" k1] [K"call" _::K"Identifier" [K"kw" _::K"Identifier" k2]]] -> 1 end @test 1 === @stm st begin - [K"call" _ _ [K"kw" [K"Identifier"] k1] [K"call" [K"Identifier"] [K"kw" [K"Identifier"] k2] bad]] -> 4 - [K"call" _ _ [K"kw" [K"Identifier"] k1] [K"call" [K"Identifier"] [K"kw" [K"Identifier"] k2 bad]]] -> 3 - [K"call" _ _ [K"kw" [K"Identifier"] k1] [K"call" [K"Identifier"] [K"kw" [K"Identifier" bad] k2]]] -> 2 - [K"call" _ _ [K"kw" [K"Identifier"] k1] [K"call" [K"Identifier"] [K"kw" [K"Identifier"] k2]]] -> 1 + [K"call" _ _ [K"kw" _::K"Identifier" k1] [K"call" _::K"Identifier" [K"kw" _::K"Identifier" k2] bad]] -> 4 + [K"call" _ _ [K"kw" _::K"Identifier" k1] [K"call" _::K"Identifier" [K"kw" _::K"Identifier" k2 bad]]] -> 3 + [K"call" _ _ [K"kw" _::K"Identifier" k1] [K"call" _::K"Identifier" [K"kw" [K"Identifier" bad] k2]]] -> 2 + [K"call" _ _ [K"kw" _::K"Identifier" k1] [K"call" _::K"Identifier" [K"kw" _::K"Identifier" k2]]] -> 1 + end + @test @stm st begin + [K"call" x::K"Identifier" _ _ _] -> x === st[1] + end + @test @stm st begin + [K"call" _ _ [K"kw" x::K"Identifier" _] _] -> x === st[3][1] end end @@ -221,12 +239,12 @@ end end @test @stm st begin [K"call" - [K"Identifier"] [K"Identifier"] - [K"kw" [K"Identifier"] k1] + _::K"Identifier" _::K"Identifier" + [K"kw" _::K"Identifier" k1] [K"call" - [K"Identifier"] + _::K"Identifier" [K"kw" - [K"Identifier"] + _::K"Identifier" k2]]] -> true end end From ea112baa2e5eae5eed8345658302f8781c0f8db1 Mon Sep 17 00:00:00 2001 From: Em Chu Date: Wed, 7 Jan 2026 08:58:18 -0800 Subject: [PATCH 7/7] Revert "Use `x::K"kind"` instead of `[K"kind"]` for leaf nodes" This reverts commit 23e6283bffbdefa6b0f43243616532e3577debe1. --- JuliaSyntax/src/porcelain/syntax_graph.jl | 177 +++++++++------------- JuliaSyntax/test/syntax_graph.jl | 54 +++---- 2 files changed, 90 insertions(+), 141 deletions(-) diff --git a/JuliaSyntax/src/porcelain/syntax_graph.jl b/JuliaSyntax/src/porcelain/syntax_graph.jl index 5387a6477d472..d47ce59b77176 100644 --- a/JuliaSyntax/src/porcelain/syntax_graph.jl +++ b/JuliaSyntax/src/porcelain/syntax_graph.jl @@ -756,24 +756,17 @@ structure?) and a `let` (bind trees to these names if so). Each pattern uses a limited version of the @ast syntax: ``` - = - | ::K"" + = | [K"" *] - | [K"" * ... *] + | [K"" * ... *] # note "*" is the meta-operator meaning one or more, and "..." is literal ``` -where a `[K"k" p1 p2 ps...]` form matches any non-leaf 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`. - -Note that the syntax for matching leaves is different: `someleaf::K"kind"`, even -though the K"kind" determines whether it's a leaf anyway. This is for -consistency with `@ast`. - -Identifiers (except `_`) can't be re-used, but reuse may check for some form of -tree equivalence in a future implementation. +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 condition: `when` @@ -797,7 +790,7 @@ julia> st = JuliaSyntax.parsestmt( JuliaSyntax.SyntaxTree, "function foo(x,y,z); x; end") julia> JuliaSyntax.@stm st begin - [K"function" [K"call" fname::K"Identifier" [K"parameters" kws...]] body] -> + [K"function" [K"call" fname [K"parameters" kws...]] body] -> "no positional args, only kwargs: $(kws)" [K"function" fname] -> "zero-method function $fname" @@ -894,50 +887,36 @@ function _stm_matches_wrapper(p::Expr, st_ex, debug) end function _stm_matches(p::Expr, st_gs::Symbol, k_gs::Symbol, nc_gs::Symbol, debug) - if Meta.isexpr(p, :(::)) - pat_k = Kind(p.args[2].args[3]) - if !debug - Expr(:&&, :($pat_k === $k_gs), :($nc_gs === 0)) - else - Expr(:&&, :($pat_k === $k_gs), - Expr(:block, :(printstyled( - string("[kind]: ", $k_gs, "\n"); color=:yellow)), true), - :($nc_gs === 0), - Expr(:block, :(printstyled( - string("[numc]: ", $nc_gs, "\n"); color=:green)), true)) - end - else - pat_k = Kind(p.args[1].args[3]) - out = Expr(:&&, :($pat_k === $k_gs)) - debug && push!(out.args, Expr(:block, :(printstyled( - string("[kind]: ", $k_gs, "\n"); color=:yellow)), true)) - - p_args = p.args[2:end] - dots_i = findfirst(x->Meta.isexpr(x, :(...)), p_args) - dots_start = something(dots_i, length(p_args) + 1) - n_after_dots = length(p_args) - dots_start # -1 if no dots - - push!(out.args, isnothing(dots_i) ? - :($nc_gs === $(length(p_args))) : - :($nc_gs >= $(length(p_args) - 1))) - debug && push!(out.args, Expr(:block, :(printstyled( - string("[numc]: ", $nc_gs, "\n"); color=:yellow)), true)) - - for i in 1:dots_start-1 - p_args[i] isa Symbol && continue - push!(out.args, - _stm_matches_wrapper(p_args[i], :($st_gs[$i]), debug)) - end - for i in n_after_dots-1:-1:0 - p_args[end-i] isa Symbol && continue - push!(out.args, - _stm_matches_wrapper(p_args[end-i], :($st_gs[end-$i]), debug)) - end - debug && push!(out.args, Expr(:block, :(printstyled( - string("matched: ", $st_gs, " with ", $(QuoteNode(p)), "\n"); - color=:green)), true)) - return out + pat_k = Kind(p.args[1].args[3]) + out = Expr(:&&, :($pat_k === $k_gs)) + debug && push!(out.args, Expr(:block, :(printstyled( + string("[kind]: ", $k_gs, "\n"); color=:yellow)), true)) + + p_args = p.args[2:end] + dots_i = findfirst(x->Meta.isexpr(x, :(...)), p_args) + dots_start = something(dots_i, length(p_args) + 1) + n_after_dots = length(p_args) - dots_start # -1 if no dots + + push!(out.args, isnothing(dots_i) ? + :($nc_gs === $(length(p_args))) : + :($nc_gs >= $(length(p_args) - 1))) + debug && push!(out.args, Expr(:block, :(printstyled( + string("[numc]: ", $nc_gs, "\n"); color=:yellow)), true)) + + for i in 1:dots_start-1 + p_args[i] isa Symbol && continue + push!(out.args, + _stm_matches_wrapper(p_args[i], :($st_gs[$i]), debug)) end + for i in n_after_dots-1:-1:0 + p_args[end-i] isa Symbol && continue + push!(out.args, + _stm_matches_wrapper(p_args[end-i], :($st_gs[end-$i]), debug)) + end + debug && push!(out.args, Expr(:block, :(printstyled( + string("matched: ", $st_gs, " with ", $(QuoteNode(p)), "\n"); + color=:green)), true)) + return out end # Assuming _stm_matches, construct an Expr that assigns syms to SyntaxTrees. @@ -946,8 +925,6 @@ function _stm_assigns(p, st_rhs_expr; assigns=Expr(:block)) if p isa Symbol p != :_ && push!(assigns.args, Expr(:(=), p, st_rhs_expr)) return assigns - elseif Meta.isexpr(p, :(::)) - return _stm_assigns(p.args[1], st_rhs_expr; assigns) elseif p isa Expr p_args = p.args[2:end] dots_i = findfirst(x->Meta.isexpr(x, :(...)), p_args) @@ -970,64 +947,54 @@ end # Check for correct pattern syntax. Not needed outside of development. function _stm_check_usage(pats::Expr) - # This exact `K"kind"` syntax is not necessary since the kind can't be - # provided by a variable, but requiring [K"kinds"] is consistent with `@ast` - # and allows us to implement list matching later. - function _stm_check_k_str(p) - @assert(Meta.isexpr(p, :macrocall, 3) && - p.args[1] === Symbol("@K_str") && - p.args[3] isa String, "expected `K\"kind\"`, got $p") - end - function _stm_check_pattern_sym(p::Symbol, syms=Set{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) - @assert(!dup, "invalid duplicate identifier $p") - end - function _stm_check_pattern(p, syms=Set{Symbol}()) + function _stm_check_pattern(p; syms=Set{Symbol}()) if Meta.isexpr(p, :(...), 1) - @assert(p.args[1] isa Symbol, "expected symbol before `...` in $p") - _stm_check_pattern_sym(p.args[1], syms) - elseif p isa Symbol - _stm_check_pattern_sym(p, syms) - elseif Meta.isexpr(p, :(::)) - @assert(length(p.args) === 2, "expected two args to `::` in $p") - @assert(p.args[1] isa Symbol, "expected symbol before `::` in $p") - _stm_check_pattern_sym(p.args[1], syms) - _stm_check_k_str(p.args[2]) - elseif Meta.isexpr(p, (:vect, :hcat, :vcat)) - # may have subpatterns - if Meta.isexpr(p, :vect) - @assert(length(p.args) === 1, - "use spaces, not commas, in @stm []-patterns") - elseif Meta.isexpr(p, :hcat) - @assert(length(p.args) >= 2) - elseif Meta.isexpr(p, :vcat) - p = _stm_vcat_to_hcat(p) - @assert(length(p.args) >= 2) - end - @assert(count(x->Meta.isexpr(x, :(...)), p.args[2:end]) <= 1, - "multiple `...` in a pattern is ambiguous") - _stm_check_k_str(p.args[1]) - for subp in p.args[2:end] - _stm_check_pattern(subp, syms) - end + 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) + @assert(!dup, "invalid duplicate non-underscore identifier $p") + return nothing + elseif Meta.isexpr(p, :vect) + @assert(length(p.args) === 1, + "use spaces, not commas, in @stm []-patterns") + elseif Meta.isexpr(p, :hcat) + @assert(length(p.args) >= 2) + elseif Meta.isexpr(p, :vcat) + p = _stm_vcat_to_hcat(p) + @assert(length(p.args) >= 2) else @assert(false, "malformed pattern $p") end + @assert(count(x->Meta.isexpr(x, :(...)), p.args[2:end]) <= 1, + "Multiple `...` in a pattern is ambiguous") + + # This exact `K"kind"` syntax is not necessary since the kind can't be + # provided by a variable, but requiring [K"kinds"] is consistent with + # `@ast` and allows us to implement list matching later. + @assert(Meta.isexpr(p.args[1], :macrocall, 3) && + p.args[1].args[1] === Symbol("@K_str") && + p.args[1].args[3] isa String, "first pattern elt must be K\"\"") + + for subp in p.args[2:end] + _stm_check_pattern(subp; syms) + end end - @assert Meta.isexpr(pats, :block) "usage: @stm st begin; ...; end" + @assert Meta.isexpr(pats, :block) "Usage: @stm st begin; ...; end" for pcr in filter(e->!isa(e, LineNumberNode), pats.args) - @assert(Meta.isexpr(pcr, :(->), 2), "expected pat -> res, got malformed case: $pcr") + @assert(Meta.isexpr(pcr, :(->), 2), "Expected pat -> res, got malformed case: $pcr") if Meta.isexpr(pcr.args[1], :tuple) @assert(length(pcr.args[1].args) === 2, - "expected `pat` or `(pat, when=cond)`, got $(pcr.args[1])") + "Expected `pat` or `(pat, when=cond)`, got $(pcr.args[1])") p = pcr.args[1].args[1] c = pcr.args[1].args[2] @assert(Meta.isexpr(c, :(=), 2) && c.args[1] === :when, - "expected `(when=cond)` in tuple pattern, got $(c)") + "Expected `(when=cond)` in tuple pattern, got $(c)") else p = pcr.args[1] end diff --git a/JuliaSyntax/test/syntax_graph.jl b/JuliaSyntax/test/syntax_graph.jl index f30560945b8c0..7d7a6776a70ab 100644 --- a/JuliaSyntax/test/syntax_graph.jl +++ b/JuliaSyntax/test/syntax_graph.jl @@ -141,7 +141,7 @@ end @testset "errors" begin # no match @test_throws ErrorException @stm st begin - _::K"Identifier" -> false + [K"Identifier"] -> false end # assuming we run this checker by default @@ -168,18 +168,6 @@ end :(@stm st begin [K"None" a... b...] -> false end) - :(@stm st begin - ([K"None"]...) -> false - end) - :(@stm st begin - [K"None" [K"None"]...] -> false - end) - :(@stm st begin - [K"None" x::K"Identifier"...] -> false - end) - :(@stm st begin - [[K"None"]::K"Identifier"] -> false - end) ] for e in bad Base.remove_linenums!(e) @@ -192,30 +180,24 @@ end @testset "nested patterns" begin @test 1 === @stm st begin - [K"call" _::K"Identifier" _::K"Identifier" [K"kw" _::K"Identifier" k1] [K"call" _::K"Identifier" [K"kw" _::K"Identifier" k2]]] -> 1 - [K"call" _::K"Identifier" _::K"Identifier" [K"kw" _::K"Identifier" k1] [K"call" _::K"Identifier" [K"kw" _ k2]]] -> 2 - [K"call" _::K"Identifier" _::K"Identifier" [K"kw" _ k1] [K"call" _ _]] -> 3 - [K"call" _::K"Identifier" _::K"Identifier" _ _ ] -> 4 + [K"call" [K"Identifier"] [K"Identifier"] [K"kw" [K"Identifier"] k1] [K"call" [K"Identifier"] [K"kw" [K"Identifier"] k2]]] -> 1 + [K"call" [K"Identifier"] [K"Identifier"] [K"kw" [K"Identifier"] k1] [K"call" [K"Identifier"] [K"kw" _ k2]]] -> 2 + [K"call" [K"Identifier"] [K"Identifier"] [K"kw" _ k1] [K"call" _ _]] -> 3 + [K"call" [K"Identifier"] [K"Identifier"] _ _ ] -> 4 [K"call" _ _ _ _] -> 5 end @test 1 === @stm st begin - [K"call" _ _ [K"None" _::K"Identifier" k1] [K"None" _::K"Identifier" [K"None" [K"None"] k2]]] -> 5 - [K"call" _ _ [K"kw" _::K"Identifier" k1] [K"None" _::K"Identifier" [K"None" [K"None"] k2]]] -> 4 - [K"call" _ _ [K"kw" _::K"Identifier" k1] [K"call" _::K"Identifier" [K"None" [K"None"] k2]]] -> 3 - [K"call" _ _ [K"kw" _::K"Identifier" k1] [K"call" _::K"Identifier" [K"kw" [K"None"] k2]]] -> 2 - [K"call" _ _ [K"kw" _::K"Identifier" k1] [K"call" _::K"Identifier" [K"kw" _::K"Identifier" k2]]] -> 1 + [K"call" _ _ [K"None" [K"Identifier"] k1] [K"None" [K"Identifier"] [K"None" [K"None"] k2]]] -> 5 + [K"call" _ _ [K"kw" [K"Identifier"] k1] [K"None" [K"Identifier"] [K"None" [K"None"] k2]]] -> 4 + [K"call" _ _ [K"kw" [K"Identifier"] k1] [K"call" [K"Identifier"] [K"None" [K"None"] k2]]] -> 3 + [K"call" _ _ [K"kw" [K"Identifier"] k1] [K"call" [K"Identifier"] [K"kw" [K"None"] k2]]] -> 2 + [K"call" _ _ [K"kw" [K"Identifier"] k1] [K"call" [K"Identifier"] [K"kw" [K"Identifier"] k2]]] -> 1 end @test 1 === @stm st begin - [K"call" _ _ [K"kw" _::K"Identifier" k1] [K"call" _::K"Identifier" [K"kw" _::K"Identifier" k2] bad]] -> 4 - [K"call" _ _ [K"kw" _::K"Identifier" k1] [K"call" _::K"Identifier" [K"kw" _::K"Identifier" k2 bad]]] -> 3 - [K"call" _ _ [K"kw" _::K"Identifier" k1] [K"call" _::K"Identifier" [K"kw" [K"Identifier" bad] k2]]] -> 2 - [K"call" _ _ [K"kw" _::K"Identifier" k1] [K"call" _::K"Identifier" [K"kw" _::K"Identifier" k2]]] -> 1 - end - @test @stm st begin - [K"call" x::K"Identifier" _ _ _] -> x === st[1] - end - @test @stm st begin - [K"call" _ _ [K"kw" x::K"Identifier" _] _] -> x === st[3][1] + [K"call" _ _ [K"kw" [K"Identifier"] k1] [K"call" [K"Identifier"] [K"kw" [K"Identifier"] k2] bad]] -> 4 + [K"call" _ _ [K"kw" [K"Identifier"] k1] [K"call" [K"Identifier"] [K"kw" [K"Identifier"] k2 bad]]] -> 3 + [K"call" _ _ [K"kw" [K"Identifier"] k1] [K"call" [K"Identifier"] [K"kw" [K"Identifier" bad] k2]]] -> 2 + [K"call" _ _ [K"kw" [K"Identifier"] k1] [K"call" [K"Identifier"] [K"kw" [K"Identifier"] k2]]] -> 1 end end @@ -239,12 +221,12 @@ end end @test @stm st begin [K"call" - _::K"Identifier" _::K"Identifier" - [K"kw" _::K"Identifier" k1] + [K"Identifier"] [K"Identifier"] + [K"kw" [K"Identifier"] k1] [K"call" - _::K"Identifier" + [K"Identifier"] [K"kw" - _::K"Identifier" + [K"Identifier"] k2]]] -> true end end