Make possible to put a function on stack
We would like to reduce size of generated code, so we want to implement a machinery, which would allow to put functions on stack and then call them several times.
To implement this, we are going to use lambda
instruction from Michelson.
The idea is pretty straightforward, but there are several obstacles to overcome.
First of all, we would like to make Indigo interface as simple as possible, ideally, to keep it as it's now. So, I suggest the following interface:
defNamedFunction1
:: (ToExpr arg, ScopeCodeGen a)
=> String
-> ((HasStorage st, HasSideEffects) => arg -> IndigoM a)
-> (arg -> IndigoM a)
defNamedFunction1 name body = \ex -> IndigoM $ Fun1Call name body ex
where defNamedFunction1
works only for unary functions, and Fun1Call
is a constructor of StatementF
.
It's implied, that universal function for multi arguments or several such functions for each arity will be implemented within this task, but let's leave this out of scope of this description.
This interface is almost the same as current one, so to define function you need to write something:
factorial :: Expr Int -> IndigoM (Expr Int)
factorial = defNamedFunction1 "factorial" $ do ... your function body is here ...
The only one detail, that the name of function is added, it will be explained later.
Calling of the function is equal to calling Haskell one: factorial n
.
The next issue: that implementation of the functionality has to correctly handle functions, which changes storage or make side effects. The idea is straightforward: two additional (implicit) arguments will be passed in lambda: current value of storage and current value of operations list, then returned back, and set back.
Let's consider the implementation of this functionality: before generation Lorentz code, we need to traverse over freer monad, look for Fun1Call/Fun2Call/Fun3Call
constructors, gather all pairs (name, body)
for all functions, and before generation of actual code, we need to put all found lambdas on stack. Now you see why we need this extra name parameter: we need to distinguish functions somehow (because we can meet several Fun*Call
, which will correspond to the same function). So basically, Fun*Call
combines two things: definition of a function and calling. So, as drawback we have repetition Fun*Call
constructors, corresponding to the same function name and body, in several places, where it was called, but we can neglect it.
The overhead of this approach is that we need to move storageб operations, lambda on the top of stack, wrap arguments of function to tuple, then call the lambda, then unwrap result, and then set new values of storage and operations.
Pretty big for small functions, but it can be reduced if we move not storage/operations, but lambda and function's arguments to the bottom of the stack, call dip n exec
, then move result back on top. But this seems complicate the initial version too much, so I think it can be implemented further (in a separate issue).
As result, I don't think this optimisation would improve situation with small functions, so we definitely should leave vanilla defFunction
operation.
UPD1 I've just realised the one more issue with this approach is that calling function func0
from func1
(both of them are lambdas) is huge pain in the ass, because func0
lambda should be passed to func1
lambda, and it's transitively :pepe_cry:.
There are several possible ways to tackle with it:
- prohibit it and throw a runtime exception (the worst one)
- inplace
func0
code (easiest one) - put on stack
func0
lambda infunc1
, if func0 is used more than once infunc1
(also easy) - put ALL lambdas in storage, consequently, any lambda will have access to every other (hardest one, not sure how implement it properly, if we need to modify storage type) I think I will go with the third option for initial implementation, it seems the best in sense of benefit/efforts