Skip to content

Commit 16948c1

Browse files
committed
Attempt to define the AST
1 parent f8e84e1 commit 16948c1

17 files changed

+1195
-56
lines changed

src/JuliaLowering.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ _include("syntax_macros.jl")
3737
_include("eval.jl")
3838
_include("compat.jl")
3939
_include("hooks.jl")
40+
_include("validation.jl")
4041

4142
function __init__()
4243
_register_kinds()

src/desugaring.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4557,6 +4557,7 @@ end
45574557

45584558
@fzone "JL: desugar" function expand_forms_2(ctx::MacroExpansionContext, ex::SyntaxTree)
45594559
ctx1 = DesugaringContext(ctx, ctx.expr_compat_mode)
4560+
valid_st1_or_throw(ex)
45604561
ex1 = expand_forms_2(ctx1, reparent(ctx1, ex))
45614562
ctx1, ex1
45624563
end

src/syntax_graph.jl

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)