-
Notifications
You must be signed in to change notification settings - Fork 5
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
Fix argument evaluation issue in conditional expressions #218
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -32,7 +32,6 @@ defmodule Expression.Eval do | |
|
||
@numeric_kernel_operators [:+, :-, :*, :/, :>, :>=, :<, :<=] | ||
@kernel_operators @numeric_kernel_operators ++ [:==, :!=] | ||
@allowed_nested_function_arguments [:function, :lambda] ++ @kernel_operators | ||
|
||
def eval!(ast, context, mod \\ Expression.Callbacks) | ||
|
||
|
@@ -70,9 +69,8 @@ defmodule Expression.Eval do | |
def eval!({:function, opts}, context, mod) do | ||
function_name = opts[:name] || raise "Functions need a name" | ||
args = opts[:args] || [] | ||
arguments = Enum.reduce_while(args, [], &args_reducer(&1, function_name, context, mod, &2)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It should also make the library a lot faster because now it can short-circuit There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it's interesting as the intention of using reduce while here was to only evaluate until one of the things resolved to true. I'm happy that it's now solved but I'm unsure why it's solved. I'm going to need to spend some more time on this to make sure I understand. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It seems like the only reason to do this here is:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Given that the custom callback module does not change between function calls, I didn't see it as problematic to add it here and so pass it along every call to It is basically the implementation of the state monad |
||
|
||
case mod.handle(function_name, arguments, context) do | ||
case mod.handle(function_name, args, Map.put(context, "__CUSTOM_CALLBACK__", mod)) do | ||
{:ok, value} -> value | ||
{:error, reason} -> "ERROR: #{inspect(reason)}" | ||
end | ||
|
@@ -312,15 +310,4 @@ defmodule Expression.Eval do | |
|
||
def handle_not_found({:not_found, _}), do: nil | ||
def handle_not_found(value), do: value | ||
|
||
defp args_reducer({type, _args} = function, function_name, context, mod, acc) | ||
when type in @allowed_nested_function_arguments do | ||
value = eval!(function, context, mod) | ||
flag = if value == true && function_name == "or", do: :halt, else: :cont | ||
|
||
{flag, acc ++ [[literal: value]]} | ||
end | ||
|
||
defp args_reducer(function, _function_name, _context, _mod, acc), | ||
do: {:cont, acc ++ [function]} | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -113,6 +113,16 @@ defmodule Expression.EvalTest do | |
}) | ||
end | ||
|
||
test "if with nil values" do | ||
{:ok, ast, "", _, _, _} = | ||
Parser.parse("@if(status,\nLEFT(status, 2),\nfalse)") | ||
|
||
assert false == | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Without this change, this AST will break because it will try to "LEFT" "nil" but this is not needed because if will evaluate to false and so return it There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for this contribution! I was looking into a similar problem.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hi @Djack1010, I don't think there is a good way to fix the "and" because unfortunately every argument of the function needs to be evaluated and not return error in order to check if the function can return true or false. This fix would only work for OR and IFs |
||
Eval.eval!(ast, %{ | ||
"status" => nil | ||
}) | ||
end | ||
|
||
describe "lambdas" do | ||
test "with map" do | ||
{:ok, ast, "", _, _, _} = Parser.parse("@map(foo, &([&1, 'Button']))") | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This feels like we need to give this some more thought, it's an entirely different means of specifying the callback module. In other places we're passing it in explicitly as an optional argument.
Maybe this helper just isn't useful enough to be defined in a separate module, would that prevent us having to supply the callback module as an argument? Especially because the eval! function is used in the callback module primarily.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assumed that this was done before the implantation of “or” in the same way. I think it is fixed because essentially this code was duplicating what “or” is doing in the body of its function 🤔And yes I agree that definining the callback like that changes things quite a bit but I haven’t found a better way to pass it as argument to all functions without changing all of their arity (and so make it also incompatible with all existing callback modules), any idea of how to still solve the issue with the test and keep it as argument?