dasylang / dasy

a lisp built on top of vyper
51 stars 2 forks source link

reimplement visibility decorators as functions #10

Open z80dev opened 1 year ago

z80dev commented 1 year ago

Currently, defn takes the form of:

(defn <fn-name> <fn-args> [<return-type>] <decorator(s)> &<body>)

Where:

Some examples:

(defn __init__ [] :external
    (setv self/greet "Hello World"))

(defn multiply [:uint256 x y] :uint256 [:external :pure]
  (* x y))

(defn addAndSub [:uint256 x y] '(:uint256 :uint256) [:external :pure]
  '((+ x y) (- x y)))

I think having the decorators so far from the function name isn't great. This leads to the first line (declaration line, before the body) to grow pretty long, but breaking it up into multiple lines can get confusing as these elements start to look like they're part of the function body

Some options are implementing decorators as functions

(external 
  (defn __init__ []
    (setv self/greet "Hello World")))

This could in theory allow multiple functions to be defined under the same external block. This might not be a bad pattern, having all the external functions together, but decorators like :view or :pure would still have to go in the defn form, or the nesting will get too ugly

(external 
  (defn __init__ []
    (setv self/greet "Hello World"))
  (defn multiply [:uint256 x y] :uint256 :pure
    (* x y)))

The gains here are not insignificant. we separate the concept of "accessibility decorators" (:external, :internal) from "state modifiers" (:pure, :view). We'll lump in :payable with the "state decorators", mainly because if a function is :payable then it can't be :pure or :view, so we'll never have to have a list of decorators.

Then, we wouldn't really need :internal either if we just set functions to be internal by default (i.e. internal unless explicitly marked external)

New defn specification

A valid defn form would then be:

(defn <fn-name> <fn-args> [<return-type>] [<state-decorator>] &<body>)

Implementation as a macro

The coolest thing about this is we can make this change in a backwards-compatible way by just writing an external macro. This macro does nothing more than take any defn forms passed to it, add an :external decorator according to the current syntax, and output that form.

The road to public

In solidity, a function can be marked public, which makes it accessible both internally and externally.

We can also easily implement public, but not as a macro, because it would require processing code outside of the macro body. But the flow would go like this:

  1. Declare an internal function with the function body and a prepend the name with an underscore
  2. Replace internal invocations of the function with the underscore-prepended invocation
  3. Declare an external function which passes through to the internal function

such that:

(public
  (defn foo [] :uint256 :view
    (+ 3 7)))

(defn internal-user [] :uint256
  (- (self.foo) 2))

is transformed into:

(defn _foo [] :uint256 :view
  (+ 3 7))

(defn internal-user [] :uint256
  (- (self._foo) 2))

(external
  (defn foo [] :uint256 :view
    (self._foo)))