convex.core.core.cvx Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of convex-core Show documentation
Show all versions of convex-core Show documentation
Convex core libraries and common utilities
The newest version!
;;
;;
;; Core definitions executed as part of core runtime environment bootstrap, supplementing utilities defined in Java.
;;
;; First are defined core building blocks which must be kept at the beginning of the file. Order matters!
;; Then is defined the rest of the API in alphabetical order.
;;
;;
;;;;;;;;;; Values
(def *lang*
^{:doc {:description ["Advanced feature. Language front-end function."
"If set to a function via `def`, will be called with the code for each transaction instead of delegating to normal `eval` behavior."
"Pre-compiled operations (see `compile`) bypass this language setting."]
:examples [{:code "(def *lang* (fn [trx] (str trx)))"}]}}
nil)
(def *registry*
^{:doc {:description "Address of the Convex registry actor."
:examples [{:code "(call *registry* (register {:name \"My name\"}))"}]}}
(address 9))
;;;;;;;;;; Convex Lisp quasiquote impl
(def quasiquote*)
(def qq*)
(def quasiquote
^{:doc {:description "Returns the quoted value of a form, without evaluating it. Like `quote`, but elements within the form may be unquoted via `unquote`."
:examples [{:code "(quasiquote foo)" :return "foo"}
{:code "(quasiquote (:a :b (unquote (+ 2 3))))" :return "(:a :b 5)"}]
:signature [{:params [form]}]}
:expander true}
(fn [[_ form] e]
(tailcall* e (qq* form 1) e)))
;; private helper, always produces a generator for a form
(def ^:private qq* (fn [form depth]
(let [qf (quasiquote* form depth)]
(cond
qf qf
(list 'quote
form)
))))
;; quasiquote elements of sequence, produce a sequence of generators or nil if "pure" quotation
;; nil production is an important optimisation to avoid regenerating static subtrees
(def ^:private qq-seq (fn
[ss depth]
(let [n (count ss)] ;; size of sequence]
(loop [i 0
found false
ss ss]
(cond (< i n)
(let [v (nth ss i)
e (quasiquote* v depth)]
(cond (nil? e)
(set! ss (assoc ss i (list 'quote
v))) ;; we need these if a generator is required
(do
(set! found true)
(set! ss (assoc ss i e))))
(recur (inc i) found ss))
(cond found ss nil) ;; end of loop
)))))
;; Quasiquote expand function, returns nil if no change (i.e. a pure quotable form), otherwise a generator for the form
(def quasiquote* (fn [form depth]
(cond
;; first catch [] () {} #{} and nil, which dont expand
(empty? form) (return nil)
;; handle each type of data structure
(list? form)
(let [fst (first form)]
(cond
;; Nested quasiquote, needs to increase depth
(= 'quasiquote fst
)
(cond
(!= 2 (count form)) (fail :EXPAND "nested quasiquote requires 1 argument")
(let [snd (second form)
ev (quasiquote* snd (inc depth))]
(cond
(nil? ev) (return nil)
(list 'list
(quote 'quasiquote
)
ev))))
;; unquote unwraps one level of quasiquote
(= 'unquote
fst)
(cond
(!= 2 (count form)) (fail :EXPAND "unquote requires 1 argument")
(let [snd (second form)]
(cond
(> depth 1)
(let [ev (quasiquote* snd (dec depth))]
(cond (nil? ev) (return nil))
(list 'list
(quote 'unquote
)
ev))
(nil? snd) (compile nil) ;; special case, generator for nil
snd
)))
(let [es (qq-seq form depth)]
(cond (nil? es) (return nil))
(cons 'list
es))))
(vector? form)
(let [es (qq-seq (vec form) depth)]
(cond (nil? es) (return nil))
(vec es))
(set? form)
(let [es (qq-seq (vec form) depth)]
(cond
(nil? es) (return nil)
(cons 'list
(quote 'hash-set
) es)))
(map? form)
(let [es (qq-seq (apply concat (vec form)) depth)]
(cond
(nil? es) (return nil)
(cons 'list
(quote 'hash-map
) es)))
;; Nothing special possible, so just return nil to signal no change
nil)))
;; Test expansion
`[1 ~(inc 2)]
;;;;;;;;;; Expanders, creating macros and defining functions
;; TODO. Review expanders and `macro`, API is not clear. + macros cannot be used within the transaction where they are created
(def defexpander
^{:doc {:description "Advanced feature. Defines an expander in the current environment."
:examples [{:code "(defexpander expand-once [x e] (e x identity))"}]
:signature [{:params [x e]}]}
:expander true}
(fn [x e]
(let [[_ name & decl] x
exp (cons 'fn
decl)
form `(def ~(syntax name {:expander true})
~exp)]
(tailcall* e form e)))) ;; Note: tailcall macro not yet defined, so use core runtime function directly
(def defmacro
^{:doc {:description ["Like `defn` but defines a macro instead of a regular function."
"A macro is a special function that is executed at expansion, before compilation, and produces valid Convex Lisp code for subsequent execution."]
:signature [{:params [name params & body]}]}
:expander true}
(fn [x e]
(let [[_ name & decl] x
mac (cons 'fn
decl)
form `(def ~(syntax name
(assoc (meta (first decl))
:expander true)) ;; merge in metadata on parameter plus :expander tag
(let [m# ~mac] ; This is an optimisation to construct the macro function only once
(fn [x e]
(tailcall* e (apply m#
(next x)) ; Apply macro function to the macro arguments
e))))]
(tailcall* e form e))))
(defmacro macro
^{:doc {:description "Creates an anonymous macro function, suitable for use as an expander."
:examples [{:code "(macro [x] (if x :foo :bar))"}]
:signature [{:params [params & body]}]}}
[& decl]
(let [mfunc (cons 'fn
decl)]
`(let [m# ~mfunc] ;; set up a closure containing the macro function
(fn [x e]
(tailcall* e
(apply m# (next x))
e)))))
(defmacro define
^{:doc {:description "Defines a value in the current environment, like def, but at expand time."
:examples [{:code "(define cols [:red :green])"}]
:signature [{:params [name]}
{:params [name vale]}]}}
([name]
(eval `(def ~name)))
([name value]
(eval `(def ~name ~value))))
(defmacro defn
^{:doc {:description "Defines a function in the current environment."
:examples [{:code "(defn my-square [x] (* x x))"}]
:signature [{:params [name params & body]}
{:params [name & fn-decls]}]}}
[name & decl]
(cond
(empty? decl) (fail :ARITY "`defn` requires at least one function definition"))
(let [fnform (cons 'fn
decl)
nf (syntax name (meta (first decl)))] ;; Note: merges in metadata on parameter list
(eval `(def ~name)) ;; ensure we have a forward declaration at expand time
`(def ~nf ~fnform)))
(defn identity
^{:doc {:description "An identity function which returns its first argument unchanged."
:examples [{:code "(identity :foo)"}
{:code "(map identity [1 2 3])"}]
:signature [{:params [x]}]}}
[x & _]
x)
(defn expand-1
^{:doc {:description "Expands a form once."
:examples [{:code "(expand-1 '(or 1 2 3))"}]
:signature [{:params [x]}]}}
([x]
(expand x *initial-expander* identity))
([x e]
(expand x e identity)))
;;;;;;;;;; Environment setup
(defmacro declare
^{:doc {:description "Declares symbols in the current environment. Used for forward declarations."
:examples [{:code "(declare forward-decl)"}]
:signature [{:params [& syms]
:return Nil}]}}
[& syms]
(map
(fn [sym]
(cond (symbol? sym)
(eval `(def ~sym))
(fail :CAST "declare requires symbols only as arguments")))
syms)
nil)
;;;;;;;;;; Logic Operations
(defmacro and
^{:doc {:description ["Executes expressions in sequence, returning the first falsey value (false or nil), or the last value otherwise."
"Does not evaluate later expressions, so can be used to short circuit execution."
"Returns true with no expressions present."]
:examples [{:code "(and (< 1 2) :last)"}]
:signature [{:params [& exprs]}]}}
[& exprs]
(let [n (count exprs)]
(cond
(== n 0) true
(== n 1) (first exprs)
:else `(let [v# ~(first exprs)]
(cond v#
~(cons 'and
(next exprs))
v#)))))
(defmacro or
^{:doc {:description ["Executes expressions in sequence, returning the first truthy value, or the last value if all were falsey (false or nil)."
"Does not evaluate later expressions, so can be used to short circuit execution."
"Returns nil with no expressions present."]
:examples [{:code "(or nil 1)"}]
:signature [{:params [& exprs]}]}}
[& exprs]
(let [n (count exprs)]
(cond
(== n 0) nil
(== n 1) (first exprs)
:else `(let [v# ~(first exprs)]
(cond
v#
v#
~(cons 'or
(next exprs)))))))
;;;;;;;;;; `cond` variants
(defmacro if
^{:doc {:description ["If `test` expression evaluates to a truthy value (anything but false or nil), executes `expr-true`. Otherwise, executes `expr-false`."
"For a more general conditional expression that can handle multiple branches, see `cond.` Also see `when`."]
:examples [{:code "(if (< 1 2) :yes :no)"}]
:signature [{:params [test expr-true]}
{:params [test expr-true expr-false]}]}}
[test & cases]
(cond (<= 1 (count cases) 2)
nil
(fail :ARITY "`if` requires 2 or 3 arguments"))
(cons 'cond
test
cases))
(defmacro if-let
^{:doc {:description "Similar to `if`, but the test expression in bound to a symbol so that it can be accessed in the `expr-true` branch."
:examples [{:code "(if-let [addr (some-function)] (transfer addr 1000) (fail \"Address missing\"))"}]
:signature [{:params [[sym exp] expr-true expr-false]}]}}
[[sym exp] & branches]
`(let [~sym ~exp]
~(cons 'if sym branches)))
(defmacro when
^{:doc {:description "Executes body expressions in an implicit `do` block if and only if the `test` expression evaluates to a truthy value (anything but false or nil)."
:examples [{:code "(when (some-condition) (def foo 42) (+ 2 2))"}]
:signature [{:params [test & body]}]}}
[test & body]
`(cond
~test
~(cons 'do body)
nil))
(defmacro when-let
^{:doc {:description ["Evaluates a binding expression. If truthy, executes the body with the symbol bound to the result."
"Returns nil otherwise."]
:examples [{:code "(when-let [addr (some-function)] (transfer addr 1000))"}]
:signature [{:params [[sym exp] & body]}]}}
[[sym exp] & body]
(let [dobody (cons 'let [sym 'test#]
body)]
`(let [test# ~exp]
(cond test#
~dobody))))
(defmacro when-not
^{:doc {:description "Like `when` but the opposite: body is executed only if the result is false or nil."
:examples [{:code "(when-not (some-condition) :okay)"}]
:signature [{:params [test & body]}]}}
[test & body]
`(cond
~test
nil
~(cons 'do
body)))
;;;;;;;;;; Rest of the API
(defn account?
^{:doc {:description "Returns true if the given address refers to an existing actor or user account, false otherwise."
:examples [{:code "(account? *caller*)"}]
:signature [{:params [address]
:return Boolean}]}}
[addr]
(cond
(address? addr)
(boolean (account addr))
false))
(defn actor?
^{:doc {:description "Returns true if the given address refers to an actor."
:examples [{:code "(actor? #1345)"}]
:signature [{:params [addr]
:return Boolean}]}}
[addr]
(cond
(address? addr)
(let [act (account addr)]
(cond act
(nil? (:key act))
false))
false))
(defmacro assert
^{:doc {:description "Evaluates each test (a form), and raises an `:ASSERT` error if any are not truthy."
:errors {:ASSERT "If a `test` form evaluates to false or nil."}
:examples [{:code "(assert (= owner *caller*))"}]
:signature [{:params [& tests]}]}}
[& tests]
(cons 'do
(map (fn [test]
`(cond
~test
nil
(fail :ASSERT
~(str "Assert failed: "
(str test)))))
tests)))
(defmacro call
^{:doc {:description ["Calls a function in another account, optionally offering coins which the account may receive using `accept`."
"Must refer to a callable function defined in the actor, called with appropriate arguments."]
:errors {:ARGUMENT "If the offer is negative."
:ARITY "If the supplied arguments are the wrong arity for the called function."
:CAST "If the address argument is an Address, the offer is not a Long, or the function name is not a Symbol."
:STATE "If the address does not refer to an Account with the callable function specified by fn-name."}
:examples [{:code "(call some-contract 1000 (contract-fn arg1 arg2))"}]
:signature [{:params [address call-form]
:return Any}
{:params [address offer call-form]
:return Any}]}}
[addr & more]
(let [addr (unsyntax addr)]
(if (empty? more)
(fail :ARITY
"Insufficient arguments to call"))
(let [n (count more)
fnargs (unsyntax (last more))
_ (or (list? fnargs)
(fail :COMPILE
"`call` must have function call list form as last argument."))
sym (unsyntax (first fnargs))
fnlist (cons (list 'quote
sym)
(next fnargs))]
(cond
(== n 1) (cons 'call*
addr
0
fnlist)
(== n 2) (cons 'call*
addr
(first more)
fnlist)))))
(defn comp
^{:doc {:description ["Returns a function that is the composition of the given functions."
"Functions are executed left to right, The righmost function may take a variable number of arguments."
"The result of each function is passed to the next one."]
:examples [{:code "((comp inc inc) 1)"}]
:signature [{:params [f & more]
:return Function}]}}
([f]
f)
([f g]
(fn [& args]
(f (apply g
args))))
([f g h]
(fn [& args]
(f (g (apply h
args)))))
([f g h & more]
(apply comp
(fn [x]
(f (g (h x))))
more)))
(defn create-account
^{:doc {:description "Creates an account with the specified account public key and returns its address."
:errors {:CAST "If the argument is not a blob key of 32 bytes."}
:examples [{:code "(create-account 0x817934590c058ee5b7f1265053eeb4cf77b869e14c33e7f85b2babc85d672bbc)"}]
:signature [{:params [key]
:return Address}]}}
[key]
(or (blob? key)
(fail :CAST
"create-account requires a blob key"))
(deploy `(set-key ~key)))
(defn defined?
^{:doc {:description "Returns true if the given symbol name is defined in the current or specified account environment, false otherwise."
:examples [{:code "(defined? 'max)"}]
:signature [{:params [sym]}
{:params [addr sym]}]}}
([sym]
(or (symbol? sym)
(fail :CAST
"defined? requires a Symbol"))
(boolean (lookup-meta sym)))
([addr sym]
(or (symbol? sym)
(fail :CAST
"defined? requires a Symbol"))
(boolean (lookup-meta addr sym))))
(defmacro doc
^{:doc {:description "Returns the documentation for a given definition."
:examples [{:code "(doc count)"}]
:signature [{:params [sym]}]}}
;; Accepts actual symbols or lookups.
[sym]
`(:doc ~(if (symbol? sym)
`(lookup-meta (quote ~sym))
`(lookup-meta ~(nth sym
1)
(quote ~(nth sym
2))))))
(defmacro dotimes
^{:doc {:description ["Repeats execution of the body `count` times, binding the specified symbol from 0 to `(- count 1)` on successive iterations."
"Always Returns nil."]
:examples [{:code "(dotimes [i 10] (transfer *address* 10))"}]
:signature [{:params [[sym count] & body]}]}}
[[sym count] & body]
(let [n (long count)]
(or (symbol? (unsyntax sym))
(fail :CAST "`dotimes` requires a symbol for loop binding"))
`(loop [~sym 0]
(if (< ~sym
~n)
(do
~(cons do
body)
(recur (inc ~sym)))
nil))))
(defn filter
^{:doc {:description ["Filters a collection by applying the given predicate to each element."
"Each element is included in the new collection if and only if the predicate returns a truthy value (anything but false or nil)."]
:errors {:CAST "If the coll argeument is not a Data Structure."}
:examples [{:code "(filter (fn [x] (> 2 x)) [1 2 3 4])"}]
:signature [{:params [key]
:return Address}]}}
[pred coll]
(reduce (fn [acc e]
(cond (pred e)
(conj acc
e)
acc))
(empty coll)
;; Lists must be reversed so that elements are conjed in the correct order.
;;
(cond (list? coll)
(reverse coll)
coll)))
(defn filterv
^{:doc {:description ["Filters a collection by applying the given predicate to each element, returning a Vector."
"Each element is included in the Vector if and only if the predicate returns a truthy value (anything but false or nil)."]
:errors {:CAST "If the coll argeument is not a Data Structure."}
:examples [{:code "(filter (fn [x] (> 2 x)) [1 2 3 4])"}]
:signature [{:params [key]
:return Address}]}}
[pred coll]
(reduce (fn [acc e]
(cond (pred e)
(conj acc
e)
acc))
[]
coll))
(defmacro for
^{:doc {:description "Executes the body with the symbol `sym` bound to each value of the given sequence. Returns a vector of results."
:examples [{:code "(for [x [1 2 3]] (inc x))"}]
:signature [{:params [[sym sequence] & body]}]}}
[[sym sequence] & body]
`(let [s# ~sequence
n# (count s#)]
(loop [a []
i 0]
(cond (< i n#)
(recur (conj a (let [~sym (nth s# i)] ~(cons 'do
body)))
(inc i))
a))))
(defmacro set-in!
^{:doc {:description "Sets a value within a nested associative structure, combining behaviour of set! and assoc-in."
:examples [{:code "(let [foo {}] (set-in! foo [:a] 1) foo)"}]
:signature [{:params [[sym sequence] & body]}]}}
[sym path val]
`(set! ~sym (assoc-in ~sym ~path ~val)))
(defmacro resolve
^{:doc {:description ["Resolves a value in CNS."]
:examples [{:code "(resolve convex.asset)"}]
:signature [{:params [& args]}]}}
[sym]
`(call* *registry*
0
'cns-resolve
(quote ~sym)))
(defmacro import
^{:doc {:description ["Imports a library for use in the current environment."
"Creates an alias to the library so that symbols defined in the library can be addressed directly in the form 'alias/symbol-name'."
"Returns the address of the imported account."]
:examples [{:code "(import some.library :as alias)"}]
:signature [{:params [& args]}]}}
([addr]
(recur addr :as addr))
([addr as sym]
(cond (symbol? sym) nil (fail :SYNTAX "import: alias must be a Symbol"))
(cond (= :as as) nil (fail :SYNTAX "import expects :as keyword"))
(let [code (cond (symbol? addr)
`(or (resolve ~addr)
(fail :NOBODY
(str "Could not resolve library name for import: "
(quote ~addr))))
`(address ~addr))]
`(def ~(syntax sym {:static true})
~code))))
(defn mapcat
^{:doc {:description "Maps a function across the given collections, then concatenates the results. Nil is treated as an empty collection. See `map`."
:examples [{:code "(mapcat vector [:foo :bar :baz] [1 2 3])"}]
:signature [{:params [test & body]}]}}
[f coll & more]
(apply concat
(empty coll)
(apply map
f
coll
more)))
(defn mapv
^{:doc {:description "Like `map` but systematically returns the result as a vector."
:examples [{:code "(mapv inc '(1 2 3))"}]
:signature [{:params [f & colls] }]}}
[f fst & more]
(apply map f (vec fst) more))
(defmacro schedule
^{:doc {:description "Schedules a transaction for future execution under this account. Expands and compiles code now, but does not execute until the specified timestamp."
:examples [{:code "(schedule (+ *timestamp* 1000) (transfer my-friend 1000000))"}]
:signature [{:params [timestamp code]}]}}
[timestamp code]
`(schedule* ~timestamp
(compile (quote ~code))))
(defmacro tailcall
^{:doc {:description ["Advanced feature. While `return` stops the execution of a function and return, `tailcall` calls another one without consuming additional stack depth."
"Rest of the current function will never be executed."]
:examples [{:code "(tailcall (some-function 1 2 3))"}]
:signature [{:params [[f & args]] }]}}
[callspec]
(let []
(when-not (list? callspec)
(fail :ARGUMENT
"tailcall requires a list representing function invocation"))
(let [n (count callspec)]
(if (== n 0)
(fail :ARGUMENT "Tailcall requires at least a function argument in call list"))
(cons 'tailcall*
callspec))))
(defmacro undef
^{:doc {:description "Opposite of `def`. Undefines a symbol, removing the mapping from the current environment if it exists."
:examples [{:code "(do (def foo 1) (undef foo))"}]
:signature [{:params [sym]}]}}
[sym]
`(undef* (quote ~sym)))
(defmacro for-loop
^{:doc {:description "Executes a code body repeatedy while a condition is true. At the first iteration, `sym` is locally bound to the `init` value. For each subsequent iteration, sym is bound to the result of `change`. The result it the result of the last execution of `body`, or `nil` if no iterations were made."
:examples [{:code "(for-loop [i 0 (< i 10) (inc i)] i)"}]
:signature [{:params [[sym init condition change] & body]}]}}
[[sym init condition change] & body]
`(loop [~sym ~init value# nil]
(if ~condition
(let [new-value# ~(cons 'do body)]
(recur ~change new-value#))
value#)))