Skip to content

Commit

Permalink
move module call to common interpreter
Browse files Browse the repository at this point in the history
  • Loading branch information
herissondev committed Aug 9, 2023
1 parent 46a4f33 commit 9a91516
Show file tree
Hide file tree
Showing 9 changed files with 174 additions and 120 deletions.
89 changes: 40 additions & 49 deletions lib/archethic/contracts/interpreter/action_interpreter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreter do
alias Archethic.Contracts.Interpreter
alias Archethic.Contracts.Interpreter.ASTHelper, as: AST
alias Archethic.Contracts.Interpreter.CommonInterpreter
alias Archethic.Contracts.Interpreter.Library
alias Archethic.Contracts.Interpreter.Scope

# # Module `Contract` is handled differently
Expand Down Expand Up @@ -149,14 +148,6 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreter do
# | .__/|_| \___| \_/\_/ \__,_|_|_|\_\
# |_|
# ----------------------------------------------------------------------
# autorize the use of Contract module
defp prewalk(
node = {:__aliases__, _, [atom: "Contract"]},
acc
) do
{node, acc}
end

# # autorize the use of modules whitelisted
# defp prewalk(node = {:__aliases__, _, [atom: module_name]}, acc)
# when module_name in @modules_whitelisted,
Expand All @@ -181,46 +172,46 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreter do
# here we have 2 things to do:
# - feed the `next_transaction` as the 1st function parameter
# - update the `next_transaction` in scope
defp postwalk(
node =
{{:., _meta, [{:__aliases__, _, [atom: "Contract"]}, {:atom, function_name}]}, _, args},
acc
) do
absolute_module_atom = Archethic.Contracts.Interpreter.Library.Contract

# 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
# (we add 1 to arity because we add the contract as 1st argument implicitely)
unless 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, and allow custom function call as args
unless absolute_module_atom.check_types(function_atom, args) do
throw({:error, node, "invalid function arguments"})
end

new_node =
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

{new_node, acc}
end
# defp postwalk(
# node =
# {{:., _meta, [{:__aliases__, _, [atom: "Contract"]}, {:atom, function_name}]}, _, args},
# acc
# ) do
# absolute_module_atom = Archethic.Contracts.Interpreter.Library.Common.Contract
#
# # 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
# # (we add 1 to arity because we add the contract as 1st argument implicitely)
# unless 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, and allow custom function call as args
# unless absolute_module_atom.check_types(function_atom, args) do
# throw({:error, node, "invalid function arguments"})
# end
#
# new_node =
# 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
#
# {new_node, acc}
# end

# # handle modules whitelisted (but not Contract)
# defp postwalk(
Expand Down
53 changes: 32 additions & 21 deletions lib/archethic/contracts/interpreter/common_interpreter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ defmodule Archethic.Contracts.Interpreter.CommonInterpreter do
alias Archethic.Contracts.Interpreter.Library
alias Archethic.Contracts.Interpreter.Scope

@modules_whitelisted Library.list_common_modules()

# ----------------------------------------------------------------------
# _ _
# _ __ _ __ _____ ____ _| | | __
Expand Down Expand Up @@ -96,9 +94,8 @@ defmodule Archethic.Contracts.Interpreter.CommonInterpreter do
def prewalk(node = {:., _, [{:__aliases__, _, _}, _]}, acc), do: {node, acc}

# whitelisted modules
def prewalk(node = {:__aliases__, _, [atom: module_name]}, acc)
when module_name in @modules_whitelisted,
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 @@ -290,11 +287,10 @@ defmodule Archethic.Contracts.Interpreter.CommonInterpreter do
# common modules call
def postwalk(
node =
{{:., _meta, [{:__aliases__, _, [atom: module_name]}, {:atom, _function_name}]}, _,
{{:., _meta, [{:__aliases__, _, [atom: _module_name]}, {:atom, _function_name}]}, _,
_args},
acc
)
when module_name in @modules_whitelisted do
) do
{module_call(node), acc}
end

Expand Down Expand Up @@ -375,25 +371,23 @@ defmodule Archethic.Contracts.Interpreter.CommonInterpreter do

def module_call(
node =
{{:., meta, [{:__aliases__, _, [atom: module_name]}, {:atom, function_name}]}, _, args},
opts \\ []
) do
absolute_module_name =
if Keyword.get(opts, :common, true) do
"Elixir.Archethic.Contracts.Interpreter.Library.Common.#{module_name}"
else
"Elixir.Archethic.Contracts.Interpreter.Library.#{module_name}"
{{:., 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

absolute_module_atom = Code.ensure_loaded!(String.to_existing_atom(absolute_module_name))

# 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)) do
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

Expand All @@ -405,9 +399,26 @@ defmodule Archethic.Contracts.Interpreter.CommonInterpreter do
throw({:error, node, "invalid function arguments"})
end

meta_with_alias = Keyword.put(meta, :alias, absolute_module_atom)
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, [module_atom]}, function_atom]}, meta, args}
end

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

# ----------------------------------------------------------------------
Expand Down
34 changes: 27 additions & 7 deletions lib/archethic/contracts/interpreter/condition_interpreter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ defmodule Archethic.Contracts.Interpreter.ConditionInterpreter do
alias Archethic.Contracts.Interpreter.ASTHelper, as: AST
alias Archethic.Contracts.Interpreter

@modules_whitelisted Library.list_common_modules()
@condition_fields Conditions.__struct__()
|> Map.keys()
|> Enum.reject(&(&1 == :__struct__))
Expand Down Expand Up @@ -125,6 +124,27 @@ defmodule Archethic.Contracts.Interpreter.ConditionInterpreter do
# | .__/|_| \___| \_/\_/ \__,_|_|_|\_\
# |_|
# ----------------------------------------------------------------------

defp prewalk(
_subject,
node =
{{:., _meta, [{:__aliases__, _, [atom: module_name]}, {:atom, function_name}]}, _, _},
acc
) do
{_absolute_module_atom, module_impl} =
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

function_atom = String.to_existing_atom(function_name)

if module_impl.tagged_with?(function_atom, :write_contract),
do: throw({:error, node, "Write contract functions are not allowed in condition block"})

CommonInterpreter.prewalk(node, acc)
end

defp prewalk(_subject, node, acc) do
CommonInterpreter.prewalk(node, acc)
end
Expand Down Expand Up @@ -182,15 +202,15 @@ defmodule Archethic.Contracts.Interpreter.ConditionInterpreter do
node =
{{:., meta, [{:__aliases__, _, [atom: module_name]}, {:atom, function_name}]}, _, args},
acc
)
when module_name in @modules_whitelisted do
) do
# if function exist with arity => node
arity = length(args)

absolute_module_atom =
String.to_existing_atom(
"Elixir.Archethic.Contracts.Interpreter.Library.Common.#{module_name}"
)
{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

new_node =
cond do
Expand Down
52 changes: 17 additions & 35 deletions lib/archethic/contracts/interpreter/function_interpreter.ex
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
defmodule Archethic.Contracts.Interpreter.FunctionInterpreter do
@moduledoc false
alias Archethic.Contracts.Interpreter.Library
alias Archethic.Contracts.Interpreter
alias Archethic.Contracts.Interpreter.ASTHelper, as: AST
alias Archethic.Contracts.Interpreter.Scope
Expand Down Expand Up @@ -97,49 +98,30 @@ defmodule Archethic.Contracts.Interpreter.FunctionInterpreter do
# | .__/|_| \___| \_/\_/ \__,_|_|_|\_\
# |_|
# ----------------------------------------------------------------------
# Ban access to Contract module
defp prewalk(
node =
{{:., _meta, [{:__aliases__, _, [atom: "Contract"]}, {:atom, function_name}]}, _, _},
{{:., _meta, [{:__aliases__, _, [atom: module_name]}, {:atom, function_name}]}, _, _},
acc,
_visibility
is_internal?
) do
absolute_module_atom =
Code.ensure_loaded!(
String.to_existing_atom("Elixir.Archethic.Contracts.Interpreter.Library.Contract")
)
{_absolute_module_atom, module_impl} =
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

if absolute_module_atom.tagged_with?(String.to_atom(function_name), :write_contract) do
throw({:error, node, "Write contract functions are not allowed in custom functions"})
end
function_atom = String.to_existing_atom(function_name)

CommonInterpreter.prewalk(node, acc)
end
if is_internal? do
if module_impl.tagged_with?(function_atom, :io),
do: throw({:error, node, "IO function calls not allowed in public functions"})

defp prewalk(
node =
{{:., _meta, [{:__aliases__, _, [atom: module_name]}, {:atom, function_name}]}, _, _},
acc,
true
) do
absolute_module_atom =
Code.ensure_loaded!(
String.to_existing_atom(
"Elixir.Archethic.Contracts.Interpreter.Library.Common.#{module_name}"
)
)

absolute_module_atom =
try do
%Knigge.Options{default: module} = Knigge.options!(absolute_module_atom)
module
rescue
_ ->
absolute_module_atom
if module_impl.tagged_with?(function_atom, :write_contract),
do: throw({:error, node, "Write contract functions are not allowed in custom functions"})
else
if module_impl.tagged_with?(function_atom, :write_contract) do
throw({:error, node, "Write contract functions are not allowed in custom functions"})
end

if absolute_module_atom.tagged_with?(String.to_atom(function_name), :io) do
throw({:error, node, "IO function calls not allowed in public functions"})
end

CommonInterpreter.prewalk(node, acc)
Expand Down
34 changes: 28 additions & 6 deletions lib/archethic/contracts/interpreter/library.ex
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,35 @@ defmodule Archethic.Contracts.Interpreter.Library do
end

@doc """
Returns the list of common modules available.
This function is also used to create the atoms of the modules
Gets a module and its imlpementation when there is one
"""
def list_common_modules() do
[:Map, :List, :Regex, :Json, :Time, :Chain, :Crypto, :Token, :String, :Code, :Http]
|> Enum.map(&Atom.to_string/1)
@spec get_module(binary()) :: {:ok, module(), module()} | {:error, binary()}
def get_module(module_name) do
try do
case Code.ensure_loaded(
String.to_existing_atom(
"Elixir.Archethic.Contracts.Interpreter.Library.Common.#{module_name}"
)
) do
{:module, module_atom} ->
module_atom_impl =
try do
%Knigge.Options{default: module} = Knigge.options!(module_atom)
module
rescue
_ ->
module_atom
end

{:ok, module_atom, module_atom_impl}

_ ->
{:error, "Module #{module_name} not found"}
end
rescue
_ ->
{:error, "Module #{module_name} not found"}
end
end

# ----------------------------------------
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
defmodule Archethic.Contracts.Interpreter.Library.Contract do
defmodule Archethic.Contracts.Interpreter.Library.Common.Contract do
@moduledoc """
We are delegating to the legacy transaction statements.
This is fine as long as we don't need to change anything.
Expand Down
Loading

0 comments on commit 9a91516

Please sign in to comment.