@@ -818,3 +818,228 @@ end
818818# out
819819# end
820820
821+ # TODO : forgot to support vcat (i.e. newlines in patterns currently require a
822+ # double-semicolon continuation)
823+
824+ # TODO : SyntaxList pattern matching could take similar syntax and use most of
825+ # the same machinery
826+
827+ raw """
828+ Simple SyntaxTree pattern matching
829+
830+ ```
831+ @stm syntax_tree begin
832+ pattern1 -> result1
833+ (pattern2, when=cond2) -> result2
834+ (pattern3, when=cond3, run=run3, when=cond4) -> result3
835+ ...
836+ end
837+ ```
838+
839+ Returns the first result where its corresponding pattern matches `syntax_tree`
840+ and each extra `cond` is true. Throws an error if no match is found.
841+
842+ This macro (especially `when` and `run`) takes heavy inspiration from [Racket's
843+ pattern matching](https://docs.racket-lang.org/reference/match.html), but with a
844+ significantly less featureful pattern language.
845+
846+ ## Patterns
847+
848+ A pattern is used as both a conditional (does this syntax tree have a certain
849+ structure?) and a `let` (bind trees to these names if so). Each pattern uses a
850+ limited version of the @ast syntax:
851+
852+ ```
853+ <pattern> = <tree_identifier>
854+ | [K"<kind>" <pattern>*]
855+ | [K"<kind>" <pattern>* <list_identifier>... <pattern>*]
856+
857+ # note "*" is the meta-operator meaning one or more, and "..." is literal
858+ ```
859+
860+ where a `[K"k" p1 p2 ps...]` form matches any tree with kind `k` and >=2
861+ children (bound to `p1` and `p2`), and `ps` is bound to the possibly-empty
862+ SyntaxList of children `3:end`. Identifiers (except `_`) can't be re-used, but
863+ may check for some form of tree equivalence in a future implementation.
864+
865+ ## Extra conditions: `when`, `run`
866+
867+ Like an escape hatch to the structure-matching mechanism. `when=cond` requires
868+ `cond`'s value be `true` for this pattern to match. `run=code` evaluates
869+ `code`, usually to bind variables. For convenience, the value of `code` is
870+ bound to the local `run`, which can be opted out of by quoting `run`.
871+
872+ `when` and `run` clauses may appear multiple times in any order after the
873+ pattern. They are executed left-to-right, stopping if any `when=cond` evaluates
874+ to false. These may not mutate the object being matched.
875+
876+ ## Scope of variables
877+
878+ Every `(pattern, extras...) -> result` introduces a local scope. Identifiers in
879+ the pattern are let-bound when evaluating `extras` and `result`. Any `extra` can
880+ introduce variables for use in later `extras` and `result`. User code in
881+ `extras` and `result` can refer to outer variables.
882+
883+ ## Example
884+
885+ ```
886+ julia> st = JuliaSyntax.parsestmt(JuliaLowering.SyntaxTree, "function foo(x,y,z); x; end")
887+ julia> JuliaLowering.@stm st begin
888+ [K"function" [K"call" fname args... [K"parameters" kws...]] body] ->
889+ "has kwargs: $(kws)"
890+ [K"function" fname] ->
891+ "zero-method function $fname"
892+ [K"function" [K"call" fname args...] body] ->
893+ "normal function $fname"
894+ ([K"=" [K"call" _...] _...], run=if_valid_get_args(st[1]), when=!isnothing(run)) ->
895+ "deprecated call-equals form with args $run"
896+ (_, run=show("printf debugging is great")) -> "something else"
897+ _ -> "something else"
898+ end
899+ "normal function foo"
900+ ```
901+ """
902+ macro stm (st, pats)
903+ _stm (__source__, st, pats; debug= false )
904+ end
905+
906+ " Like `@stm`, but prints a trace during matching."
907+ macro stm_debug (st, pats)
908+ _stm (__source__, st, pats; debug= true )
909+ end
910+
911+ function _stm (line:: LineNumberNode , st, pats; debug= false )
912+ _stm_check_usage (pats)
913+ # We leave most code untouched, so the user probably wants esc(output)
914+ st_gs, result_gs = gensym (" st" ), gensym (" result" )
915+ out_blk = Expr (:let ,
916+ Expr (:block , :($ st_gs = $ st:: SyntaxTree ), :($ result_gs = nothing )),
917+ Expr (:if , false , nothing ))
918+ needs_else = out_blk. args[2 ]. args
919+ for per in pats. args
920+ per isa LineNumberNode && (line = per; continue )
921+ p, extras, result = _stm_destruct_pat (Base. remove_linenums! (per))
922+ # We need to let-bind patvars in both extras and the result, so result
923+ # needs to live in the first argument of :if with the extra conditions.
924+ e_check = Expr (:&& )
925+ for (ek, ev) in extras
926+ push! (e_check. args, ek === :when ? ev :
927+ Expr (:block , ek === :run ? :(local run = $ ev) : ev, true ))
928+ end
929+ # final arg to e_check: successful match
930+ push! (e_check. args, Expr (:block , line, :($ result_gs = $ result), true ))
931+ case = Expr (:elseif ,
932+ Expr (:&& , :(JuliaLowering. _stm_matches ($ (Expr (:quote , p)), $ st_gs, $ debug)),
933+ Expr (:let , _stm_assigns (p, st_gs), e_check)),
934+ result_gs)
935+ push! (needs_else, case)
936+ needs_else = needs_else[3 ]. args
937+ end
938+ push! (needs_else, :(throw (" No match found for $($ st_gs) at $($ (string (line))) " )))
939+ return esc (out_blk)
940+ end
941+
942+ function _stm_destruct_pat (per)
943+ pe, r = per. args[1 : 2 ]
944+ return ! Meta. isexpr (pe, :tuple ) ? (pe, Tuple[], r) :
945+ let extras = pe. args[2 : end ]
946+ (pe. args[1 ], Tuple[(e. args[1 ], e. args[2 ]) for e in extras], r)
947+ end
948+ end
949+
950+ function _stm_matches (p:: Union{Symbol, Expr} , st, debug= false , indent= " " )
951+ if p isa Symbol
952+ debug && printstyled (indent, " $p = $st \n " ; color= :yellow )
953+ return true
954+ elseif Meta. isexpr (p, (:vect , :hcat ))
955+ p_kind = Kind (p. args[1 ]. args[3 ])
956+ kind_ok = p_kind === kind (st)
957+ if ! kind_ok
958+ debug && printstyled (indent, " [kind]: $(kind (st)) !=$p_kind \n " ; color= :red )
959+ return false
960+ end
961+ p_args = filter (e-> ! (e isa LineNumberNode), p. args)[2 : end ]
962+ dots_i = findfirst (x-> Meta. isexpr (x, :(... )), p_args)
963+ dots_start = something (dots_i, length (p_args) + 1 )
964+ n_after = length (p_args) - dots_start
965+ npats = dots_start + n_after
966+ n_ok = (isnothing (dots_i) ? numchildren (st) === npats : numchildren (st) >= npats - 1 )
967+ if ! n_ok
968+ debug && printstyled (indent, " [numc]: $(numchildren (st)) !=$npats \n " ; color= :red )
969+ return false
970+ end
971+ all_ok = all (i-> _stm_matches (p_args[i], st[i], debug, indent* " " ), 1 : dots_start- 1 ) &&
972+ all (i-> _stm_matches (p_args[end - i], st[end - i], debug, indent* " " ), n_after- 1 : - 1 : 0 )
973+ debug && printstyled (indent, st, all_ok ? " matched\n " : " not matched\n " ;
974+ color= (all_ok ? :green : :red ))
975+ return all_ok
976+ end
977+ @assert false
978+ end
979+
980+ # Assuming _stm_matches, construct an Expr that assigns syms to SyntaxTrees.
981+ # Note st_rhs_expr is a ref-expr with a SyntaxTree/List value (in context).
982+ function _stm_assigns (p, st_rhs_expr; assigns= Expr (:block ))
983+ if p isa Symbol && p != :_
984+ push! (assigns. args, Expr (:(= ), p, st_rhs_expr))
985+ elseif p isa Expr
986+ p_args = filter (e-> ! (e isa LineNumberNode), p. args)[2 : end ]
987+ dots_i = findfirst (x-> Meta. isexpr (x, :(... )), p_args)
988+ dots_start = something (dots_i, length (p_args) + 1 )
989+ n_after = length (p_args) - dots_start
990+ for i in 1 : dots_start- 1
991+ _stm_assigns (p_args[i], :($ st_rhs_expr[$ i]); assigns)
992+ end
993+ if ! isnothing (dots_i)
994+ _stm_assigns (p_args[dots_i]. args[1 ],
995+ :($ st_rhs_expr[$ dots_i: end - $ n_after]); assigns)
996+ for i in n_after- 1 : - 1 : 0
997+ _stm_assigns (p_args[end - i], :($ st_rhs_expr[end - $ i]); assigns)
998+ end
999+ end
1000+ end
1001+ return assigns
1002+ @assert false
1003+ end
1004+
1005+ # Check for correct pattern syntax. Not needed outside of development.
1006+ function _stm_check_usage (pats)
1007+ function _stm_check_pattern (p; syms= Set {Symbol} ())
1008+ if Meta. isexpr (p, :(... ), 1 )
1009+ p = p. args[1 ]
1010+ @assert (p isa Symbol, " Expected symbol before `...` in $p " )
1011+ end
1012+ if p isa Symbol
1013+ # No support for duplicate syms for now (user is either looking for
1014+ # some form of equality we don't implement, or they made a mistake)
1015+ dup = p in syms && p != = :_
1016+ push! (syms, p)
1017+ return ! dup || @assert (false , " invalid duplicate non-underscore identifier $p " )
1018+ end
1019+ return (Meta. isexpr (p, :vect , 1 ) ||
1020+ (Meta. isexpr (p, :hcat ) &&
1021+ isnothing (@assert (count (x-> Meta. isexpr (x, :(... )), p. args[2 : end ]) <= 1 ,
1022+ " Multiple `...` in a pattern is ambiguous" )) &&
1023+ all (x-> _stm_check_pattern (x; syms), p. args[2 : end ])) &&
1024+ # This exact syntax is not necessary since the kind can't be
1025+ # provided by a variable, but requiring [K"kinds"] is consistent.
1026+ Meta. isexpr (p. args[1 ], :macrocall , 3 ) &&
1027+ p. args[1 ]. args[1 ] === Symbol (" @K_str" ) && p. args[1 ]. args[3 ] isa String)
1028+ end
1029+
1030+ @assert Meta. isexpr (pats, :block ) " Usage: @st_match st begin; ...; end"
1031+ for per in filter (e-> ! isa (e, LineNumberNode), pats. args)
1032+ @assert (Meta. isexpr (per, :(-> ), 2 ), " Expected pat -> res, got malformed pair: $per " )
1033+ if Meta. isexpr (per. args[1 ], :tuple )
1034+ @assert length (per. args[1 ]. args) >= 2 " Unnecessary tuple in $(per. args[1 ]) "
1035+ for e in per. args[1 ]. args[2 : end ]
1036+ @assert (Meta. isexpr (e, :(= ), 2 ) && e. args[1 ] in (:when , :run , QuoteNode (:run )),
1037+ " Expected `when=<cond>` or `run=<stmts>`, got $e " )
1038+ end
1039+ p = per. args[1 ]. args[1 ]
1040+ else
1041+ p = per. args[1 ]
1042+ end
1043+ @assert _stm_check_pattern (p) " Malformed pattern: $p "
1044+ end
1045+ end
0 commit comments