From 3c02f7c73f80f254df0631a8977bb44385c9a692 Mon Sep 17 00:00:00 2001 From: Ashton Wiersdorf Date: Tue, 30 Jul 2024 17:02:21 -0600 Subject: [PATCH 01/10] Add test to showcase how higher-order args should be projected --- test/chorex_test.exs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/chorex_test.exs b/test/chorex_test.exs index 4ffdbc5..0be96ac 100644 --- a/test/chorex_test.exs +++ b/test/chorex_test.exs @@ -344,5 +344,21 @@ defmodule ChorexTest do assert {{^foo_var, {:{}, [], [^bar_var, ^baz_var, [^zoop_var]]}}, [], []} = Chorex.project_local_expr(stx, __ENV__, Alice, Chorex.empty_ctx()) end + + test "passing functions as arguments doesn't get confused" do + stx = + quote do + Alice.(special_func(&foo/1)) + end + + foo_var = {:&, [], [{:/, [], [render_var(:foo), 1]}]} + + Chorex.project_local_expr(stx, __ENV__, Alice, Chorex.empty_ctx()) + |> Macro.to_string() + |> IO.inspect(label: "result") + + assert {{{:., [], [{:impl, [], Chorex}, :special_func]}, [], [^foo_var]}, [{Alice, {:special_func, 1}}], []} = + Chorex.project_local_expr(stx, __ENV__, Alice, Chorex.empty_ctx()) + end end end From 70fec300bd410fc2767fb32a8c127a5b589b1505 Mon Sep 17 00:00:00 2001 From: Ashton Wiersdorf Date: Wed, 31 Jul 2024 15:37:05 -0600 Subject: [PATCH 02/10] Functions can take multiple args; remove global/local distinction --- lib/chorex.ex | 302 +++++++++++++++++----------- test/chorex_test.exs | 24 +++ test/generalized_functions_test.exs | 34 ++++ 3 files changed, 242 insertions(+), 118 deletions(-) create mode 100644 test/generalized_functions_test.exs diff --git a/lib/chorex.ex b/lib/chorex.ex index a96dd06..f161f84 100644 --- a/lib/chorex.ex +++ b/lib/chorex.ex @@ -612,18 +612,6 @@ defmodule Chorex do def project({:__block__, _meta, terms}, env, label, ctx), do: project_sequence(terms, env, label, ctx) - def project({:def, _meta, [fn_name, [do: fn_body]]}, env, label, ctx) do - case fn_name do - # Local functions - {_name, _, [{{:., _, _}, _, _} | _]} -> - project_local_func(fn_name, fn_body, env, label, ctx) - - # Global functions - _ -> - project_global_func(fn_name, fn_body, env, label, ctx) - end - end - # Alice.e ~> Bob.x def project( {:~>, _meta, [party1, party2]}, @@ -742,51 +730,98 @@ defmodule Chorex do project_local_expr(expr, env, label, ctx) end - # Application projection - def project({fn_name, _meta, []}, _env, _label, _ctx) - when is_atom(fn_name) do - return( - quote do - unquote(fn_name)(impl, config, nil) - end - ) + # def project({:def, _meta, [{fn_name, _meta, params}, [do: fn_body]]}, env, label, ctx) do + # # case fn_name do + # # # Local functions + # # {_name, _, [{{:., _, _}, _, _} | _]} -> + # # project_local_func(fn_name, fn_body, env, label, ctx) + + # # # Global functions + # # _ -> + # # project_global_func(fn_name, fn_body, env, label, ctx) + # # end + # end + + def project({:def, _meta, [{fn_name, _meta2, params}, [do: body]]}, env, label, ctx) do + monadic do + params_ <- mapM(params, &project_identifier(&1, env, label)) + body_ <- project(body, env, label, ctx) + # no return value from a function definition + r <- mzero() + + return( + r, + [], + [ + {fn_name, + quote do + def unquote(fn_name)(impl, config, unquote_splicing(params_)) do + unquote(body_) + end + end} + ] + ) + end end - def project({fn_name, _meta, [arg]}, env, label, ctx) + # Application projection + def project({fn_name, _meta, args}, env, label, ctx) when is_atom(fn_name) do - with {:ok, actor} <- actor_from_local_exp(arg, env) do - if label == actor do - monadic do - arg_ <- project(arg, env, label, ctx) + monadic do + args_ <- mapM(args, &project_local_expr(&1, env, label, ctx)) - return( - quote do - unquote(fn_name)(impl, config, unquote(arg_)) - end - ) + return( + quote do + unquote(fn_name)(impl, config, unquote_splicing(args_)) end - else - return( - quote do - # dummy value; shouldn't be used - unquote(fn_name)(impl, config, nil) - end - ) - end - else - :error -> - # Add two to the arity to account for impl, config - {:&, m1, [{:/, m2, [{var_name, m3, var_ctx}, arity]}]} = arg - arg_ = {:&, m1, [{:/, m2, [{var_name, m3, var_ctx}, arity + 2]}]} - - return( - quote do - unquote(fn_name)(impl, config, unquote(arg_)) - end - ) + ) end end + # def project({fn_name, _meta, []}, _env, _label, _ctx) + # when is_atom(fn_name) do + # return( + # quote do + # unquote(fn_name)(impl, config, nil) + # end + # ) + # end + + # def project({fn_name, _meta, [arg]}, env, label, ctx) + # when is_atom(fn_name) do + # with {:ok, actor} <- actor_from_local_exp(arg, env) do + # if label == actor do + # monadic do + # arg_ <- project(arg, env, label, ctx) + + # return( + # quote do + # unquote(fn_name)(impl, config, unquote(arg_)) + # end + # ) + # end + # else + # return( + # quote do + # # dummy value; shouldn't be used + # unquote(fn_name)(impl, config, nil) + # end + # ) + # end + # else + # :error -> + # # Add two to the arity to account for impl, config + # {:&, m1, [{:/, m2, [{var_name, m3, var_ctx}, arity]}]} = arg + # arg_ = {:&, m1, [{:/, m2, [{var_name, m3, var_ctx}, arity + 2]}]} + + # return( + # quote do + # unquote(fn_name)(impl, config, unquote(arg_)) + # end + # ) + # end + # end + def project(code, _env, _label, _ctx) do raise ProjectionError, message: "Unrecognized code: #{inspect(code)}" end @@ -893,75 +928,75 @@ defmodule Chorex do def project_sequence(expr, env, label, ctx), do: project(expr, env, label, ctx) - def project_local_func( - {fn_name, _, [{{:., _, [actor]}, _, [{var_name, _, _}]}]}, - body, - env, - label, - ctx - ) do - {:ok, actor} = actor_from_local_exp(actor, env) - var = Macro.var(var_name, nil) - - monadic do - body_ <- project(body, env, label, ctx) - r <- mzero() - - if actor == label do - return(r, [], [ - {fn_name, - quote do - def unquote(fn_name)(impl, config, unquote(var)) do - unquote(body_) - end - end} - ]) - else - return(r, [], [ - {fn_name, - quote do - # var shouldn't be capturable - def unquote(fn_name)(impl, config, _input_x) do - unquote(body_) - end - end} - ]) - end - end - end - - # # TODO generalize these handlers - def project_global_func({fn_name, _, []}, body, env, label, ctx) do - monadic do - body_ <- project(body, env, label, ctx) - r <- mzero() - - return(r, [], [ - {fn_name, - quote do - def unquote(fn_name)(impl, config) do - unquote(body_) - end - end} - ]) - end - end - - def project_global_func({fn_name, _, [var]}, body, env, label, ctx) do - monadic do - body_ <- project(body, env, label, ctx) - r <- mzero() - - return(r, [], [ - {fn_name, - quote do - def unquote(fn_name)(impl, config, unquote(var)) do - unquote(body_) - end - end} - ]) - end - end + # def project_local_func( + # {fn_name, _, [{{:., _, [actor]}, _, [{var_name, _, _}]}]}, + # body, + # env, + # label, + # ctx + # ) do + # {:ok, actor} = actor_from_local_exp(actor, env) + # var = Macro.var(var_name, nil) + + # monadic do + # body_ <- project(body, env, label, ctx) + # r <- mzero() + + # if actor == label do + # return(r, [], [ + # {fn_name, + # quote do + # def unquote(fn_name)(impl, config, unquote(var)) do + # unquote(body_) + # end + # end} + # ]) + # else + # return(r, [], [ + # {fn_name, + # quote do + # # var shouldn't be capturable + # def unquote(fn_name)(impl, config, _input_x) do + # unquote(body_) + # end + # end} + # ]) + # end + # end + # end + + # # # TODO generalize these handlers + # def project_global_func({fn_name, _, []}, body, env, label, ctx) do + # monadic do + # body_ <- project(body, env, label, ctx) + # r <- mzero() + + # return(r, [], [ + # {fn_name, + # quote do + # def unquote(fn_name)(impl, config) do + # unquote(body_) + # end + # end} + # ]) + # end + # end + + # def project_global_func({fn_name, _, [var]}, body, env, label, ctx) do + # monadic do + # body_ <- project(body, env, label, ctx) + # r <- mzero() + + # return(r, [], [ + # {fn_name, + # quote do + # def unquote(fn_name)(impl, config, unquote(var)) do + # unquote(body_) + # end + # end} + # ]) + # end + # end # # Local expression handling @@ -989,6 +1024,23 @@ defmodule Chorex do # defp local_var_or_expr?({{:., _, [_]}, _, _}), # do: :expr + # ⟦ℓ₁.var⟧_ℓ₁ ⇒ var + # ⟦ℓ₂.var⟧_ℓ₁ ⇒ _ + def project_identifier({{:., _m0, [actor]}, _m1, [var]}, env, label) do + {:ok, actor} = actor_from_local_exp(actor, env) + + if actor == label do + return(var) + else + return(Macro.var(:_, nil)) + end + end + + def project_identifier({var, _m, _ctx} = stx, _env, _label) + when is_atom(var) do + return(stx) + end + @doc """ Like `project/3`, but focus on handling `ActorName.local_var`, `ActorName.local_func()` or `ActorName.(local_exp)`. Handles walking @@ -1059,6 +1111,20 @@ defmodule Chorex do end end + # Choreography higher-order function @fn_name/3 + def project_local_expr({:/, m1, [{:@, m2, [fn_name]}, arity]}, _env, _label, _ctx) + when is_number(arity) do + # arity + 2 to account for the args `impl` and `config` + return({:&, m2, [{:/, m1, [fn_name, arity + 2]}]}) + end + + def project_local_expr({:_, _meta, _ctx1} = stx, _env, _label, _ctx) do + return(stx) + end + + # Need to handle @fn_name/1 syntax + # def project_local_expr({:/ }) + @doc """ Walks a local expression to pull out/convert function calls. diff --git a/test/chorex_test.exs b/test/chorex_test.exs index 0be96ac..d5ce630 100644 --- a/test/chorex_test.exs +++ b/test/chorex_test.exs @@ -346,6 +346,30 @@ defmodule ChorexTest do end test "passing functions as arguments doesn't get confused" do + quote do + defchor [Alice, Bob] do + def main(func) do + with Alice.(a) <- func.(Alice.get_b()) do + Alice.(a) ~> Bob.(b) + end + end + + def f1(Alice.(x), Bob.(y)) do + Bob.(y) ~> Alice.(y) + Alice.(x + y) + end + + def run(_) do + # main(&f1/1) + # main(@f1/1) + f1(Alice.(42), Bob.(17)) + end + end + end + |> Macro.expand_once(__ENV__) + |> Macro.to_string() + |> IO.puts() + stx = quote do Alice.(special_func(&foo/1)) diff --git a/test/generalized_functions_test.exs b/test/generalized_functions_test.exs new file mode 100644 index 0000000..82b4e20 --- /dev/null +++ b/test/generalized_functions_test.exs @@ -0,0 +1,34 @@ +defmodule GeneralizedFunctionsTest do + use ExUnit.Case + import Chorex + + quote do + defchor [Alice, Bob] do + def main(func, Alice.(c)) do + with Alice.(a) <- func.(Alice.get_b(c)) do + Alice.(a) ~> Bob.(b) + end + end + + def f1(Alice.(x), Bob.(y)) do + Bob.(y) ~> Alice.(y) + Alice.(x + y) + end + + def f2(Alice.(x)) do + Alice.(x * 2) + end + + def run(_) do + # main(&f1/1) + # main(@f1/1) + f1(Alice.(42), Bob.(17)) + # Alice.(&should_be_local/3, 42) + main(@f2/1, Alice.(6)) + end + end + end + |> Macro.expand_once(__ENV__) + |> Macro.to_string() + |> IO.puts() +end From f10a2a3d626cdd7c4d76d0bb60d398966210d51f Mon Sep 17 00:00:00 2001 From: Ashton Wiersdorf Date: Wed, 31 Jul 2024 16:58:48 -0600 Subject: [PATCH 03/10] Fix func refs: prefix `&impl.` where needed --- lib/chorex.ex | 28 ++++++++++++++++++++++++---- test/generalized_functions_test.exs | 11 +++++------ 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/lib/chorex.ex b/lib/chorex.ex index f161f84..4a22f0b 100644 --- a/lib/chorex.ex +++ b/lib/chorex.ex @@ -1111,7 +1111,7 @@ defmodule Chorex do end end - # Choreography higher-order function @fn_name/3 + # @fn_name/3 - Choreography higher-order function: appears in local expr locations def project_local_expr({:/, m1, [{:@, m2, [fn_name]}, arity]}, _env, _label, _ctx) when is_number(arity) do # arity + 2 to account for the args `impl` and `config` @@ -1122,9 +1122,6 @@ defmodule Chorex do return(stx) end - # Need to handle @fn_name/1 syntax - # def project_local_expr({:/ }) - @doc """ Walks a local expression to pull out/convert function calls. @@ -1136,6 +1133,11 @@ defmodule Chorex do `foo/0` needs to be added to the list of functions for the Alice behaviour. + - `Alice.some_func(&other_local_func/2)` needs to be rewritten as + `impl.some_func(&impl.other_local_func/2)` and both `some_func` and + `other_local_func` need 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 @@ -1155,6 +1157,7 @@ defmodule Chorex do end def do_local_project_wrapper(code, acc, env, label, ctx) do + # We should never synthesize new functions, so last tuple value is [] {code_, acc_, []} = do_local_project(code, acc, env, label, ctx) {code_, acc_} end @@ -1166,6 +1169,23 @@ defmodule Chorex do return(Macro.var(:config, __MODULE__), acc) end + # References to functions (if not prefixed with a module, it needs + # to get impl. prefix and added to behaviour list) + defp do_local_project({:&, m1, [{:/, m2, [fn_name, arity]}]} = stx, acc, _env, label, _ctx) + when is_integer(arity) do + case fn_name do + {fn_name, _, _} when is_atom(fn_name) -> + stx = {:&, m1, [{:/, m2, [ + {{:., [], [Macro.var(:impl, nil), fn_name]}, [no_parens: true], []}, + arity + ]}]} + return(stx, [{label, {fn_name, arity}} | acc]) + + {{:., _, _}, _, _} -> + return(stx, acc) + end + end + defp do_local_project({varname, _meta, nil} = var, acc, _env, _label, _ctx) when is_atom(varname) do return(var, acc) diff --git a/test/generalized_functions_test.exs b/test/generalized_functions_test.exs index 82b4e20..cfdd9ab 100644 --- a/test/generalized_functions_test.exs +++ b/test/generalized_functions_test.exs @@ -10,7 +10,7 @@ defmodule GeneralizedFunctionsTest do end end - def f1(Alice.(x), Bob.(y)) do + def f1(Alice.({:ok, x}), Bob.(y)) do Bob.(y) ~> Alice.(y) Alice.(x + y) end @@ -19,11 +19,10 @@ defmodule GeneralizedFunctionsTest do Alice.(x * 2) end - def run(_) do - # main(&f1/1) - # main(@f1/1) - f1(Alice.(42), Bob.(17)) - # Alice.(&should_be_local/3, 42) + def run() do + f1(Alice.({:ok, 42}), Bob.(17)) + Alice.foobar(&should_be_local/3, 42) + Alice.foobar(&Enum.should_be_remote/3, 42) main(@f2/1, Alice.(6)) end end From dc115874e7349a234d9f58d4b0a95d3a4233324e Mon Sep 17 00:00:00 2001 From: Ashton Wiersdorf Date: Wed, 31 Jul 2024 17:11:17 -0600 Subject: [PATCH 04/10] Cleanup old code --- lib/chorex.ex | 127 +------------------------------------------------- 1 file changed, 1 insertion(+), 126 deletions(-) diff --git a/lib/chorex.ex b/lib/chorex.ex index 4a22f0b..0f11b3f 100644 --- a/lib/chorex.ex +++ b/lib/chorex.ex @@ -730,18 +730,7 @@ defmodule Chorex do project_local_expr(expr, env, label, ctx) end - # def project({:def, _meta, [{fn_name, _meta, params}, [do: fn_body]]}, env, label, ctx) do - # # case fn_name do - # # # Local functions - # # {_name, _, [{{:., _, _}, _, _} | _]} -> - # # project_local_func(fn_name, fn_body, env, label, ctx) - - # # # Global functions - # # _ -> - # # project_global_func(fn_name, fn_body, env, label, ctx) - # # end - # end - + # Function projection def project({:def, _meta, [{fn_name, _meta2, params}, [do: body]]}, env, label, ctx) do monadic do params_ <- mapM(params, &project_identifier(&1, env, label)) @@ -778,50 +767,6 @@ defmodule Chorex do end end - # def project({fn_name, _meta, []}, _env, _label, _ctx) - # when is_atom(fn_name) do - # return( - # quote do - # unquote(fn_name)(impl, config, nil) - # end - # ) - # end - - # def project({fn_name, _meta, [arg]}, env, label, ctx) - # when is_atom(fn_name) do - # with {:ok, actor} <- actor_from_local_exp(arg, env) do - # if label == actor do - # monadic do - # arg_ <- project(arg, env, label, ctx) - - # return( - # quote do - # unquote(fn_name)(impl, config, unquote(arg_)) - # end - # ) - # end - # else - # return( - # quote do - # # dummy value; shouldn't be used - # unquote(fn_name)(impl, config, nil) - # end - # ) - # end - # else - # :error -> - # # Add two to the arity to account for impl, config - # {:&, m1, [{:/, m2, [{var_name, m3, var_ctx}, arity]}]} = arg - # arg_ = {:&, m1, [{:/, m2, [{var_name, m3, var_ctx}, arity + 2]}]} - - # return( - # quote do - # unquote(fn_name)(impl, config, unquote(arg_)) - # end - # ) - # end - # end - def project(code, _env, _label, _ctx) do raise ProjectionError, message: "Unrecognized code: #{inspect(code)}" end @@ -928,76 +873,6 @@ defmodule Chorex do def project_sequence(expr, env, label, ctx), do: project(expr, env, label, ctx) - # def project_local_func( - # {fn_name, _, [{{:., _, [actor]}, _, [{var_name, _, _}]}]}, - # body, - # env, - # label, - # ctx - # ) do - # {:ok, actor} = actor_from_local_exp(actor, env) - # var = Macro.var(var_name, nil) - - # monadic do - # body_ <- project(body, env, label, ctx) - # r <- mzero() - - # if actor == label do - # return(r, [], [ - # {fn_name, - # quote do - # def unquote(fn_name)(impl, config, unquote(var)) do - # unquote(body_) - # end - # end} - # ]) - # else - # return(r, [], [ - # {fn_name, - # quote do - # # var shouldn't be capturable - # def unquote(fn_name)(impl, config, _input_x) do - # unquote(body_) - # end - # end} - # ]) - # end - # end - # end - - # # # TODO generalize these handlers - # def project_global_func({fn_name, _, []}, body, env, label, ctx) do - # monadic do - # body_ <- project(body, env, label, ctx) - # r <- mzero() - - # return(r, [], [ - # {fn_name, - # quote do - # def unquote(fn_name)(impl, config) do - # unquote(body_) - # end - # end} - # ]) - # end - # end - - # def project_global_func({fn_name, _, [var]}, body, env, label, ctx) do - # monadic do - # body_ <- project(body, env, label, ctx) - # r <- mzero() - - # return(r, [], [ - # {fn_name, - # quote do - # def unquote(fn_name)(impl, config, unquote(var)) do - # unquote(body_) - # end - # end} - # ]) - # end - # end - # # Local expression handling # From 2c918be9cd402ef0e8ae44da8631fa3424d04883 Mon Sep 17 00:00:00 2001 From: Ashton Wiersdorf Date: Wed, 31 Jul 2024 17:24:17 -0600 Subject: [PATCH 05/10] WIP convert tests to use new syntax for higher-order choreographies --- README.md | 8 ++--- lib/chorex.ex | 8 ++--- test/chorex_test.exs | 46 +++++++------------------ test/function_test.exs | 4 +-- test/higher_order_test.exs | 65 +++-------------------------------- test/non_behaviour_example.ex | 2 +- test/proxied_actor_test.exs | 4 +-- 7 files changed, 30 insertions(+), 107 deletions(-) diff --git a/README.md b/README.md index bd27152..e410b91 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,7 @@ defchor [Actor1, Actor2, ...] do ... end - def run(_) do + def run() do ... end end @@ -226,7 +226,7 @@ defchor [Actor, OtherActor] do OtherActor.(other_var) end - def run(_) do + def run() do higher_order_chor(&some_local_chor/1) end end @@ -250,7 +250,7 @@ To create a choreography, start by making a module, and writing the choreography ```elixir defmodule Bookstore do defchor [Actor1, Actor2] do - def run(_) do + def run() do Actor1.(... some expr ...) ~> Actor2.(some_var) Actor2.some_computation(some_var) ~> Actor1.(the_result) ... @@ -363,7 +363,7 @@ So, for example, if you have a simple choreography like this: ```elixir defchor [Alice, Bob] do - def run(_) do + def run() do Alice.pick_modulus() ~> Bob.(m) Bob.gen_key(m) ~> Alice.(bob_key) Alice.encrypt(message, bob_key) diff --git a/lib/chorex.ex b/lib/chorex.ex index 0f11b3f..d330cfc 100644 --- a/lib/chorex.ex +++ b/lib/chorex.ex @@ -45,7 +45,7 @@ defmodule Chorex do ```elixir defmodule ThreePartySeller do defchor [Buyer1, Buyer2, Seller] do - def run(_) do + def run() do Buyer1.get_book_title() ~> Seller.(b) Seller.get_price("book:" <> b) ~> Buyer1.(p) Seller.get_price("book:" <> b) ~> Buyer2.(p) @@ -237,7 +237,7 @@ defmodule Chorex do ```elixir defchor [Buyer, {Seller, :singleton}] do - def run(_) do + def run() do Buyer.get_book_title() ~> Seller.(b) Seller.get_price(b) ~> Buyer.(p) if Buyer.in_budget(p) do @@ -528,9 +528,7 @@ defmodule Chorex do def init(impl, args) do receive do {:config, config} -> - # only supporting one argument right now - arg = Enum.at(args, 0, nil) - ret = run(impl, config, arg) + ret = apply(__MODULE__, :run, [impl, config | args])# run(impl, config, arg) send(config[:super], {:chorex_return, unquote(actor), ret}) end end diff --git a/test/chorex_test.exs b/test/chorex_test.exs index d5ce630..86cb272 100644 --- a/test/chorex_test.exs +++ b/test/chorex_test.exs @@ -5,7 +5,7 @@ defmodule ChorexTest do defmodule TestChor do defchor [Buyer, Seller] do - def run(_) do + def run() do Buyer.get_book_title() ~> Seller.(b) Seller.get_price("book:" <> b) ~> Buyer.(p) Buyer.(p + 2) @@ -51,7 +51,7 @@ defmodule ChorexTest do defmodule TestChor2 do defchor [Buyer1, Buyer2, Seller1] do - def run(_) do + def run() do Buyer1.get_book_title() ~> Seller1.(b) Seller1.get_price("book:" <> b) ~> Buyer1.(p) Seller1.get_price("book:" <> b) ~> Buyer2.(p) @@ -346,42 +346,22 @@ defmodule ChorexTest do end test "passing functions as arguments doesn't get confused" do - quote do - defchor [Alice, Bob] do - def main(func) do - with Alice.(a) <- func.(Alice.get_b()) do - Alice.(a) ~> Bob.(b) - end - end - - def f1(Alice.(x), Bob.(y)) do - Bob.(y) ~> Alice.(y) - Alice.(x + y) - end - - def run(_) do - # main(&f1/1) - # main(@f1/1) - f1(Alice.(42), Bob.(17)) - end - end - end - |> Macro.expand_once(__ENV__) - |> Macro.to_string() - |> IO.puts() - stx = quote do Alice.(special_func(&foo/1)) end - foo_var = {:&, [], [{:/, [], [render_var(:foo), 1]}]} - - Chorex.project_local_expr(stx, __ENV__, Alice, Chorex.empty_ctx()) - |> Macro.to_string() - |> IO.inspect(label: "result") - - assert {{{:., [], [{:impl, [], Chorex}, :special_func]}, [], [^foo_var]}, [{Alice, {:special_func, 1}}], []} = + # quoted form of &impl.foo/1 + foo_var = + {:&, [], + [ + {:/, [context: ChorexTest, imports: [{2, Kernel}]], + [{{:., [], [{:impl, [], nil}, :foo]}, [no_parens: true], []}, 1]} + ]} + + assert {{{:., [], [{:impl, [], Chorex}, :special_func]}, [], [^foo_var]}, + [{Alice, {:special_func, 1}}, {Alice, {:foo, 1}}], + []} = Chorex.project_local_expr(stx, __ENV__, Alice, Chorex.empty_ctx()) end end diff --git a/test/function_test.exs b/test/function_test.exs index 4863788..f69bf66 100644 --- a/test/function_test.exs +++ b/test/function_test.exs @@ -23,7 +23,7 @@ defmodule FunctionTest do end end - def run(_) do + def run() do loop() end end @@ -69,7 +69,7 @@ defmodule FunctionTest do end end - def run(_) do + def run() do loop(CounterServer.(0)) end end diff --git a/test/higher_order_test.exs b/test/higher_order_test.exs index a0019e1..88b7051 100644 --- a/test/higher_order_test.exs +++ b/test/higher_order_test.exs @@ -2,53 +2,6 @@ defmodule HigherOrderTest do use ExUnit.Case import Chorex - # quote do - # defchor [Buyer3, Contributor3, Seller3] do - # def bookseller(decision_func) do - # Buyer3.get_book_title() ~> Seller3.(the_book) - - # with Buyer3.(decision) <- decision_func.(Seller3.get_price("book:" <> the_book)) do - # if Buyer3.(decision) do - # Buyer3[L] ~> Seller3 - # Buyer3.get_address() ~> Seller3.(the_address) - # Seller3.get_delivery_date(the_book, the_address) ~> Buyer3.(d_date) - # Buyer3.(d_date) - # else - # Buyer3[R] ~> Seller3 - # Buyer3.(nil) - # end - # end - # end - - # def one_party(Seller3.(the_price)) do - # Seller3.(the_price) ~> Buyer3.(p) - # Buyer3.(p < get_budget()) - # end - - # def two_party(Seller3.(the_price)) do - # Seller3.(the_price) ~> Buyer3.(p) - # Seller3.(the_price) ~> Contributor3.(p) - # Contributor3.compute_contrib(p) ~> Buyer3.(contrib) - # Buyer3.(p - contrib < get_budget()) - # end - - # def run(Buyer3.(include_contributions?)) do - # if Buyer3.(include_contributions?) do - # Buyer3[L] ~> Contributor3 - # Buyer3[L] ~> Seller3 - # bookseller(&two_party/1) - # else - # Buyer3[R] ~> Contributor3 - # Buyer3[R] ~> Seller3 - # bookseller(&one_party/1) - # end - # end - # end - # end - # |> Macro.expand_once(__ENV__) - # |> Macro.to_string() - # |> IO.puts() - defmodule TestChor3 do defchor [Buyer3, Contributor3, Seller3] do def bookseller(decision_func) do @@ -83,11 +36,11 @@ defmodule HigherOrderTest do if Buyer3.(include_contributions?) do Buyer3[L] ~> Contributor3 Buyer3[L] ~> Seller3 - bookseller(&two_party/1) + bookseller(@two_party/1) else Buyer3[R] ~> Contributor3 Buyer3[R] ~> Seller3 - bookseller(&one_party/1) + bookseller(@one_party/1) end end end @@ -178,10 +131,10 @@ defmodule HigherOrderTest do def run(Alice.(want_pbj?)) do if Alice.(want_pbj?) do Alice[L] ~> Bob - big_chor(&pbj/1) + big_chor(@pbj/1) else Alice[R] ~> Bob - big_chor(&hamncheese/1) + big_chor(@hamncheese/1) end end end @@ -190,10 +143,6 @@ defmodule HigherOrderTest do defmodule MyAlice4 do use TestChor4.Chorex, :alice - def run_choreography(impl, config) do - Alice.big_chor(impl, config, &Alice.hamncheese/3) - end - def get_bread(), do: "Italian herbs and cheese" def get_allergens(), do: ["mushroom", "peanut_butter"] def allergic_to(lst, thing), do: Enum.any?(lst, fn x -> x == thing end) @@ -208,15 +157,11 @@ defmodule HigherOrderTest do def make_sandwich(bread, stuff) do [bread] ++ stuff ++ [bread] end - - def run_choreography(impl, config) do - Bob.big_chor(impl, config, &Bob.hamncheese/3) - end end test "big hoc test" do alice = spawn(MyAlice4, :init, [[false]]) - bob = spawn(MyBob4, :init, [[]]) + bob = spawn(MyBob4, :init, [[false]]) config = %{Alice => alice, Bob => bob, :super => self()} diff --git a/test/non_behaviour_example.ex b/test/non_behaviour_example.ex index 105b798..23518a5 100644 --- a/test/non_behaviour_example.ex +++ b/test/non_behaviour_example.ex @@ -3,7 +3,7 @@ defmodule NonBehaviourExample do defmodule TestChor do defchor [AliceBehaviorTest, BobBehaviorTest] do - def run(_) do + def run() do AliceBehaviorTest.hello() ~> BobBehaviorTest.(greeting) BobBehaviorTest.(greeting) end diff --git a/test/proxied_actor_test.exs b/test/proxied_actor_test.exs index 18cdc63..7449db1 100644 --- a/test/proxied_actor_test.exs +++ b/test/proxied_actor_test.exs @@ -5,7 +5,7 @@ defmodule ProxiedActorTest do # quote do # defchor [BuyerP, {SellerP, :singleton}] do - # def run(_) do + # def run() do # BuyerP.get_book_title() ~> SellerP.(b) # SellerP.get_price(b) ~> BuyerP.(p) @@ -32,7 +32,7 @@ defmodule ProxiedActorTest do defmodule BooksellerProxied do defchor [BuyerP, {SellerP, :singleton}] do - def run(_) do + def run() do BuyerP.get_book_title() ~> SellerP.(b) SellerP.get_price(b) ~> BuyerP.(p) From d1452e9725c830da1e9c8a35468a332e141c4c5f Mon Sep 17 00:00:00 2001 From: Ashton Wiersdorf Date: Wed, 31 Jul 2024 17:30:59 -0600 Subject: [PATCH 06/10] WIP init_func_test converted but not compiling --- test/function_test.exs | 21 ------ test/init_func_test.exs | 149 +++++++++++++++------------------------- 2 files changed, 55 insertions(+), 115 deletions(-) diff --git a/test/function_test.exs b/test/function_test.exs index f69bf66..0438857 100644 --- a/test/function_test.exs +++ b/test/function_test.exs @@ -34,27 +34,6 @@ defmodule FunctionTest do assert {_, _, _} = expanded end - # quote do - # defchor [CounterServer, CounterClient] do - # def loop(CounterServer.(i)) do - # if CounterClient.continue?() do - # CounterClient[L] ~> CounterServer - # CounterClient.bump() ~> CounterServer.(incr_amt) - # loop(CounterServer.(incr_amt + i)) - # else - # CounterClient[R] ~> CounterServer - # CounterServer.(i) ~> CounterClient.(final_result) - # CounterClient.(final_result) - # end - # end - - # loop(CounterServer.(0)) - # end - # end - # |> Macro.expand_once(__ENV__) - # |> Macro.to_string() - # |> IO.puts() - defmodule CounterTest do defchor [CounterServer, CounterClient] do def loop(CounterServer.(i)) do diff --git a/test/init_func_test.exs b/test/init_func_test.exs index e8ebc00..7b64a2b 100644 --- a/test/init_func_test.exs +++ b/test/init_func_test.exs @@ -2,120 +2,69 @@ defmodule InitFuncTest do use ExUnit.Case import Chorex - # quote do - # defchor [StarterAlice, StarterBob, StarterEve] do - # def sell_book(decision_process) do - # StarterAlice.get_book_title() ~> StarterEve.(the_book) - - # with StarterAlice.(want_book?) <- decision_process.(StarterEve.get_price(the_book)) do - # if StarterAlice.(want_book?) do - # StarterAlice[L] ~> StarterEve - # StarterAlice.get_address() ~> StarterEve.(the_address) - # StarterEve.get_shipping(the_book, the_address) ~> StarterAlice.(delivery_date) - # StarterAlice.(delivery_date) - # else - # StarterAlice[R] ~> StarterEve - # StarterAlice.(nil) - # end - # end - # end - - # def one_party(StarterEve.(the_price)) do - # StarterEve.(the_price) ~> StarterAlice.(full_price) - # StarterAlice.(full_price < get_budget()) - # end - - # def two_party(StarterEve.(the_price)) do - # StarterEve.(the_price) ~> StarterAlice.(full_price) - # StarterEve.(the_price) ~> StarterBob.(full_price) - # StarterBob.(full_price / 2) ~> StarterAlice.(contrib) - # StarterAlice.((full_price - contrib) < get_budget()) - # end - - # # def run(StarterAlice.(involve_bob?), StarterAlice.(budget)) do - # def run(StarterAlice.(involve_bob?)) do - # if StarterAlice.(involve_bob?) do - # StarterAlice[L] ~> StarterEve - # StarterAlice[L] ~> StarterBob - # sell_book(&two_party/1) - # else - # StarterAlice[R] ~> StarterEve - # StarterAlice[R] ~> StarterBob - # sell_book(&one_party/1) - # end - # end - # end - # end - # |> Macro.expand_once(__ENV__) - # |> Macro.to_string() - # |> IO.puts() - defmodule StarterChor do - # defchor [StarterAlice, StarterBob, {StarterEve, :singleton}] do - defchor [StarterAlice, StarterBob, StarterEve] do - def sell_book(decision_process) do - StarterAlice.get_book_title() ~> StarterEve.(the_book) - - with StarterAlice.({want_book?, my_cost}) <- - decision_process.(StarterEve.get_price(the_book)) do - if StarterAlice.(want_book?) do - StarterAlice[L] ~> StarterEve - StarterAlice.get_address() ~> StarterEve.(the_address) - StarterEve.get_shipping(the_book, the_address) ~> StarterAlice.(delivery_date) - StarterAlice.({delivery_date, my_cost}) + defchor [StAlice, StBob, StEve] do + def sell_book(decision_process, StAlice.(budget)) do + StAlice.get_book_title() ~> StEve.(the_book) + + with StAlice.({want_book?, my_cost}) <- + decision_process.(StEve.get_price(the_book), StAlice.(budget)) do + if StAlice.(want_book?) do + StAlice[L] ~> StEve + StAlice.get_address() ~> StEve.(the_address) + StEve.get_shipping(the_book, the_address) ~> StAlice.(delivery_date) + StAlice.({delivery_date, my_cost}) else - StarterAlice[R] ~> StarterEve - StarterAlice.(nil) + StAlice[R] ~> StEve + StAlice.(:too_expensive) end end end - def one_party(StarterEve.(the_price)) do - StarterEve.(the_price) ~> StarterAlice.(full_price) - StarterAlice.({full_price < get_budget(), full_price}) + def one_party(StEve.(the_price), StAlice.(budget)) do + StEve.(the_price) ~> StAlice.(full_price) + StAlice.({full_price < budget, full_price}) end - def two_party(StarterEve.(the_price)) do - StarterEve.(the_price) ~> StarterAlice.(full_price) - StarterEve.(the_price) ~> StarterBob.(full_price) - StarterBob.(full_price / 2) ~> StarterAlice.(contrib) + def two_party(StEve.(the_price), StAlice.(budget)) do + StEve.(the_price) ~> StAlice.(full_price) + StEve.(the_price) ~> StBob.(full_price) + StBob.(full_price / 2) ~> StAlice.(contrib) - with StarterAlice.(my_price) <- StarterAlice.(full_price - contrib) do - StarterAlice.({my_price < get_budget(), my_price}) + with StAlice.(my_price) <- StAlice.(full_price - contrib) do + StAlice.({my_price < budget, my_price}) end end - # def run(StarterAlice.(involve_bob?), StarterAlice.(budget)) do - def run(StarterAlice.(involve_bob?)) do - if StarterAlice.(involve_bob?) do - StarterAlice[L] ~> StarterEve - StarterAlice[L] ~> StarterBob - sell_book(&two_party/1) + def run(StAlice.(involve_bob?), StAlice.(budget)) do + if StAlice.(involve_bob?) do + StAlice[L] ~> StEve + StAlice[L] ~> StBob + sell_book(@two_party/1, StAlice.(budget)) else - StarterAlice[R] ~> StarterEve - StarterAlice[R] ~> StarterBob - sell_book(&one_party/1) + StAlice[R] ~> StEve + StAlice[R] ~> StBob + sell_book(@one_party/1, StAlice.(budget)) end end end end - defmodule StarterAliceImpl do + defmodule StAliceImpl do use StarterChor.Chorex, :starteralice def get_book_title(), do: "Amusing Ourselves to Death" def get_address(), do: "123 San Seriffe" - def get_budget(), do: 42 end - defmodule StarterEveImpl do + defmodule StEveImpl do use StarterChor.Chorex, :startereve def get_price(_), do: 25 def get_shipping(_book, _addr), do: "next week" end - defmodule StarterBobImpl do + defmodule StBobImpl do use StarterChor.Chorex, :starterbob end @@ -123,27 +72,39 @@ defmodule InitFuncTest do Chorex.start( StarterChor.Chorex, %{ - StarterAlice => StarterAliceImpl, - StarterEve => StarterEveImpl, - StarterBob => StarterBobImpl + StAlice => StAliceImpl, + StEve => StEveImpl, + StBob => StBobImpl }, - [false] + [false, 42] ) - assert_receive {:chorex_return, StarterAlice, {"next week", 25}} + assert_receive {:chorex_return, StAlice, {"next week", 25}} end test "startup with different arguments does what's expected" do Chorex.start( StarterChor.Chorex, %{ - StarterAlice => StarterAliceImpl, - StarterEve => StarterEveImpl, - StarterBob => StarterBobImpl + StAlice => StAliceImpl, + StEve => StEveImpl, + StBob => StBobImpl + }, + [true, 42] + ) + + assert_receive {:chorex_return, StAlice, {"next week", 12.5}} + + Chorex.start( + StarterChor.Chorex, + %{ + StAlice => StAliceImpl, + StEve => StEveImpl, + StBob => StBobImpl }, - [true] + [true, 2] ) - assert_receive {:chorex_return, StarterAlice, {"next week", 12.5}} + assert_receive {:chorex_return, StAlice, :too_expensive} end end From 0eb6acc693995dc82f11c9b682a7347179d42188 Mon Sep 17 00:00:00 2001 From: Ashton Wiersdorf Date: Thu, 1 Aug 2024 12:47:02 -0600 Subject: [PATCH 07/10] Fix different modes of function calls; all tests pass --- lib/chorex.ex | 41 +++++++++++++++++----- test/generalized_functions_test.exs | 54 ++++++++++++++--------------- test/init_func_test.exs | 10 +++--- 3 files changed, 65 insertions(+), 40 deletions(-) diff --git a/lib/chorex.ex b/lib/chorex.ex index d330cfc..abc7bfd 100644 --- a/lib/chorex.ex +++ b/lib/chorex.ex @@ -528,7 +528,7 @@ defmodule Chorex do def init(impl, args) do receive do {:config, config} -> - ret = apply(__MODULE__, :run, [impl, config | args])# run(impl, config, arg) + ret = apply(__MODULE__, :run, [impl, config | args]) send(config[:super], {:chorex_return, unquote(actor), ret}) end end @@ -724,10 +724,24 @@ defmodule Chorex do end # Local expressions of the form Actor.thing or Actor.(thing) - def project({{:., _, _}, _, _} = expr, env, label, ctx) do + def project({{:., _, [{:__aliases__, _, _} | _]}, _, _} = expr, env, label, ctx) do project_local_expr(expr, env, label, ctx) end + # Applying functions stored in variables: some_func.(args) + def project({{:., _m1, [{fn_var_name, _m2, _ctx} = fn_var]}, _m3, args}, env, label, ctx) + when is_atom(fn_var_name) do + monadic do + args_ <- mapM(args, &project_local_expr(&1, env, label, ctx)) + + return( + quote do + unquote(fn_var).(impl, config, unquote_splicing(args_)) + end + ) + end + end + # Function projection def project({:def, _meta, [{fn_name, _meta2, params}, [do: body]]}, env, label, ctx) do monadic do @@ -766,7 +780,7 @@ defmodule Chorex do end def project(code, _env, _label, _ctx) do - raise ProjectionError, message: "Unrecognized code: #{inspect(code)}" + raise ProjectionError, message: "No projection for form: #{Macro.to_string(code)}\n Stx: #{inspect(code)}" end # @@ -995,6 +1009,11 @@ defmodule Chorex do return(stx) end + def project_local_expr(stx, _, _, _) do + raise ProjectionError, + message: "Unable to project local expression: #{Macro.to_string(stx)}" + end + @doc """ Walks a local expression to pull out/convert function calls. @@ -1047,11 +1066,17 @@ defmodule Chorex do defp do_local_project({:&, m1, [{:/, m2, [fn_name, arity]}]} = stx, acc, _env, label, _ctx) when is_integer(arity) do case fn_name do - {fn_name, _, _} when is_atom(fn_name) -> - stx = {:&, m1, [{:/, m2, [ - {{:., [], [Macro.var(:impl, nil), fn_name]}, [no_parens: true], []}, - arity - ]}]} + {fn_name, _, _} when is_atom(fn_name) -> + stx = + {:&, m1, + [ + {:/, m2, + [ + {{:., [], [Macro.var(:impl, nil), fn_name]}, [no_parens: true], []}, + arity + ]} + ]} + return(stx, [{label, {fn_name, arity}} | acc]) {{:., _, _}, _, _} -> diff --git a/test/generalized_functions_test.exs b/test/generalized_functions_test.exs index cfdd9ab..6f174cc 100644 --- a/test/generalized_functions_test.exs +++ b/test/generalized_functions_test.exs @@ -1,33 +1,33 @@ defmodule GeneralizedFunctionsTest do - use ExUnit.Case - import Chorex + # use ExUnit.Case + # import Chorex - quote do - defchor [Alice, Bob] do - def main(func, Alice.(c)) do - with Alice.(a) <- func.(Alice.get_b(c)) do - Alice.(a) ~> Bob.(b) - end - end + # quote do + # defchor [Alice, Bob] do + # def main(func, Alice.(c)) do + # with Alice.(a) <- func.(Alice.get_b(c)) do + # Alice.(a) ~> Bob.(b) + # end + # end - def f1(Alice.({:ok, x}), Bob.(y)) do - Bob.(y) ~> Alice.(y) - Alice.(x + y) - end + # def f1(Alice.({:ok, x}), Bob.(y)) do + # Bob.(y) ~> Alice.(y) + # Alice.(x + y) + # end - def f2(Alice.(x)) do - Alice.(x * 2) - end + # def f2(Alice.(x)) do + # Alice.(x * 2) + # end - def run() do - f1(Alice.({:ok, 42}), Bob.(17)) - Alice.foobar(&should_be_local/3, 42) - Alice.foobar(&Enum.should_be_remote/3, 42) - main(@f2/1, Alice.(6)) - end - end - end - |> Macro.expand_once(__ENV__) - |> Macro.to_string() - |> IO.puts() + # def run() do + # f1(Alice.({:ok, 42}), Bob.(17)) + # Alice.foobar(&should_be_local/3, 42) + # Alice.foobar(&Enum.should_be_remote/3, 42) + # main(@f2/1, Alice.(6)) + # end + # end + # end + # |> Macro.expand_once(__ENV__) + # |> Macro.to_string() + # |> IO.puts() end diff --git a/test/init_func_test.exs b/test/init_func_test.exs index 7b64a2b..4ec57c7 100644 --- a/test/init_func_test.exs +++ b/test/init_func_test.exs @@ -40,32 +40,32 @@ defmodule InitFuncTest do if StAlice.(involve_bob?) do StAlice[L] ~> StEve StAlice[L] ~> StBob - sell_book(@two_party/1, StAlice.(budget)) + sell_book(@two_party/2, StAlice.(budget)) else StAlice[R] ~> StEve StAlice[R] ~> StBob - sell_book(@one_party/1, StAlice.(budget)) + sell_book(@one_party/2, StAlice.(budget)) end end end end defmodule StAliceImpl do - use StarterChor.Chorex, :starteralice + use StarterChor.Chorex, :stalice def get_book_title(), do: "Amusing Ourselves to Death" def get_address(), do: "123 San Seriffe" end defmodule StEveImpl do - use StarterChor.Chorex, :startereve + use StarterChor.Chorex, :steve def get_price(_), do: 25 def get_shipping(_book, _addr), do: "next week" end defmodule StBobImpl do - use StarterChor.Chorex, :starterbob + use StarterChor.Chorex, :stbob end test "startup with run function works" do From f489f202797266fa2ce33199d92bed4e640b7c1c Mon Sep 17 00:00:00 2001 From: Ashton Wiersdorf Date: Thu, 1 Aug 2024 15:07:59 -0600 Subject: [PATCH 08/10] Work on README, cool new test for functions using the crypto lib --- README.md | 43 +++++++++++++--- lib/chorex.ex | 68 +++++++++++++++++++++--- test/generalized_functions_test.exs | 80 ++++++++++++++++++----------- 3 files changed, 147 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index e410b91..38b77a4 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ Chorex is available on Hex.pm. Install by including the following in your `mix.e def deps do [ ..., - {:chorex, "~> 0.1.0"}, + {:chorex, "~> 0.4.0"}, ... ] end @@ -91,7 +91,7 @@ Note that this is *experimental software* and stuff *will* break. Please don't r A choreography is a birds-eye view of an interaction between nodes in a distributed system. You have some set of *actors*—in Elixir parlance processes—that exchange *messages* while also running some *local computation*—i.e. functions that don't rely on talking to other nodes in the system. -### Choreography syntax +## Choreography syntax Chorex introduces some new syntax for choreographies. Here's a breakdown of how it works: @@ -103,7 +103,7 @@ end The `defchor` macro wraps a choreography and translates it into core Elixir code. You give `defchor` a list of actors, specified as if they were module names, and then a `do` block wraps the choreography body. -The body of the choreography is a set of functions. One function named `run` must be present; this will serve as the entry point into the choreography. `run` takes one argument, which may be `_` if you don't need it. This argument comes from when you instantiate the choreography with `Chorex.start`. (More on `Chorex.start` in a minute.) +The body of the choreography is a set of functions. One function named `run` must be present; this will serve as the entry point into the choreography. The arguments to `run` come from the third argument to the `Chorex.start` function. (More on `Chorex.start` and function parameters in a minute.) ```elixir defchor [Actor1, Actor2, ...] do @@ -117,6 +117,8 @@ defchor [Actor1, Actor2, ...] do end ``` +### Message passing expressions + Inside the body of functions you can write message passing expressions. Examples: ```elixir @@ -129,15 +131,15 @@ Actor1.(var1_a + var1_b) ~> Actor2.(var2_c) Formal syntax: ```bnf - message_pass ::= $local_exp ~> $actor.($var) + message_pass ::= $local_exp ~> $actor.($pat) - local_exp ::= $actor.($var) + local_exp ::= $actor.($pat) | $actor.$func($exp, ...) | $actor.($exp) actor ::= Module name (e.g. Actor) func ::= Function name (e.g. frobnicate(...)) - var ::= Variable name (e.g. foo, i) + pat ::= Pattern match expr (e.g. a variable like `foo` or tuples `{:ok, bar}` etc.) exp ::= Elixir expression (e.g. foo + sum([1, 2, 3])) ``` @@ -147,7 +149,9 @@ The `~>` indicates sending a message between actors. The left-hand-side must be 2. A function local to Actor1 (with or without arguments, also all local to Actor1) 3. An expression local to Actor1 -The right-and-side must be `Actor2.()`. This means that the left-hand-side will be computed on `Actor1` and send to `Actor2` where it will be stored in variable ``. +The right-and-side must be `Actor2.()`. This means that the left-hand-side will be computed on `Actor1` and send to `Actor2` where it will be matched against the pattern `pattern`. + +### Local expressions *Local expressions* are computations that happen on a single node. These computations are isolated from each other—i.e. every location has its own variables. For example, if I say: @@ -184,6 +188,8 @@ The `remember` function here will be defined on the the implementation for the ` Local functions are not defined as part of the choreography; instead, you implement these in a separate Elixir module. More on that later. +### `if` expressions and knowledge of choice broadcasting + ```elixir if Actor1.make_decision() do Actor1[L] ~> Actor2 @@ -196,6 +202,29 @@ end `if` expressions are supported. Some actor makes a choice of which branch to go down. It is then *crucial* (and, at this point, entirely up to the user) that that deciding actor inform all other actors about the choice of branch with the special `ActorName[L] ~> OtherActorName` syntax. Note the lack of `.` and variable names. Furthermore, the true branch is always `L` (left) and the false branch is always `R` (right). +### Function syntax + +```elixir +defchor [Alice, Bob] do + def run(Alice.(msg)) do + with Bob.({pub, priv}) <- Bob.gen_key() do + Bob.(pub) ~> Alice.(key) + exchange_message(Alice.encrypt(msg <> "\n love, Alice", key), Bob.(priv)) + end + end + + def exchange_message(Alice.(enc_msg), Bob.(priv)) do + Alice.(enc_msg) ~> Bob.(enc_msg) + Alice.(:letter_sent) + Bob.decrypt(enc_msg, priv) + end +end +``` + +Choreographies support functions and function calls—even recursive ones. + +### Higher-order choreographies + ```elixir def higher_order_chor(other_chor) do ... other_chor.(...) ... diff --git a/lib/chorex.ex b/lib/chorex.ex index abc7bfd..f9723f6 100644 --- a/lib/chorex.ex +++ b/lib/chorex.ex @@ -197,17 +197,21 @@ defmodule Chorex do if Buyer3.(get_contribution?) do Buyer3[L] ~> Contributor3 Buyer3[L] ~> Seller3 - bookseller(&two_party/1) + bookseller(@two_party/1) else Buyer3[R] ~> Contributor3 Buyer3[R] ~> Seller3 - bookseller(&one_party/1) + bookseller(@one_party/1) end end end end ``` + Notice the `@two_part/1` syntax: the `@` is necessary so Chorex + knows that this is a reference to a function defined inside the + `defchor` block; it needs to handle these references specially. + Now, when you start up the choreography, the you can instruct the choreography whether or not to run the three-party scenario. The first item in the list of arguments will get sent to the node @@ -391,6 +395,19 @@ defmodule Chorex do defguard is_immediate(x) when is_number(x) or is_atom(x) or is_binary(x) @doc """ + Start a choreography. + + Takes a choreography module like `MyCoolThing.Chorex`, a map from + actor names to implementing modules, and a list of arguments to pass + to the `run` function. + + ## Example + + ```elixir + Chorex.start(ThreePartySeller.Chorex, + %{ Buyer1 => MyBuyer1, Buyer2 => MyBuyer2, Seller => MySeller }, + []) + ``` """ def start(chorex_module, actor_impl_map, init_args) do actor_list = chorex_module.get_actors() @@ -434,6 +451,8 @@ defmodule Chorex do @doc """ Define a new choreography. + + See the documentation for the `Chorex` module for more details. """ defmacro defchor(actor_list, do: block) do # actors is a list of *all* actors; @@ -787,6 +806,9 @@ defmodule Chorex do # Projecting sequence of statements # + @doc """ + Project a sequence of expressions, such as those found in a block. + """ @spec project_sequence(term(), Macro.Env.t(), atom(), map()) :: WriterMonad.t() def project_sequence( {:__block__, _meta, [expr]}, @@ -911,8 +933,12 @@ defmodule Chorex do # defp local_var_or_expr?({{:., _, [_]}, _, _}), # do: :expr - # ⟦ℓ₁.var⟧_ℓ₁ ⇒ var - # ⟦ℓ₂.var⟧_ℓ₁ ⇒ _ + @doc """ + Project an expression like `Actor.var` to either `var` or `_`. + + Project to `var` when `Actor` matches the label we're projecting to, + or `_` so that whatever data flows to that point can't be captured. + """ def project_identifier({{:., _m0, [actor]}, _m1, [var]}, env, label) do {:ok, actor} = actor_from_local_exp(actor, env) @@ -929,7 +955,9 @@ defmodule Chorex do end @doc """ - Like `project/3`, but focus on handling `ActorName.local_var`, + Project local expressions of the form `ActorName.(something)`. + + Like `project/4`, but focus on handling `ActorName.(local_var)`, `ActorName.local_func()` or `ActorName.(local_exp)`. Handles walking the local expression to gather list of functions needed for the behaviour to implement. @@ -1127,6 +1155,30 @@ defmodule Chorex do return(x, acc) end + @doc """ + Flatten nested block expressions as much as possible. + + Turn something like + + ```elixir + do + do + … + end + end + ``` + + into simply + + ```elixir + do + … + end + ``` + + This is important for the `merge/2` function to be able to tell when + two expressions are equivalent. + """ def flatten_block({:__block__, _meta, [expr]}), do: expr def flatten_block({:__block__, meta, exprs}) do @@ -1146,7 +1198,11 @@ defmodule Chorex do def flatten_block(other), do: other @doc """ - Perform the control merge function, but flatten block expressions at each step + Perform the control merge function. + + Flatten block expressions at each step: sometimes auxiliary blocks + get created around bits of the projection; trim these out at this + step so equivalent expressions look equivalent. """ def merge(x, x), do: x def merge(x, y), do: merge_step(flatten_block(x), flatten_block(y)) diff --git a/test/generalized_functions_test.exs b/test/generalized_functions_test.exs index 6f174cc..e725c39 100644 --- a/test/generalized_functions_test.exs +++ b/test/generalized_functions_test.exs @@ -1,33 +1,51 @@ defmodule GeneralizedFunctionsTest do - # use ExUnit.Case - # import Chorex - - # quote do - # defchor [Alice, Bob] do - # def main(func, Alice.(c)) do - # with Alice.(a) <- func.(Alice.get_b(c)) do - # Alice.(a) ~> Bob.(b) - # end - # end - - # def f1(Alice.({:ok, x}), Bob.(y)) do - # Bob.(y) ~> Alice.(y) - # Alice.(x + y) - # end - - # def f2(Alice.(x)) do - # Alice.(x * 2) - # end - - # def run() do - # f1(Alice.({:ok, 42}), Bob.(17)) - # Alice.foobar(&should_be_local/3, 42) - # Alice.foobar(&Enum.should_be_remote/3, 42) - # main(@f2/1, Alice.(6)) - # end - # end - # end - # |> Macro.expand_once(__ENV__) - # |> Macro.to_string() - # |> IO.puts() + use ExUnit.Case + import Chorex + + defmodule MyCrypto do + defchor [AliceC, BobC] do + def run(AliceC.(msg)) do + with BobC.({pub, priv}) <- BobC.gen_key() do + BobC.(pub) ~> AliceC.(key) + exchange_message(AliceC.encrypt(msg <> "\n love, Alice", key), BobC.(priv)) + end + end + + def exchange_message(AliceC.(enc_msg), BobC.(priv)) do + AliceC.(enc_msg) ~> BobC.(enc_msg) + AliceC.(:letter_sent) + BobC.decrypt(enc_msg, priv) + end + end + end + + defmodule MyAlice do + use MyCrypto.Chorex, :alicec + + def encrypt(msg, [expt, modulus]) do + :crypto.mod_pow(msg, expt, modulus) + end + end + + defmodule MyBob do + use MyCrypto.Chorex, :bobc + + def gen_key() do + :crypto.generate_key(:rsa, {512, 5}) + end + + def decrypt(msg, [_pub_expt, modulus, priv_expt | _]) do + :crypto.mod_pow(msg, priv_expt, modulus) + end + end + + test "basic key exchange with rich functions works" do + Chorex.start(MyCrypto.Chorex, + %{ AliceC => MyAlice, + BobC => MyBob }, + ["hello, world"]) + + assert_receive {:chorex_return, AliceC, :letter_sent} + assert_receive {:chorex_return, BobC, "hello, world\n love, Alice"} + end end From c519751cee227d6641161e93954996d24437bc50 Mon Sep 17 00:00:00 2001 From: Ashton Wiersdorf Date: Thu, 1 Aug 2024 15:39:40 -0600 Subject: [PATCH 09/10] Finish README updates for v0.4 --- README.md | 39 +++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 38b77a4..8069917 100644 --- a/README.md +++ b/README.md @@ -221,28 +221,23 @@ defchor [Alice, Bob] do end ``` -Choreographies support functions and function calls—even recursive ones. - -### Higher-order choreographies +Choreographies support functions and function calls—even recursive ones. Function parameters need to be annotated with the actor they live at, and the arguments when calling the function need to match. Calling a function with the wrong actor will result in the parameter getting `nil`. E.g. calling `exchange_message` above like so will not work properly: ```elixir -def higher_order_chor(other_chor) do - ... other_chor.(...) ... -end +exchange_message(Bob.(msg), Alice.(priv)) ``` -Chorex supports higher-order choreographies. These are choreographies that take another choreography as an argument where it can be applied like a function. +(and not just because the variables are wrong—the actor names don't match so the parameters won't get the values they need). + +### Higher-order choreographies ```elixir -def some_local_chor(Actor.(var_name)) do - Actor.(var_name) ~> OtherActor.(other_var) - OtherActor.(other_var) +def higher_order_chor(other_chor) do + ... other_chor.(...) ... end ``` -This creates a choreography that can be passed as an argument to the `higher_order_chor` function. This takes as an argument a variable living at a particular actor, and returns another value on a potentially different node. - -You would combine the choreographies like so: +Chorex supports higher-order choreographies. This means you can pass the functions defined *inside the `defchor` block* around as you would with functions. Higher-order choreographic functions *don't* get an actor prefix and you call them as you would a function bound to a variable, like so: ```elixir defchor [Actor, OtherActor] do @@ -256,12 +251,14 @@ defchor [Actor, OtherActor] do end def run() do - higher_order_chor(&some_local_chor/1) + higher_order_chor(@some_local_chor/1) end end ``` -Right now these functions are limited to a single argument. +Note that when referring to the function, you **must** use the `@func_name/3` syntax—the Chorex compiler notices the `@` and processes the function reference differently. This is because the functions defined with `def` inside the `defchor` block have private internal details (when Chorex builds them, they get special implicit arguments added) and Chorex needs to handle references to these functions specially. + +### Variable binding ```elixir with OtherActor.(other_var) <- other_chor.(Actor.(var)) do @@ -269,7 +266,9 @@ with OtherActor.(other_var) <- other_chor.(Actor.(var)) do end ``` -You can use `with` to bind a variable to the result of calling a higher-order choreography. Note that right now you can only have one `<-` in the expression. +You can bind the result of some expression to a variable/pattern at an actor with `with`. In the case of a higher-order choreography (seen above) this is whatever was on node `OtherActor` when `other_chor` executed. You may also use `with` for binding local expressions, as seen in the `exchange_message` example under § Function syntax. + +Right now you can only have one `<-` in the expression and there can only be one actor that binds variables. ## Creating a choreography @@ -322,14 +321,14 @@ Use the `Chorex.start/3` function to start a choreography: Chorex.start(MyChoreography.Chorex, %{ Actor1 => MyActor1Impl, Actor2 => MyActor2Impl }, - [arg_to_run]) + [args, to, run]) ``` The arguments are as follows: 1. The name of the `Chorex` module to use. (The `defchor` macro creates this module for you; in the above example there is a `MyChoreography` module with a top-level `defchor` declaration that creates the `Chorex` submodule on expansion.) 2. A map from actor name to implementation module name. - 3. A list of arguments to the `run` function in the Choreography. (Right now, this only allows a single argument.) + 3. A list of arguments to the `run` function in the Choreography. These will automatically get sent to the right nodes. Once the actors are done, they will send the last value they computed to the current process tagged with the actor they were implementing. So, for this example, you could see what `Actor1` computed by awaiting: @@ -360,6 +359,10 @@ If you find any bugs or would like to suggest a feature, please [open an issue o We will collect change descriptions here until we come up with a more stable format when changes get bigger. + - v0.4.0; 2024-08-01 + + Functions can take arbitrary number of arguments from different actors. + - v0.3.1; 2024-07-30 Fix many problems around local expression projection. From aaa5220128c387ec6f8fb33e045e9156cfe0e1ee Mon Sep 17 00:00:00 2001 From: Ashton Wiersdorf Date: Thu, 1 Aug 2024 15:40:15 -0600 Subject: [PATCH 10/10] Release v0.4.0 with bug fixes and support for arbitrary-arity funcs --- mix.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index 35fe9b8..676607d 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule Chorex.MixProject do def project do [ app: :chorex, - version: "0.3.1", + version: "0.4.0", elixir: "~> 1.16", start_permanent: Mix.env() == :prod, description: description(),