Skip to content

Commit

Permalink
Significant fixes to local expression projection
Browse files Browse the repository at this point in the history
  • Loading branch information
ashton314 committed Jul 29, 2024
1 parent 09e22a6 commit b554451
Show file tree
Hide file tree
Showing 3 changed files with 225 additions and 10 deletions.
59 changes: 50 additions & 9 deletions lib/chorex.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1059,6 +1059,30 @@ defmodule Chorex do
end
end

@doc """
Walks a local expression to pull out/convert function calls.
The `expr` in `Alice.(expr)` can *almost* be dropped directly into
the projection for the `Alice` node. Here's where that *almost*
comes in:
- `Alice.(1 + foo())` needs to be rewritten as `1 + impl.foo()` and
`foo/0` needs to be added to the list of functions for the Alice
behaviour.
- `Alice.(1 + Enum.sum(...))` should *not* be rewritten as `impl.…`.
There is some subtlety around tuples and function calls. Consider
how these expressions and their quoted representations compare:
- `{:ok, foo}` → `{:ok, {:foo, [], …}}`
- `{:ok, foo, bar}` → `{:{}, [], [:ok, {:foo, [], …}, {:bar, [], …}]}`
- `ok(bar)` → `{:ok, [], [{:bar, [], …}]}`
It seems that 2-tuples have some special representation, which is frustrating.
"""
def walk_local_expr(code, env, label, ctx) do
{code, acc} = Macro.postwalk(code, [], &do_local_project_wrapper(&1, &2, env, label, ctx))
return(code, acc)
Expand Down Expand Up @@ -1086,15 +1110,32 @@ defmodule Chorex do
num_args = length(args)
builtins = Kernel.__info__(:functions) ++ Kernel.__info__(:macros)

if Enum.member?(builtins, {funcname, num_args}) do
return(funcall, acc)
else
return(
quote do
impl.unquote(funcname)(unquote_splicing(args))
end,
[{label, {funcname, length(args)}} | acc]
)
cond do
# The function name :{} is variadic: it constructs a tuple from
# all its arguments; since it doesn't have an arity, we have to
# special-case it here.
:{} == funcname ->
return(funcall, acc)

# Likewise, __aliases__ is a special form and stays as-is.
:__aliases__ == funcname ->
return(funcall, acc)

# Foo.bar() should just get returned; that alias is a module
# name like IO or Enum.
match?({:., [{:__aliases__, _, _} | _]}, {funcname, args}) ->
return(funcall, acc)

Enum.member?(builtins, {funcname, num_args}) ->
return(funcall, acc)

true ->
return(
quote do
impl.unquote(funcname)(unquote_splicing(args))
end,
[{label, {funcname, length(args)}} | acc]
)
end
end

Expand Down
3 changes: 3 additions & 0 deletions lib/writer_monad.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ defmodule WriterMonad do
Makes it easy to do operations on code and track gathered data.
"""

@typedoc """
{expression, [callback_spec], [fresh_functions]}
"""
@type t() :: {any(), [any()], [any()]}

@spec bind({a, [b], [d]}, (a -> {c, [b], [d]})) :: {c, [b], [d]}
Expand Down
173 changes: 172 additions & 1 deletion test/chorex_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,8 @@ defmodule ChorexTest do
42 < get_answer()
end

assert {_, [{Alice, {:get_answer, 0}}], []} = walk_local_expr(stx, __ENV__, Alice, empty_ctx())
assert {_, [{Alice, {:get_answer, 0}}], []} =
walk_local_expr(stx, __ENV__, Alice, empty_ctx())
end

test "get function from inside complex if instruction" do
Expand Down Expand Up @@ -161,4 +162,174 @@ defmodule ChorexTest do

assert [] = diags
end

describe "local expression projection" do
test "single variable" do
stx =
quote do
Alice.(foo)
end

# Projection for Alice
assert {{:foo, [], _}, [], []} =
Chorex.project_local_expr(stx, __ENV__, Alice, Chorex.empty_ctx())

# Projection for Bob: should be nothing (empty block)
mzero = WriterMonad.mzero()
assert match?(^mzero, Chorex.project_local_expr(stx, __ENV__, Bob, Chorex.empty_ctx()))
end

test "simple patterns" do
stx1 =
quote do
Alice.({:ok, foo})
end

assert {{:ok, {:foo, [], _}}, [], []} =
Chorex.project_local_expr(stx1, __ENV__, Alice, Chorex.empty_ctx())

stx2 =
quote do
Alice.({:ok, foo, bar})
end

assert {{:{}, _, [:ok, {:foo, [], _}, {:bar, [], _}]}, [], []} =
Chorex.project_local_expr(stx2, __ENV__, Alice, Chorex.empty_ctx())

stx3 =
quote do
Alice.({:ok, foo, bar, baz})
end

assert {{:{}, _, [:ok, {:foo, [], _}, {:bar, [], _}, {:baz, [], _}]}, [], []} =
Chorex.project_local_expr(stx3, __ENV__, Alice, Chorex.empty_ctx())
end

test "expression with a variable" do
stx =
quote do
Alice.(1 + foo)
end

assert {{:+, _, [1, {:foo, [], ChorexTest}]}, [], []} =
Chorex.project_local_expr(stx, __ENV__, Alice, Chorex.empty_ctx())
end

# Generates syntax for a function call
def render_funcall({mod, ctx}, func, args) do
{{:., [], [{mod, [], ctx}, func]}, [], args}
end

def render_var(var_name) do
{var_name, [], __MODULE__}
end

test "expressions with function calls" do
# Simple function
stx =
quote do
Alice.(1 + foo())
end

foo_call = render_funcall({:impl, Chorex}, :foo, [])

assert {{:+, _, [1, ^foo_call]}, [{Alice, {:foo, 0}}], []} =
Chorex.project_local_expr(stx, __ENV__, Alice, Chorex.empty_ctx())

# Simple function with arg
stx =
quote do
Alice.(1 + foo(bar))
end

foo_call = render_funcall({:impl, Chorex}, :foo, [{:bar, [], ChorexTest}])

assert {{:+, _, [1, ^foo_call]}, [{Alice, {:foo, 1}}], []} =
Chorex.project_local_expr(stx, __ENV__, Alice, Chorex.empty_ctx())

# Simple function with funcall for arg
stx =
quote do
Alice.(1 + foo(bar()))
end

bar_call = render_funcall({:impl, Chorex}, :bar, [])
foo_call = render_funcall({:impl, Chorex}, :foo, [bar_call])

assert {{:+, _, [1, ^foo_call]}, [{Alice, {:foo, 1}}, {Alice, {:bar, 0}}], []} =
Chorex.project_local_expr(stx, __ENV__, Alice, Chorex.empty_ctx())

# Funcall in 1-tuple
stx =
quote do
Alice.({bar()})
end

bar_call = render_funcall({:impl, Chorex}, :bar, [])

assert {{:{}, [], [^bar_call]}, [{Alice, {:bar, 0}}], []} =
Chorex.project_local_expr(stx, __ENV__, Alice, Chorex.empty_ctx())

# Funcall in 2-tuple
stx =
quote do
Alice.({:ok, bar()})
end

bar_call = render_funcall({:impl, Chorex}, :bar, [])

assert {{:ok, ^bar_call}, [{Alice, {:bar, 0}}], []} =
Chorex.project_local_expr(stx, __ENV__, Alice, Chorex.empty_ctx())

# Funcall in 3-tuple
stx =
quote do
Alice.({:ok, foo, bar()})
end

foo_var = render_var(:foo)
bar_call = render_funcall({:impl, Chorex}, :bar, [])

assert {{:{}, _, [:ok, ^foo_var, ^bar_call]}, [{Alice, {:bar, 0}}], []} =
Chorex.project_local_expr(stx, __ENV__, Alice, Chorex.empty_ctx())
end

def render_alias(mod) do
import Utils
mod = mod |> downcase_atom() |> upcase_atom()
{:__aliases__, [alias: false], [mod]}
end

def render_alias_call(mod, func, args) do
{{:., [], [render_alias(mod), func]}, [], args}
end

test "call to Enum or IO etc. is preserved" do
stx =
quote do
Alice.(Enum.foo())
end

enum_call = render_alias_call(Enum, :foo, [])

assert {^enum_call, [], []} =
Chorex.project_local_expr(stx, __ENV__, Alice, Chorex.empty_ctx())

# Complex, nested stuff
stx =
quote do
Alice.(1 + Enum.foo(bar(42, baz)))
end

enum_call =
render_alias_call(Enum, :foo, [
render_funcall({:impl, Chorex}, :bar, [42, render_var(:baz)])
])

assert {{:+, _, [1, ^enum_call]}, [{Alice, {:bar, 2}}], []} =
Chorex.project_local_expr(stx, __ENV__, Alice, Chorex.empty_ctx())
end

test "deeply nested pattern"
end
end

0 comments on commit b554451

Please sign in to comment.