Replies: 1 comment
-
Thanks @mimoo ! I'll share a few of my thoughts on DSLs because it's relevant I think!
A few of the nodes are tree-like -- Lazy, With_label, and Exists
This pattern is a typical way to work with "initial-style monadic embedded DSLs" in functional languages. This is in contrast to a "final DSL" which uses interfaces to switch on the implementation rather than reinterpreting AST structures. An initial DSL you'd create by making a recursive Rust enum and to interpret you write a function that recursively traverses the AST. I think of it like this: an initial DSL reifies the AST into a data structure -- in its abstract form -- that you can touch and feel and actually represent in memory as bytes. While a final DSL's AST never really exists in memory, it's a shadow of its execution trace -- you can still interpret a final DSL into some serialized form, but that's just implemented as a concrete interpretation of the abstract structure. |
Beta Was this translation helpful? Give feedback.
-
Snarky exposes a functor that can be used to instantiate its API in
src/snark0.ml
. In the file, there's actually two functors: one for the monadic API (Make
), and one for the imperative API (Run.Make
).You can see the monadic API being instantiated like so:
and the imperative API being instantiated like so:
The imperative API is a thin wrapper built on top of the monadic one. In this document I explain how the monadic API works (which really is most of snarky).
The AST of the Monadic API
The monadic API relies on an AST defined in
checked_ast.ml
:Each variant is an AST node, where
('a, 'f) t
means that it returns a value of type'a
(and'f
is the field used). For example:represents a constraint being added. It contains a tuple of two children: the constraint we want to add, and the rest of the AST (which can be used to compute a value of type
'a
).To read this AST, one important thing to know is that
As_prover.t
is just the type signature of a closure that takes a map from variables to their values, and return a value:So
As_prover.t
closures are ran only when we have access to the actual values of variables: either during debugging, or during witness generation.TODO: it would be nice to have comments on each node of the AST to understand exactly their purpose. Some of the nodes are lost on me.
There's two aspects to using the monadic API:
The rest of this document goes over both of these.
Constructing a circuit with the monadic API
You can see how a circuit is constructed in the tests:
Notice that ppx_let is use with the
let%
syntax.These functions are all defined in
checked_ast.ml
in aBasic
module:The functions are quite simple there and mostly construct nodes of the tree (or extend the tree via
bind
).For example, you can see that
exists
will simply create a new node, with an emptyreturn
instruction which can be used later to extend the program.The logic wrapper by a functor in
checked.ml
, which adds some functionality around computing witness values (with some provider/handler logic, which we will not explain here):This is it for the construction of the AST.
Next let's look at how snarky parses the AST.
Compiling a circuit with the monadic API
Compiling a circuit with the monadic API is call "running" it. Lots of functions are called
run
so beware.The runner is created in
checked_runner.ml
by aMake
functor.This
Make
functor essentially calls another functor inast_runner.ml
(Make
).The
ast_runner.ml
functor looks like this:Interestingly, we are using a state monad to parse the AST.
The actual logic behind the
Checked
function that can handle the different nodes in the tree is implemented inchecked_runner.ml
, and the two are glued together in a functor inside ofchecked_runner.ml
as well.The flow is a bit convoluted, but it looks like this:
Ast_runner.Make_runner
relies on aChecked_intf.Basic
type signatureChecked_runner.Make
, the basic checked interface is created by theChecked_runner.Make_checked
functorChecked_runner.Make
, the newly created basic checked interface is passed toAst_runner.Make_runer
to summarize this in pseudo-code:
More importantly, the state monad has the following type signature:
Which performs a state transition on a
Run_state.t
and produces a value'a
'.So the function that runs the AST actually parses the AST with the state monad, to create such a function. At the end, we are left with a big state transition acting on a (presumably initial)
Run_state.t
, which is chaining multiple state transitions together. This represents the computation of the AST.What is left is just to run that function with an initialized state (and parameterized the state depending on if we want to compile the circuit, generate a witness, or do both).
Beta Was this translation helpful? Give feedback.
All reactions