From b5544511e1405d506dfe9ac5da6a4403c39426d7 Mon Sep 17 00:00:00 2001 From: Ashton Wiersdorf Date: Mon, 29 Jul 2024 17:17:11 -0600 Subject: [PATCH] Significant fixes to local expression projection --- lib/chorex.ex | 59 ++++++++++++--- lib/writer_monad.ex | 3 + test/chorex_test.exs | 173 ++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 225 insertions(+), 10 deletions(-) diff --git a/lib/chorex.ex b/lib/chorex.ex index b4aed3d..a96dd06 100644 --- a/lib/chorex.ex +++ b/lib/chorex.ex @@ -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) @@ -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 diff --git a/lib/writer_monad.ex b/lib/writer_monad.ex index 622c335..56d485e 100644 --- a/lib/writer_monad.ex +++ b/lib/writer_monad.ex @@ -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]} diff --git a/test/chorex_test.exs b/test/chorex_test.exs index fff2a7a..011f93e 100644 --- a/test/chorex_test.exs +++ b/test/chorex_test.exs @@ -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 @@ -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