Skip to content

Commit

Permalink
Smart contract functions without args (#1181)
Browse files Browse the repository at this point in the history
* function parsing

* function parsing tests

* interpreter parse functions

* contract add functions to struct

* function_interpreter check if function exist

* get available functions and pass them to parsers

* add functions to scope

* add functions contract struct

* allow call to functions

* postwalk functions common_interpreter.ex

* remove functions postwalk function_interpreter.ex

* allow custom function call in action_interpreter.ex

* function call return value common_interpreter.ex

* function_interpreter.ex add constants in scope

* remove add_function as not used

* add possibility to pass existing custom functions to sanitize_parse_execute

* pass functions to action interpreter scope's

* move function prewalk to not catch "for var..."

* function_interpreter test parsing and execute

* create execute_function_ast for scope

* custom function matches check_types

* test function parsing in action_interpreter

* test function execution in action_interpreter

* format

* allow function calls in condition block

* fix function test

* test function in conditions

* add functions to condition constatns

* add functions test in interpreter

* fucntions key to string

* function_key function

* format

* case to if

* function_key to tuple

* add get_function_ast doc

* merge public_functions and private_functions into functions

* remove default functions_keys

* remove unused function

* move test to interpreter_test.exs

* format

* pass function_keys in condition_interpreter.ex's postwalk

* add function_key type

* fix spec and doc

* scope test

* Wrap single line function in ast block

* Fix @SPEC

* Rename get_functions to get_functions_keys

* Use String interpolation

---------

Co-authored-by: Neylix <[email protected]>
  • Loading branch information
herissondev and Neylix authored Jul 25, 2023
1 parent ba76e75 commit c5952b5
Show file tree
Hide file tree
Showing 13 changed files with 839 additions and 107 deletions.
16 changes: 12 additions & 4 deletions lib/archethic/contracts.ex
Original file line number Diff line number Diff line change
Expand Up @@ -258,27 +258,35 @@ defmodule Archethic.Contracts do

defp get_condition_constants(
:inherit,
%Contract{constants: %Constants{contract: contract_constant}},
%Contract{
constants: %Constants{contract: contract_constant},
functions: functions
},
transaction,
datetime
) do
%{
"previous" => contract_constant,
"next" => Constants.from_transaction(transaction),
"_time_now" => DateTime.to_unix(datetime)
"_time_now" => DateTime.to_unix(datetime),
"functions" => functions
}
end

defp get_condition_constants(
_,
%Contract{constants: %Constants{contract: contract_constant}},
%Contract{
constants: %Constants{contract: contract_constant},
functions: functions
},
transaction,
datetime
) do
%{
"transaction" => Constants.from_transaction(transaction),
"contract" => contract_constant,
"_time_now" => DateTime.to_unix(datetime)
"_time_now" => DateTime.to_unix(datetime),
"functions" => functions
}
end
end
25 changes: 25 additions & 0 deletions lib/archethic/contracts/contract.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ defmodule Archethic.Contracts.Contract do
alias Archethic.TransactionChain.TransactionData

defstruct triggers: %{},
functions: %{},
version: 0,
conditions: %{},
constants: %Constants{},
Expand Down Expand Up @@ -94,4 +95,28 @@ defmodule Archethic.Contracts.Contract do
) do
Map.update!(contract, :conditions, &Map.put(&1, condition_name, conditions))
end

@doc """
Add a public or private function to the contract
"""
@spec add_function(
contract :: t(),
function_name :: binary(),
ast :: any(),
args :: list(),
visibility :: atom()
) :: t()
def add_function(
contract = %__MODULE__{},
function_name,
ast,
args,
visibility
) do
Map.update!(
contract,
:functions,
&Map.put(&1, {function_name, length(args)}, %{args: args, ast: ast, visibility: visibility})
)
end
end
70 changes: 58 additions & 12 deletions lib/archethic/contracts/interpreter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ defmodule Archethic.Contracts.Interpreter do
alias __MODULE__.Legacy
alias __MODULE__.ActionInterpreter
alias __MODULE__.ConditionInterpreter
alias __MODULE__.FunctionInterpreter

alias __MODULE__.ConditionValidator

alias Archethic.Contracts.Contract
Expand All @@ -19,6 +21,7 @@ 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 @@ -117,7 +120,8 @@ defmodule Archethic.Contracts.Interpreter do
%Contract{
version: version,
triggers: triggers,
constants: %Constants{contract: contract_constants}
constants: %Constants{contract: contract_constants},
functions: functions
},
maybe_trigger_tx,
opts \\ []
Expand Down Expand Up @@ -150,7 +154,8 @@ defmodule Archethic.Contracts.Interpreter do
Constants.from_transaction(trigger_tx)
end,
"contract" => contract_constants,
"_time_now" => timestamp_now
"_time_now" => timestamp_now,
"functions" => functions
}

result =
Expand Down Expand Up @@ -291,7 +296,9 @@ defmodule Archethic.Contracts.Interpreter do
end

defp parse_contract(1, ast) do
case parse_ast_block(ast, %Contract{}) do
functions_keys = get_function_keys(ast)

case parse_ast_block(ast, %Contract{}, functions_keys) do
{:ok, contract} ->
{:ok, %{contract | version: 1}}

Expand All @@ -304,20 +311,20 @@ defmodule Archethic.Contracts.Interpreter do
{:error, "@version not supported"}
end

defp parse_ast_block([ast | rest], contract) do
case parse_ast(ast, contract) do
defp parse_ast_block([ast | rest], contract, functions_keys) do
case parse_ast(ast, contract, functions_keys) do
{:ok, contract} ->
parse_ast_block(rest, contract)
parse_ast_block(rest, contract, functions_keys)

{:error, _, _} = e ->
e
end
end

defp parse_ast_block([], contract), do: {:ok, contract}
defp parse_ast_block([], contract, _), do: {:ok, contract}

defp parse_ast(ast = {{:atom, "condition"}, _, _}, contract) do
case ConditionInterpreter.parse(ast) do
defp parse_ast(ast = {{:atom, "condition"}, _, _}, contract, functions_keys) do
case ConditionInterpreter.parse(ast, functions_keys) do
{:ok, condition_type, condition} ->
{:ok, Contract.add_condition(contract, condition_type, condition)}

Expand All @@ -326,8 +333,8 @@ defmodule Archethic.Contracts.Interpreter do
end
end

defp parse_ast(ast = {{:atom, "actions"}, _, _}, contract) do
case ActionInterpreter.parse(ast) do
defp parse_ast(ast = {{:atom, "actions"}, _, _}, contract, functions_keys) do
case ActionInterpreter.parse(ast, functions_keys) do
{:ok, trigger_type, actions} ->
{:ok, Contract.add_trigger(contract, trigger_type, actions)}

Expand All @@ -336,7 +343,31 @@ defmodule Archethic.Contracts.Interpreter do
end
end

defp parse_ast(ast, _), do: {:error, ast, "unexpected term"}
defp parse_ast(
ast = {{:atom, "export"}, _, [{{:atom, "fun"}, _, _} | _]},
contract,
functions_keys
) do
case FunctionInterpreter.parse(ast, functions_keys) do
{:ok, function_name, args, ast} ->
{:ok, Contract.add_function(contract, function_name, ast, args, :public)}

{:error, _, _} = e ->
e
end
end

defp parse_ast(ast = {{:atom, "fun"}, _, _}, contract, functions_keys) do
case FunctionInterpreter.parse(ast, functions_keys) do
{:ok, function_name, args, ast} ->
{:ok, Contract.add_function(contract, function_name, ast, args, :private)}

{:error, _, _} = e ->
e
end
end

defp parse_ast(ast, _, _), do: {:error, ast, "unexpected term"}

defp time_now(:transaction, %Transaction{
validation_stamp: %ValidationStamp{timestamp: timestamp}
Expand All @@ -358,6 +389,21 @@ 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)} | get_function_keys(rest)]
end

defp get_function_keys([
{{:atom, "export"}, _,
[{{:atom, "fun"}, _, [{{:atom, function_name}, _, args} | _]} | _]}
| rest
]) do
[{function_name, length(args)} | get_function_keys(rest)]
end

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

# -----------------------------------------
# contract validation
# -----------------------------------------
Expand Down
23 changes: 13 additions & 10 deletions lib/archethic/contracts/interpreter/action_interpreter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreter do
alias Archethic.TransactionChain.TransactionData

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.Library
Expand All @@ -13,13 +14,14 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreter do
@doc """
Parse the given node and return the trigger and the actions block.
"""
@spec parse(any()) :: {:ok, atom(), any()} | {:error, any(), String.t()}
def parse({{:atom, "actions"}, _, [keyword, [do: block]]}) do
@spec parse(any(), list(Interpreter.function_key())) ::
{:ok, atom(), any()} | {:error, any(), String.t()}
def parse({{:atom, "actions"}, _, [keyword, [do: block]]}, functions_keys) do
trigger_type = extract_trigger(keyword)

# We only parse the do..end block with the macro.traverse
# this help us keep a clean accumulator that is used only for scoping.
actions_ast = parse_block(AST.wrap_in_block(block))
actions_ast = parse_block(AST.wrap_in_block(block), functions_keys)

{:ok, trigger_type, actions_ast}
catch
Expand All @@ -30,7 +32,7 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreter do
{:error, node, reason}
end

def parse(node) do
def parse(node, _) do
{:error, node, "unexpected term"}
end

Expand Down Expand Up @@ -127,7 +129,7 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreter do
throw({:error, node, "Invalid trigger"})
end

defp parse_block(ast) do
defp parse_block(ast, functions_keys) do
# here the accumulator is an list of parent scopes & current scope
# where we can access variables from all of them
# `acc = [ref1]` means read variable from scope.ref1 or scope
Expand All @@ -142,7 +144,7 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreter do
prewalk(node, acc)
end,
fn node, acc ->
postwalk(node, acc)
postwalk(node, acc, functions_keys)
end
)

Expand Down Expand Up @@ -187,7 +189,8 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreter do
defp postwalk(
node =
{{:., _meta, [{:__aliases__, _, [atom: "Contract"]}, {:atom, function_name}]}, _, args},
acc
acc,
_
) do
absolute_module_atom = Archethic.Contracts.Interpreter.Library.Contract

Expand All @@ -204,7 +207,7 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreter do

function_atom = String.to_existing_atom(function_name)

# check the type of the args
# 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
Expand All @@ -226,8 +229,8 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreter do
end

# --------------- catch all -------------------
defp postwalk(node, acc) do
CommonInterpreter.postwalk(node, acc)
defp postwalk(node, acc, functions_keys) do
CommonInterpreter.postwalk(node, acc, functions_keys)
end

# keep only the transaction fields we are interested in
Expand Down
35 changes: 29 additions & 6 deletions lib/archethic/contracts/interpreter/common_interpreter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,9 @@ defmodule Archethic.Contracts.Interpreter.CommonInterpreter do
{ast, acc}
end

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

# log (not documented, only useful for developer debugging)
# will soon be updated to log into the playground console
def prewalk(_node = {{:atom, "log"}, _, [data]}, acc) do
Expand All @@ -274,7 +277,8 @@ defmodule Archethic.Contracts.Interpreter.CommonInterpreter do
# exit block == set parent scope
def postwalk(
node = {:__block__, _, _},
acc
acc,
_
) do
{node, List.delete_at(acc, -1)}
end
Expand All @@ -283,7 +287,8 @@ defmodule Archethic.Contracts.Interpreter.CommonInterpreter do
def postwalk(
node =
{{:., meta, [{:__aliases__, _, [atom: module_name]}, {:atom, function_name}]}, _, args},
acc
acc,
_
)
when module_name in @modules_whitelisted do
absolute_module_atom =
Expand All @@ -307,6 +312,7 @@ defmodule Archethic.Contracts.Interpreter.CommonInterpreter do
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
Expand All @@ -322,7 +328,8 @@ defmodule Archethic.Contracts.Interpreter.CommonInterpreter do
# variable are read from scope
def postwalk(
_node = {{:atom, var_name}, _, nil},
acc
acc,
_
) do
new_node =
quote do
Expand All @@ -340,7 +347,8 @@ defmodule Archethic.Contracts.Interpreter.CommonInterpreter do
{:%{}, _, [{var_name, list}]},
[do: block]
]},
acc
acc,
_
) do
# FIXME: here acc is already the parent acc, it is not the acc of the do block
# FIXME: this means that our `var_name` will live in the parent scope
Expand All @@ -360,8 +368,23 @@ defmodule Archethic.Contracts.Interpreter.CommonInterpreter do
{new_node, acc}
end

def postwalk(node = {{:atom, function_name}, _, args}, acc, function_keys) when is_list(args) do
if Enum.member?(function_keys, {function_name, length(args)}) do
new_node =
quote do
Scope.execute_function_ast(unquote(function_name), unquote(args))
end

{new_node, acc}
else
reason = "The function #{function_name}/#{Integer.to_string(length(args))} does not exist"

throw({:error, node, reason})
end
end

# BigInt mathematics to avoid floating point issues
def postwalk(_node = {ast, meta, [lhs, rhs]}, acc) when ast in [:*, :/, :+, :-] do
def postwalk(_node = {ast, meta, [lhs, rhs]}, acc, _) when ast in [:*, :/, :+, :-] do
new_node =
quote line: Keyword.fetch!(meta, :line) do
AST.decimal_arithmetic(unquote(ast), unquote(lhs), unquote(rhs))
Expand All @@ -371,7 +394,7 @@ defmodule Archethic.Contracts.Interpreter.CommonInterpreter do
end

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

# ----------------------------------------------------------------------
# _ _
Expand Down
Loading

0 comments on commit c5952b5

Please sign in to comment.