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 6fc81df
Show file tree
Hide file tree
Showing 9 changed files with 134 additions and 135 deletions.
64 changes: 0 additions & 64 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 @@ -177,61 +168,6 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreter do
# | .__/ \___/|___/\__| \_/\_/ \__,_|_|_|\_\
# |_|
# ----------------------------------------------------------------------
# handle the Contract module
# 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

# # handle modules whitelisted (but not Contract)
# defp postwalk(
# node =
# {{:., _meta, [{:__aliases__, _, [atom: module_name]}, {:atom, _function_name}]}, _,
# _args},
# acc
# )
# when module_name in @modules_whitelisted do
# {CommonInterpreter.module_call(node, common: false), acc}
# end

# --------------- catch all -------------------
defp postwalk(node, acc) do
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
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,20 @@ defmodule Archethic.Contracts.Interpreter.ConditionInterpreterTest do
assert is_tuple(ast) && :ok == Macro.validate(ast)
end

test "should not parse :write_contract functions" do
code = ~s"""
condition transaction: [
uco_transfers: Contract.set_content "content"
]
"""

assert {:error, _, _} =
code
|> Interpreter.sanitize_code()
|> elem(1)
|> ConditionInterpreter.parse([])
end

test "parse custom functions" do
code = ~s"""
condition transaction: [
Expand Down
Loading

0 comments on commit 6fc81df

Please sign in to comment.