rainlanguage / dotrain

.rain to rainlang composer and rain language server protocol services
3 stars 4 forks source link

ability to rebind quotes #91

Closed thedavidmeister closed 7 months ago

thedavidmeister commented 7 months ago

motivation

So previously we discussed wanting to be able to (re)bind RHS items with dotrain

actually there's even a poorly defined idea of a "rainlang fragment" in the dotrain spec https://github.com/rainlanguage/specs/blob/main/dotrain.md#rainlang-fragment

at a high level, we want to be able to rebind stuff other than just literals.

Say for example, we have some trading strat written in rainlang that has a ratio that represents a multiplier that we apply to our price and/or amount, maybe to respond to some kind of trend in the market or something.

A reasonable workflow is something like:

  1. Define a binding in the dotrain file for the ratio, elided with a nice description
  2. Write the strat that uses the binding for all its internal logic
  3. Do some kind of monte carlo analysis over the ratio to make nice simulations and charts, by rebinding and evaling locally
  4. To go to production, rebind the ratio to the logic that will calculate the real ratios onchain, e.g. it could pull from a uni v3 twap or similar

Steps [0-2] all work great already, we can simulate things with native rainlang using local evm environments, without the need for complex test harness logic like needing to mock underlying contracts.

This is a very haskell-like approach, lifting everything that is impure to the entrypoint, we don't formally enforce it anywhere but it's a simple pattern that allows dotrain composition to also be a native replacement for the need to mock externalities.

In the future we might have a rainlang native alternative - https://github.com/rainlanguage/rain.interpreter/issues/137 - but for now the pattern of rebinding and composing is totally workable and better than needing to use foundry/hardhat for mocking or modelling in separate systems (e.g. python). Remember that the ultimate goal is that everyone can do everything they need to do by only learning rainlang and using rainlang native tooling.

Unfortunately step 3. doesn't actually work currently, in dotrain you can't rebind #ratio !the ratio to #ratio uniswap-v3-twap-output-ratio( ... ) because non-literal bindings are not supported.

The "obvious solution" and why it is not so obvious

Ok so we can just allow binding to RHS items, right?

Actually this is what I was thinking we'd do for a while, but thinking about it more (and realising a potential alternative, detailed below), it's problematic.

Macrology is subtle to implement and often hard to use

Basically this suggestion introduces some gnarly problems native to macros in other languages, such as macro hygiene https://en.wikipedia.org/wiki/Hygienic_macro

If we are rebinding things that reference other things then the lexical nature of these references is important.

Consider some binding:

---
#foo add(a b)

What exactly are a and b? is it expected that there exists some defined a and b already in scope wherever the binding is realised? If so, doesn't this make composition and re-use quite difficult, having to match the names of everything everywhere? If not, where do they come from?

Multi-output RHS items are awkward to bind

Consider some dotrain with hypothetical RHS binding:

---
#foo word-with-two-outputs(1)

#bar
a b: foo;

This is subjective, but it's kinda awkward to read imo.

Worse, the rebinding of literals for the sake of testing would have to match the outputs.

---
#foo 1 2

#bar
a b: foo;

or in cli form --bind 'foo=1 2'

It's weird now because a single "thing" in the rhs as foo is actually two different literal values once the binding resolves.

Alternative approach

What we could do is allow quotes to be bound, which might give us just enough indirection to do what we want (i.e. dev/prod parity with a pure rebinding based workflow).

Consider the following in the current state of dotrain, to test then deploy a simple ratio value.

---
#ratio !The ratio that main will return

#main-test
_: ratio;

#main
_: call<'the-real-ratio-logic 1>();

#the-real-ratio-logic
...

We need (at least) two different entrypoints, #main-test and #main so that we can use the rebindable version of the code, and the version of the code that has the production ratio logic in it. Presumably #the-real-ratio-logic itself is tested somehow, for the sake of the example we're just looking at the relationship between rebinding and shipping to production.

Here is how the same thing above could look if we could rebind quotes

---
#test-ratio !The ratio that main will return.
#ratio-binding !The binding to use to calculate the ratio.

#main
_: call<ratio-binding 1>();

#always-test-ratio
_: ratio;

#real-ratio-logic
...

In this case, we replace the need to ship a different entrypoint to production with the ability to ship the same entrypoint with a different rebinding. Specifically, we rebind the call in #main to either 'always-test-ratio or 'real-ratio-logic

In yaml form this would be a test case (I think the single quote needs escaping in yaml):

bind:
  - test-ratio: 5e18
  - ratio-binding \'always-test-ratio
---
#test-ratio !The ratio that main will return.
#ratio-binding !The binding to use to calculate the ratio.

#main
_: call<ratio-binding 1>();

#always-test-ratio
_: ratio;

#real-ratio-logic
...

Then the production deployment looks like:

bind:
  - ratio-binding \'real-ratio-logic
---
#test-ratio !The ratio that main will return.
#ratio-binding !The binding to use to calculate the ratio.

#main
_: call<ratio-binding 1>();

#always-test-ratio
_: test-ratio;

#real-ratio-logic
...

Note that the strat itself doesn't need to change, nor do we need redundant entrypoint definitions, we just modify the quote that ends up in the call in the main entrypoint, to change the subsequent dispatch.

This avoids the issues with RHS item binding because:

Things NOT in scope

It seems obvious that a shallow implementation of the above should be relatively easy to implement. We substitute the binding for the quote, then resolve quotes.

As long as this substitution happens before quote resolution, everything works as normal.

A deep implementation that requires resolving quotes in order to substitute them could be much more complex, and it's not needed.

Let's keep

out of scope