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

Refactor module call and function call in SC #1221

Merged
merged 4 commits into from
Aug 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 24 additions & 12 deletions lib/archethic/contracts/interpreter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ defmodule Archethic.Contracts.Interpreter do
alias __MODULE__.ActionInterpreter
alias __MODULE__.ConditionInterpreter
alias __MODULE__.FunctionInterpreter
alias __MODULE__.FunctionKeys

alias __MODULE__.ConditionValidator

Expand All @@ -22,7 +23,6 @@ defmodule Archethic.Contracts.Interpreter do

@type version() :: integer()
@type execute_opts :: [time_now: DateTime.t()]
@type function_key() :: {String.t(), integer()}

@doc """
Dispatch through the correct interpreter.
Expand Down Expand Up @@ -341,7 +341,7 @@ defmodule Archethic.Contracts.Interpreter do
end

defp parse_contract(1, ast) do
functions_keys = get_function_keys(ast)
functions_keys = parse_functions_keys(ast)

case parse_ast_block(ast, %Contract{}, functions_keys) do
{:ok, contract} ->
Expand Down Expand Up @@ -434,20 +434,32 @@ defmodule Archethic.Contracts.Interpreter do
Utils.get_current_time_for_interval(interval)
end

defp get_function_keys([{{:atom, "fun"}, _, [{{:atom, function_name}, _, args} | _]} | rest]) do
[{function_name, length(args), :private} | get_function_keys(rest)]
defp parse_functions_keys(blocks, function_keys \\ FunctionKeys.new())

defp parse_functions_keys(
[{{:atom, "fun"}, _, [{{:atom, function_name}, _, args} | _]} | rest],
function_keys
) do
new_keys = FunctionKeys.add_private(function_keys, function_name, length(args))
parse_functions_keys(rest, new_keys)
end

defp get_function_keys([
{{:atom, "export"}, _,
[{{:atom, "fun"}, _, [{{:atom, function_name}, _, args} | _]} | _]}
| rest
]) do
[{function_name, length(args), :public} | get_function_keys(rest)]
defp parse_functions_keys(
[
{{:atom, "export"}, _,
[{{:atom, "fun"}, _, [{{:atom, function_name}, _, args} | _]} | _]}
| rest
],
function_keys
) do
new_keys = FunctionKeys.add_public(function_keys, function_name, length(args))
parse_functions_keys(rest, new_keys)
end

defp get_function_keys([_ | rest]), do: get_function_keys(rest)
defp get_function_keys([]), do: []
defp parse_functions_keys([_ | rest], function_keys),
do: parse_functions_keys(rest, function_keys)

defp parse_functions_keys([], function_keys), do: function_keys

# -----------------------------------------
# contract validation
Expand Down
28 changes: 22 additions & 6 deletions lib/archethic/contracts/interpreter/action_interpreter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreter do

alias Archethic.Contracts.Contract
alias Archethic.Contracts.ContractConstants, as: Constants
alias Archethic.Contracts.Interpreter
alias Archethic.Contracts.Interpreter.ASTHelper, as: AST
alias Archethic.Contracts.Interpreter.CommonInterpreter
alias Archethic.Contracts.Interpreter.FunctionKeys
alias Archethic.Contracts.Interpreter.Library
alias Archethic.Contracts.Interpreter.Scope

# # Module `Contract` is handled differently
Expand All @@ -17,7 +18,7 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreter do
@doc """
Parse the given node and return the trigger and the actions block.
"""
@spec parse(any(), list(Interpreter.function_key())) ::
@spec parse(any(), FunctionKeys.t()) ::
{:ok, Contract.trigger_type(), Macro.t()} | {:error, any(), String.t()}
def parse({{:atom, "actions"}, _, [keyword, [do: block]]}, functions_keys) do
trigger_type = extract_trigger(keyword)
Expand Down Expand Up @@ -162,10 +163,26 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreter do
# | .__/|_| \___| \_/\_/ \__,_|_|_|\_\
# |_|
# ----------------------------------------------------------------------

# module call
defp prewalk(
node,
node =
{{:., _meta, [{:__aliases__, _, [atom: module_name]}, {:atom, function_name}]}, _,
args},
acc
) do
arity =
if Library.function_tagged_with?(module_name, function_name, :write_contract),
do: length(args) + 1,
else: length(args)

case Library.validate_module_call(module_name, function_name, arity) do
:ok -> {node, acc}
{:error, _reason, message} -> throw({:error, node, message})
end
end

defp prewalk(node, acc) do
CommonInterpreter.prewalk(node, acc)
end

Expand All @@ -177,9 +194,8 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreter do
# | .__/ \___/|___/\__| \_/\_/ \__,_|_|_|\_\
# |_|
# ----------------------------------------------------------------------
defp postwalk(node, acc) do
CommonInterpreter.postwalk(node, acc)
end

defp postwalk(node, acc), do: CommonInterpreter.postwalk(node, acc)

# keep only the transaction fields we are interested in
# these are all the fields that are copied from `prev_tx` to `next_tx`
Expand Down
159 changes: 59 additions & 100 deletions lib/archethic/contracts/interpreter/common_interpreter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ defmodule Archethic.Contracts.Interpreter.CommonInterpreter do

alias Archethic.Contracts.Interpreter.ASTHelper, as: AST
alias Archethic.Contracts.Interpreter.Library
alias Archethic.Contracts.Interpreter.FunctionKeys
alias Archethic.Contracts.Interpreter.Scope

# ----------------------------------------------------------------------
Expand Down Expand Up @@ -90,14 +91,12 @@ defmodule Archethic.Contracts.Interpreter.CommonInterpreter do
def prewalk(node = {{:atom, var_name}, _, nil}, acc) when is_binary(var_name), do: {node, acc}
def prewalk(node = {:atom, var_name}, acc) when is_binary(var_name), do: {node, acc}

# module call
def prewalk(node = {{:., _, [{:__aliases__, _, _}, _]}, _, _}, acc), do: {node, acc}
def prewalk(node = {:., _, [{:__aliases__, _, _}, _]}, acc), do: {node, acc}

# whitelisted modules
def prewalk(node = {:__aliases__, _, [atom: _module_name]}, acc),
def prewalk(node = {{:., _, [{:__aliases__, _, [atom]}, _]}, _, _}, acc) when is_atom(atom),
do: {node, acc}

def prewalk(node = {:., _, [{:__aliases__, _, _}, _]}, acc), do: {node, acc}
def prewalk(node = {:__aliases__, _, [atom: _module_name]}, acc), do: {node, acc}

# internal modules (Process/Scope/Kernel)
def prewalk(node = {:__aliases__, _, [atom]}, acc) when is_atom(atom), do: {node, acc}

Expand Down Expand Up @@ -178,15 +177,14 @@ defmodule Archethic.Contracts.Interpreter.CommonInterpreter do
end

# Dot access nested (x.y.z)
def prewalk({{:., _, [first_arg = {{:., _, _}, _, _}, {:atom, key_name}]}, _, []}, acc) do
{nested, new_acc} = prewalk(first_arg, acc)

# or Module.function().z
def prewalk({{:., _, [first_arg, {:atom, key_name}]}, _, []}, acc) do
new_node =
quote do
get_in(unquote(nested), [unquote(key_name)])
Map.get(unquote(first_arg), unquote(key_name))
end

{new_node, new_acc}
{new_node, acc}
end

# Map access non-nested (x[y])
Expand All @@ -205,17 +203,15 @@ defmodule Archethic.Contracts.Interpreter.CommonInterpreter do

# Map access nested (x[y][z])
def prewalk(
_node = {{:., _, [Access, :get]}, _, [first_arg = {{:., _, _}, _, _}, accessor]},
_node = {{:., _, [Access, :get]}, _, [first_arg, accessor]},
acc
) do
{nested, new_acc} = prewalk(first_arg, acc)

new_node =
quote do
get_in(unquote(nested), [unquote(accessor)])
Map.get(unquote(first_arg), unquote(accessor))
end

{new_node, new_acc}
{new_node, acc}
end

# for var in list
Expand Down Expand Up @@ -254,7 +250,16 @@ defmodule Archethic.Contracts.Interpreter.CommonInterpreter do
end

# function call, should be placed after "for" prewalk
def prewalk(node = {{:atom, _}, _, args}, acc) when is_list(args), do: {node, acc}
def prewalk(node = {{:atom, function_name}, _, args}, acc = %{functions: functions})
when is_list(args) do
arity = length(args)

if FunctionKeys.exist?(functions, function_name, arity) do
{node, acc}
else
throw({:error, node, "The function #{function_name}/#{arity} does not exist"})
end
end

# blacklist rest
def prewalk(node, _acc), do: throw({:error, node, "unexpected term"})
Expand Down Expand Up @@ -285,14 +290,41 @@ defmodule Archethic.Contracts.Interpreter.CommonInterpreter do
{{:__block__, meta, expressions ++ new_expressions}, acc}
end

# common modules call
# Module function call
def postwalk(
node =
{{:., _meta, [{:__aliases__, _, [atom: _module_name]}, {:atom, _function_name}]}, _,
_args},
{{:., meta, [{:__aliases__, _, [atom: module_name]}, {:atom, function_name}]}, _, args},
acc
) do
{module_call(node), acc}
# Module and function has already been verified
module = Library.get_module!(module_name)
function = String.to_existing_atom(function_name)

# check the type of the args
unless module.check_types(function, args) do
throw({:error, node, "invalid function arguments"})
end

new_node =
if Library.function_tagged_with?(module_name, function_name, :write_contract) do
quote do
# mark the next_tx as dirty
Scope.update_global([:next_transaction_changed], fn _ -> true end)

# call the function with the next_transaction as the 1st argument
# and update it in the scope
Scope.update_global(
[:next_transaction],
&apply(unquote(module), unquote(function), [&1 | unquote(args)])
)
end
else
meta_with_alias = Keyword.put(meta, :alias, module)

{{:., meta, [{:__aliases__, meta_with_alias, [module]}, function]}, meta, args}
end

{new_node, acc}
end

# variable are read from scope
Expand Down Expand Up @@ -336,25 +368,13 @@ defmodule Archethic.Contracts.Interpreter.CommonInterpreter do
{new_node, acc}
end

def postwalk(node = {{:atom, function_name}, _, args}, acc = %{functions: functions})
when is_list(args) do
arity = length(args)

case Enum.find(functions, fn
{^function_name, ^arity, _} -> true
_ -> false
end) do
nil ->
throw({:error, node, "The function #{function_name}/#{arity} does not exist"})

{_, _, _} ->
new_node =
quote do
Scope.execute_function_ast(unquote(function_name), unquote(args))
end
def postwalk({{:atom, function_name}, _, args}, acc) when is_list(args) do
new_node =
quote do
Scope.execute_function_ast(unquote(function_name), unquote(args))
end

{new_node, acc}
end
{new_node, acc}
end

# BigInt mathematics to avoid floating point issues
Expand All @@ -369,65 +389,4 @@ defmodule Archethic.Contracts.Interpreter.CommonInterpreter do

# whitelist rest
def postwalk(node, acc), do: {node, acc}

def module_call(
node =
{{:., meta, [{:__aliases__, _, [atom: module_name]}, {:atom, function_name}]}, _, args}
)
when is_binary(module_name) do
{absolute_module_atom, _} =
case Library.get_module(module_name) do
{:ok, module_atom, module_atom_impl} -> {module_atom, module_atom_impl}
{:error, message} -> throw({:error, node, message})
end

# check function exists
unless Library.function_exists?(absolute_module_atom, function_name) do
throw({:error, node, "unknown function"})
end

# check function is available with given arity
unless Library.function_exists?(absolute_module_atom, function_name, length(args)) or
Library.function_exists?(absolute_module_atom, function_name, length(args) + 1) do
throw({:error, node, "invalid function arity"})
end

function_atom = String.to_existing_atom(function_name)

# check the type of the args
unless absolute_module_atom.check_types(function_atom, args) do
throw({:error, node, "invalid function arguments"})
end

new_node =
if module_name == "Contract" do
quote do
# mark the next_tx as dirty
Scope.update_global([:next_transaction_changed], fn _ -> true end)

# call the function with the next_transaction as the 1st argument
# and update it in the scope
Scope.update_global(
[:next_transaction],
&apply(unquote(absolute_module_atom), unquote(function_atom), [&1 | unquote(args)])
)
end
else
meta_with_alias = Keyword.put(meta, :alias, absolute_module_atom)

{{:., meta, [{:__aliases__, meta_with_alias, [absolute_module_atom]}, function_atom]},
meta, args}
end

new_node
end

# ----------------------------------------------------------------------
# _ _
# _ __ _ __(___ ____ _| |_ ___
# | '_ \| '__| \ \ / / _` | __/ _ \
# | |_) | | | |\ V | (_| | || __/
# | .__/|_| |_| \_/ \__,_|\__\___|
# |_|
# ----------------------------------------------------------------------
end
Loading