Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Experiment: Playing with currying, chaining and underscores #148

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
135 changes: 130 additions & 5 deletions src/expr.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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...)
Expand Down
3 changes: 3 additions & 0 deletions src/kinds.jl
Original file line number Diff line number Diff line change
Expand Up @@ -823,6 +823,8 @@ const _kind_names =
"'"
".'"
"->"
"/>"
"/>>"

"BEGIN_UNICODE_OPS"
"¬"
Expand Down Expand Up @@ -878,6 +880,7 @@ const _kind_names =
"block"
"call"
"dotcall"
"chain"
"comparison"
"curly"
"inert" # QuoteNode; not quasiquote
Expand Down
42 changes: 38 additions & 4 deletions src/parser.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 ...
Expand Down Expand Up @@ -1417,6 +1448,7 @@ end
# &a ==> (& a)
# ::a ==> (::-pre a)
# $a ==> ($ a)
# ->a ==> (-> a)
#
# flisp: parse-unary-prefix
function parse_unary_prefix(ps::ParseState)
Expand All @@ -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
Expand Down
6 changes: 6 additions & 0 deletions src/tokenize.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions test/parser.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
Expand Down