diff --git a/src/expr.jl b/src/expr.jl index 61ec2e75..3fc745c0 100644 --- a/src/expr.jl +++ b/src/expr.jl @@ -30,6 +30,31 @@ function reorder_parameters!(args, params_pos) insert!(args, params_pos, pop!(args)) end +function lower_underscores!(anon_args, args, argrange=1:length(args)) + for i in argrange + a = args[i] + if a == :_ + if isempty(anon_args) + g = gensym() + push!(anon_args, g) + else + g = anon_args[1] + end + args[i] = g + elseif a isa Expr + if Meta.isexpr(a, :call) && length(a.args) > 2 && + Meta.isexpr(a.args[2], :parameters) + lower_underscores!(anon_args, a.args, 1:1) + lower_underscores!(anon_args, a.args, 3:length(a.args)) + lower_underscores!(anon_args, a.args, 2:2) + else + # FIXME: Other out-of-source-order Exprs + lower_underscores!(anon_args, a.args) + end + end + end +end + function _to_expr(node::SyntaxNode; iteration_spec=false, need_linenodes=true, eq_to_kw=false, map_kw_in_params=false) if !haschildren(node) @@ -131,7 +156,7 @@ function _to_expr(node::SyntaxNode; iteration_spec=false, need_linenodes=true, args[2] = _to_expr(node_args[2]) else eq_to_kw_in_call = - ((headsym == :call || headsym == :dotcall) && is_prefix_call(node)) || + ((headsym == :call || headsym == :dotcall || headsym == Symbol("/>")) && is_prefix_call(node)) || headsym == :ref eq_to_kw_all = headsym == :parameters && !map_kw_in_params in_vcbr = headsym == :vect || headsym == :curly || headsym == :braces || headsym == :ref @@ -250,11 +275,17 @@ function _to_expr(node::SyntaxNode; iteration_spec=false, need_linenodes=true, # Block for conditional's source location args[1] = Expr(:block, loc, args[1]) elseif headsym === :(->) - if Meta.isexpr(args[2], :block) - pushfirst!(args[2].args, loc) + if is_prefix_op_call(node) + anon_args = Symbol[] + lower_underscores!(anon_args, args) + pushfirst!(args, Expr(:tuple, anon_args...)) else - # Add block for source locations - args[2] = Expr(:block, loc, args[2]) + if Meta.isexpr(args[2], :block) + pushfirst!(args[2].args, loc) + else + # Add block for source locations + args[2] = Expr(:block, loc, args[2]) + end end elseif headsym === :function if length(args) > 1 @@ -299,10 +330,104 @@ function _to_expr(node::SyntaxNode; iteration_spec=false, need_linenodes=true, args[1] = Expr(headsym, args[1].args...) headsym = :const end + elseif headsym == Symbol("/>") || headsym == Symbol("/>>") + callex = only(args) + @assert Meta.isexpr(callex, :call) + args = callex.args + func = headsym == Symbol("/>") ? + :(JuliaSyntax.fixbutfirst) : + :(JuliaSyntax.fixbutlast) + + # Automatic underscore lowering within pipes + for i = 2:length(args) + anon_args = Symbol[] + if i == 2 && Meta.isexpr(args[i], :parameters) + kws = args[i].args + for j = 1:length(kws) + kw = kws[j] + if Meta.isexpr(kw, :kw) + as = Any[kw.args[2]] + lower_underscores!(anon_args, as) + if !isempty(anon_args) + kw.args[2] = Expr(:->, Expr(:tuple, anon_args...), as[1]) + end + end + end + else + as = Any[args[i]] + lower_underscores!(anon_args, as) + if !isempty(anon_args) + args[i] = Expr(:->, Expr(:tuple, anon_args...), as[1]) + end + end + end + + if length(args) >= 2 && Meta.isexpr(args[2], :parameters) + return Expr(:call, func, args[2], args[1], args[3:end]...) + else + return Expr(:call, func, args...) + end + elseif headsym == :chain + if kind(node_args[1]) in KSet"/> />>" + return Expr(:call, :(JuliaSyntax.compose_chain), args...) + else + return Expr(:call, :(JuliaSyntax.chain), args...) + end end return Expr(headsym, args...) end +#------------------------------------------------------------------------------- +# Targets for lowering /> and />> syntax + +# For use with /> +struct FixButFirst{F,Args,Kws} + f::F + args::Args + kwargs::Kws +end + +(f::FixButFirst)(x) = f.f(x, f.args...; f.kwargs...) + +""" +Fix all arguments except for the first +""" +fixbutfirst(f, args...; kws...) = FixButFirst(f, args, kws) + +# For use with />> +struct FixButLast{F,Args,Kws} + f::F + args::Args + kwargs::Kws +end + +(f::FixButLast)(x) = f.f(f.args..., x; f.kwargs...) + +""" +Fix all arguments except for the last +""" +fixbutlast(f, args...; kws...) = FixButLast(f, args, kws) + +chain(x, f, fs...) = chain(f(x), fs...) +chain(x) = x + +# An example of how chain() can be used to rewrite +# `x />> map(f) />> reduce(g)` into `mapreduce(f, g, x)` +function chain(x, f1::FixButLast{typeof(map)}, f2::FixButLast{typeof(reduce)}, fs...) + chain(x, fixbutlast(mapreduce, f1.args..., f2.args...; f1.kwargs..., f2.kwargs...), fs...) +end + +struct ComposeChain{Funcs} + fs::Funcs +end + +(f::ComposeChain)(x) = chain(x, f.fs...) + +compose_chain(fs...) = ComposeChain(fs) + + +#------------------------------------------------------------------------------- + Base.Expr(node::SyntaxNode) = _to_expr(node) function build_tree(::Type{Expr}, stream::ParseStream; kws...) diff --git a/src/kinds.jl b/src/kinds.jl index 3b5fa1bd..de8ddb41 100644 --- a/src/kinds.jl +++ b/src/kinds.jl @@ -823,6 +823,8 @@ const _kind_names = "'" ".'" "->" + "/>" + "/>>" "BEGIN_UNICODE_OPS" "¬" @@ -878,6 +880,7 @@ const _kind_names = "block" "call" "dotcall" + "chain" "comparison" "curly" "inert" # QuoteNode; not quasiquote diff --git a/src/parser.jl b/src/parser.jl index 6318054a..284dcfa2 100644 --- a/src/parser.jl +++ b/src/parser.jl @@ -276,7 +276,7 @@ function is_syntactic_operator(k) end function is_syntactic_unary_op(k) - kind(k) in KSet"$ & ::" + kind(k) in KSet"$ & :: ->" end function is_type_operator(t) @@ -820,7 +820,38 @@ end # x .|> y ==> (dotcall-i x |> y) # flisp: parse-pipe> function parse_pipe_gt(ps::ParseState) - parse_LtoR(ps, parse_range, is_prec_pipe_gt) + parse_LtoR(ps, parse_curry_chain, is_prec_pipe_gt) +end + +function parse_curry_chain(ps::ParseState) + mark = position(ps) + nterms = 0 + if (k = peek(ps); k != K"/>" && k != K"/>>") + # x /> f(a) ==> (chain x (/> (call f a))) + parse_range(ps) + nterms += 1 + else + # /> f(a) ==> (/> (call f a)) + end + while (k = peek(ps); k == K"/>" || k == K"/>>") + m = position(ps) + bump(ps, TRIVIA_FLAG) + parse_range(ps) + nterms += 1 + if (kb = peek_behind(ps).kind; kb != K"call" && kb != K"$") + emit(ps, m, K"error", error="Expected call to the right of />") + end + emit(ps, m, k) + end + if nterms > 1 + # x /> f(a) /> g(b) ==> (chain x (/> (call f a)) (/> (call g b))) + # x /> A.f(a,b) ==> (chain x (/> (call (. A (quote f)) a b))) + # /> f(a) /> g(b) ==> (chain (/> (call f a)) (/> (call g b))) + # x /> f() />> g() ==> (chain x (/> (call f)) (/>> (call g))) + # x /> $call ==> (chain x (/> ($ call))) + # x /> notcall[] ==> (chain x (/> (error (ref notcall)))) + emit(ps, mark, K"chain") + end end # parse ranges and postfix ... @@ -1417,6 +1448,7 @@ end # &a ==> (& a) # ::a ==> (::-pre a) # $a ==> ($ a) +# ->a ==> (-> a) # # flisp: parse-unary-prefix function parse_unary_prefix(ps::ParseState) @@ -1434,14 +1466,16 @@ function parse_unary_prefix(ps::ParseState) if k in KSet"& ::" # &a ==> (& a) parse_where(ps, parse_call) + elseif k == K"->" + # -> binds loosely on the right + parse_eq_star(ps) else # $a ==> ($ a) # $$a ==> ($ ($ a)) # $&a ==> ($ (& a)) parse_unary_prefix(ps) end - # Only need PREFIX_OP_FLAG for :: - f = k == K"::" ? PREFIX_OP_FLAG : EMPTY_FLAGS + f = (k == K"::" || k == K"->") ? PREFIX_OP_FLAG : EMPTY_FLAGS emit(ps, mark, k, f) end else diff --git a/src/tokenize.jl b/src/tokenize.jl index 82cab123..764b4b25 100644 --- a/src/tokenize.jl +++ b/src/tokenize.jl @@ -932,6 +932,12 @@ function lex_forwardslash(l::Lexer) end elseif accept(l, '=') return emit(l, K"/=") + elseif accept(l, '>') + if accept(l, '>') + return emit(l, K"/>>") + else + return emit(l, K"/>") + end else return emit(l, K"/") end diff --git a/test/parser.jl b/test/parser.jl index f95f176e..b930bb57 100644 --- a/test/parser.jl +++ b/test/parser.jl @@ -131,6 +131,16 @@ tests = [ "x |> y |> z" => "(call-i (call-i x |> y) |> z)" "x .|> y" => "(dotcall-i x |> y)" ], + JuliaSyntax.parse_curry_chain => [ + "x /> f(a)" => "(chain x (/> (call f a)))" + "/> f(a)" => "(/> (call f a))" + "x /> f(a) /> g(b)" => "(chain x (/> (call f a)) (/> (call g b)))" + "x /> A.f(a,b)" => "(chain x (/> (call (. A (quote f)) a b)))" + "/> f(a) /> g(b)" => "(chain (/> (call f a)) (/> (call g b)))" + "x /> f() />> g()" => "(chain x (/> (call f)) (/>> (call g)))" + "x /> \$call" => "(chain x (/> (\$ call)))" + "x /> notcall[]" => "(chain x (/> (error (ref notcall))))" + ], JuliaSyntax.parse_range => [ "1:2" => "(call-i 1 : 2)" "1:2:3" => "(call-i 1 : 2 3)"