Skip to content

Commit 28a9ee6

Browse files
0xddomtim-hoffman
andauthored
[LLZK-335] Documentation for LLZK crate (#50)
* Add code documentation * Fix docs linking * Polish README file and add example to llzk crate * Add README to macros crate * Fix typos in example * Remove commented out dead code * Add deny configuration to llzk-macro and do the root comment like in llzk * Fix typos in README * Tweak README's syntax to make it work with rustdoc * Apply some suggestions from code review Co-authored-by: Timothy Hoffman <4001421+tim-hoffman@users.noreply.github.com> * Fix typos in example and README file Co-authored-by: Timothy Hoffman <4001421+tim-hoffman@users.noreply.github.com> * Improve comment based on code review feedback * Tweak example * Add options for formatting comments to rustfmt * Format example * Reformat docs * Improve comment * Add nix docs in README * Update README.md Co-authored-by: Timothy Hoffman <4001421+tim-hoffman@users.noreply.github.com> * Correct comment --------- Co-authored-by: Timothy Hoffman <4001421+tim-hoffman@users.noreply.github.com>
1 parent 0ddcc97 commit 28a9ee6

File tree

37 files changed

+524
-58
lines changed

37 files changed

+524
-58
lines changed

.rustfmt.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,7 @@ use_try_shorthand = true
2525
use_field_init_shorthand = true
2626
force_explicit_abi = true
2727
disable_all_formatting = false
28+
unstable_features = true
29+
wrap_comments = true
30+
normalize_comments = true
31+
comment_width = 100

README.md

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,52 @@
1-
# LLZK's Rust SDK
1+
# Rust bindings for LLZK.
22

3-
This repository is a collection of Rust crates that gives Rust developers access to [LLZK](https://veridise.github.io/llzk-lib/).
3+
⚠️ These crates are under active development and things may change unexpectedly.
44

5-
> [!warning]
6-
> These crates are under active development and things may change unexpectedly.
5+
Rust bindings for [LLZK](https://veridise.github.io/llzk-lib/) over its C API.
6+
The bindings' API is meant to be more user friendly than the raw C API and more idiomatic. Its design is heavily inspired
7+
by [melior](https://github.com/mlir-rs/melior) and depends on it for handling the MLIR parts that are not
8+
specific to LLZK.
79

8-
## Usage (pre v1 release)
10+
The primary supported use case of these bindings is creating IR and running passes on it. Support for other things,
11+
such as writing custom passes, is limited and not as ergonomic as it is in C++.
912

10-
To use the llzk bindings add the crates to your Cargo.toml:
13+
## Usage
1114

15+
Run `cargo doc -p llzk` to generate the API documentation of the rust API and you can visit
16+
[LLZK's documentation](https://veridise.github.io/llzk-lib/) for more information about the IR itself.
17+
For the high-level usage of the bindings you can check the examples in `llzk/examples`.
18+
19+
### Optional features
20+
21+
We include some optional functionality guarded by feature flags. We currently have the following features:
22+
23+
- `bigint`: Allows creating constant values from [`num-bigint`'s Big integers](https://docs.rs/num-bigint/latest/num_bigint/struct.BigUint.html).
24+
25+
## Manual installation (pre v1 release)
26+
27+
Install LLVM 20 and note the installation path. While building your project the build scripts will look for LLVM using `llvm-config`.
28+
If you don't have that tool in your `PATH` or it doesn't point to an LLVM 20 installation set the following environment variables
29+
to the path where LLVM is installed and the build scripts will then use `$MLIR_SYS_200_PREFIX/bin/llvm-config` instead.
30+
31+
```text
32+
export MLIR_SYS_200_PREFIX=/path/to/llvm/20/
33+
export TABLEGEN_200_PREFIX=/path/to/llvm/20/
1234
```
35+
36+
In your rust project, add the crates to your Cargo.toml:
37+
38+
```text
1339
llzk-sys = { git = "https://github.com/Veridise/llzk-rs" }
1440
llzk = { git = "https://github.com/Veridise/llzk-rs" }
1541
```
1642

17-
## Building tips
43+
### Building tips
1844

1945
If you are using homebrew in macos you can access MLIR 20 by installing `llvm@20` with homebrew.
2046
Setting the following environment variables configures the build system with the correct versions of MLIR and its dependencies.
2147
Depending on the version of your default C++ compiler you may need to set `CXX` and `CC` to a compiler that supports C++ 20.
2248

23-
```
49+
```text
2450
export MLIR_SYS_200_PREFIX=/opt/homebrew/opt/llvm@20/
2551
export TABLEGEN_200_PREFIX=/opt/homebrew/opt/llvm@20/
2652
export CXX=clang++
@@ -32,6 +58,28 @@ See [`llzk-sys`'s README](llzk-sys/README.md) for more details on setting up the
3258

3359
If working on LLZK via the submodule you can enable dumping the compile commands when building with cargo. Assuming the current directory is where your editor will look for the compile commands you can link them setting the `LLZK_EMIT_COMPILE_COMMANDS` environment variable as follows.
3460

35-
```
61+
```text
3662
LLZK_EMIT_COMPILE_COMMANDS=$(pwd) cargo build
3763
```
64+
65+
## Nix installation
66+
67+
We also include a nix flake that creates an environment with the right version of LLVM and MLIR. If you are already using nix this may be your prefered method.
68+
69+
You can use this flake for configuring your development environment.
70+
For example, to work within a nix developer shell you can use the following command.
71+
72+
```text
73+
nix develop 'github:Veridise/llzk-rs?submodules=1#llzk-rs'
74+
```
75+
76+
Another alternative is to use [direnv](https://direnv.net/) with the following `.envrc` to automatically enter
77+
the developer environment when you enter your project's directory.
78+
79+
```text
80+
if ! has nix_direnv_version || ! nix_direnv_version 3.0.4; then
81+
source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.0.4/direnvrc" "sha256-DzlYZ33mWF/Gs8DDeyjr8mnVmQGx7ASYqA5WlxwvBG4="
82+
fi
83+
84+
use flake 'github:Veridise/llzk-rs?submodules=1#llzk-rs'
85+
```

llzk-macro/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ edition = "2021"
66
license = "Apache-2.0"
77
repository = "https://github.com/Veridise/llzk-rs"
88
keywords = ["mlir", "llvm", "llzk"]
9+
readme = "README.md"
910

1011
[lib]
1112
proc-macro = true

llzk-macro/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# LLZK proc-macros
2+
3+
Proc macros used internally in llzk-rs.
4+
5+
They are heavily inspired by melior's proc macros with appropriate changes on the import names
6+
to adapt them to llzk.

llzk-macro/src/lib.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
//! Proc macros used internally in llzk-rs.
2-
//!
3-
//! They are heavily inspired by melior's proc macros with appropriate changes on the import names
4-
//! to adapt them to llzk.
1+
#![doc = include_str!("../README.md")]
2+
#![deny(rustdoc::broken_intra_doc_links)]
3+
#![deny(missing_debug_implementations)]
4+
#![deny(missing_docs)]
5+
56
use error::Error;
67
use parse::*;
78
use proc_macro::TokenStream;

llzk/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ categories = ["api-bindings"]
77
description = "Rust bindings to the LLZK C API."
88
repository = "https://github.com/Veridise/llzk-rs"
99
license = "Apache-2.0"
10+
readme = "README.md"
1011

1112
[dependencies]
1213
llzk-sys = { path = "../llzk-sys/", version = "0.1.0"}
@@ -28,3 +29,6 @@ similar-asserts = "1.7"
2829
[features]
2930
default = []
3031
bigint = ["num-bigint"]
32+
33+
[[example]]
34+
name = "division"

llzk/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../README.md

llzk/examples/division.rs

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
//! Heavily commented example of creating IR representing a circuit for a division gadget.
2+
//!
3+
//! The gadget performs the division and constrains the dividend to be equal to the quotient times
4+
//! the divisor.
5+
//!
6+
//! Creates a single struct with two inputs and one output.
7+
8+
use std::error::Error as StdError;
9+
use std::result::Result as StdResult;
10+
11+
// Commonly used types are re-exported in the prelude.
12+
use llzk::{builder::OpBuilder, prelude::*};
13+
14+
type Result<T> = StdResult<T, Box<dyn StdError>>;
15+
16+
fn main() -> Result<()> {
17+
// The context preloads the LLZK dialects for convenience.
18+
let context = LlzkContext::new();
19+
// IR objects have a location associated to them. Usually a source location
20+
// but we won't bother with that in this case.
21+
let location = Location::unknown(&context);
22+
// LLZK top-level modules require some additional attributes.
23+
// This function creates a module preconfigured with these attributes.
24+
let module = llzk_module(location);
25+
26+
// The entry point of the circuit is always a struct named `@Main`.
27+
// Operations can be created with factory methods with the same name as the op they create,
28+
// mimicking its mnemonic (struct.def in this case).
29+
let main_st = r#struct::def(location, "Main", &[], [])?;
30+
31+
// The inputs of `@Main` must be of type !struct.type<@Signal>.
32+
// We need to create this struct to generate properly constructed IR.
33+
let signal_st: StructDefOpRef = module
34+
.body()
35+
.insert_operation(0, r#struct::helpers::define_signal_struct(&context)?.into())
36+
.try_into()?;
37+
38+
// We store the output of the division in a data field.
39+
// Fields can have two extra annotations; column and public.
40+
// The public annotation makes the field an output of the circuit.
41+
let out_field = {
42+
let is_column = false;
43+
let is_public = true;
44+
r#struct::field(location, "c", FeltType::new(&context), is_column, is_public)?
45+
};
46+
let compute_fn = witness(&context, location, signal_st.r#type().into(), &out_field)?;
47+
let constrain_fn = constraints(&context, location, signal_st.r#type().into(), &out_field)?;
48+
49+
main_st.body().append_operation(out_field.into());
50+
main_st.body().append_operation(compute_fn.into());
51+
main_st.body().append_operation(constrain_fn.into());
52+
53+
// Now that we have filled out the struct we can add it to the module, verify it, and print it.
54+
module.body().append_operation(main_st.into());
55+
// For verifying and printing we need get a reference to the `builtin.module` op representing
56+
// the module.
57+
let module_op = module.as_operation();
58+
59+
if module_op.verify() {
60+
println!("{module_op}")
61+
} else {
62+
eprintln!("Module failed to verify");
63+
}
64+
65+
Ok(())
66+
}
67+
68+
fn witness<'c>(
69+
// Context is the type used in melior to represent the MLIRContext.
70+
// A reference to a LlzkContext can be used as a reference to a Context.
71+
context: &'c Context,
72+
location: Location<'c>,
73+
signal_ty: Type<'c>,
74+
out_field: &FieldDefOp<'c>,
75+
) -> Result<Operation<'c>> {
76+
// The inputs to the functions are public circuit inputs.
77+
let inputs = vec![(signal_ty, location); 2];
78+
let pub_attr = [PublicAttribute::named_attr_pair(context)];
79+
let main_ty = StructType::from_str(context, "Main");
80+
81+
// The functions inside a struct need to have a particular structure. This helper creates the
82+
// `@compute` function with its proper structure.
83+
let compute_fn =
84+
r#struct::helpers::compute_fn(location, main_ty, &inputs, Some(&[&pub_attr, &pub_attr]))?;
85+
86+
// Witness generation is represented by creating an instance of the containing struct, filling
87+
// its fields, and returning the value of the struct. The `compute_fn` helper
88+
// inserts a `struct.new` operation followed by a `function.return` operation to represent this.
89+
// The specific IR for our circuit needs to go in between these two operations.
90+
// We will insert it using the return op as reference so we need to get ahold of it and the
91+
// block that contains it.
92+
let (block, ret_op) = compute_fn
93+
.region(0)?
94+
.first_block()
95+
.and_then(|b| Some((b, b.terminator()?)))
96+
.unwrap();
97+
98+
let builder = OpBuilder::new(context);
99+
100+
// To get the inputs we get the arguments and then read the inner value of the signal struct
101+
// for performing the arithmetic.
102+
let a = block
103+
.insert_operation_before(
104+
ret_op,
105+
r#struct::readf(
106+
&builder,
107+
location,
108+
FeltType::new(context).into(),
109+
block.argument(0)?.into(),
110+
"reg",
111+
)?,
112+
)
113+
.result(0)?;
114+
let b = block
115+
.insert_operation_before(
116+
ret_op,
117+
r#struct::readf(
118+
&builder,
119+
location,
120+
FeltType::new(context).into(),
121+
block.argument(1)?.into(),
122+
"reg",
123+
)?,
124+
)
125+
.result(0)?;
126+
127+
// The witness computes c = a / b
128+
let c = block
129+
.insert_operation_before(ret_op, felt::div(location, a.into(), b.into())?)
130+
.result(0)?;
131+
// The result needs to be written into the output field. For that we need to get the value
132+
// created by `struct.new` first.
133+
let self_value = block.first_operation().unwrap().result(0)?;
134+
// Then use the `struct.writef` operation to commit the value into the signal.
135+
block.insert_operation_before(
136+
ret_op,
137+
r#struct::writef(
138+
location,
139+
self_value.into(),
140+
out_field.field_name(),
141+
c.into(),
142+
)?,
143+
);
144+
145+
Ok(compute_fn.into())
146+
}
147+
148+
fn constraints<'c>(
149+
context: &'c Context,
150+
location: Location<'c>,
151+
signal_ty: Type<'c>,
152+
out_field: &FieldDefOp<'c>,
153+
) -> Result<Operation<'c>> {
154+
// The inputs to the functions are public circuit inputs.
155+
let inputs = vec![(signal_ty, location); 2];
156+
let pub_attr = [PublicAttribute::named_attr_pair(context)];
157+
let main_ty = StructType::from_str(context, "Main");
158+
159+
// The functions inside a struct need to have a particular structure. This helper creates the
160+
// `@constrain` function with its proper structure.
161+
let constrain_fn =
162+
r#struct::helpers::constrain_fn(location, main_ty, &inputs, Some(&[&pub_attr, &pub_attr]))?;
163+
164+
// The constraint system is represented by a function that takes as argument an instance of
165+
// the parent struct as well as the same inputs the `@compute` function takes.
166+
// This function returns no values.
167+
// The `constrain_fn` helper inserts an empty `function.return` operation.
168+
//
169+
// Similar to how we generated the IR for `@compute` we need to put the IR before the
170+
// `function.return` operation.
171+
let (block, ret_op) = constrain_fn
172+
.region(0)?
173+
.first_block()
174+
.and_then(|b| Some((b, b.terminator()?)))
175+
.unwrap();
176+
177+
let builder = OpBuilder::new(context);
178+
179+
// We follow the same steps for obtaining the inputs but with the offsets increased by 1.
180+
let a = block
181+
.insert_operation_before(
182+
ret_op,
183+
r#struct::readf(
184+
&builder,
185+
location,
186+
FeltType::new(context).into(),
187+
block.argument(1)?.into(),
188+
"reg",
189+
)?,
190+
)
191+
.result(0)?;
192+
let b = block
193+
.insert_operation_before(
194+
ret_op,
195+
r#struct::readf(
196+
&builder,
197+
location,
198+
FeltType::new(context).into(),
199+
block.argument(2)?.into(),
200+
"reg",
201+
)?,
202+
)
203+
.result(0)?;
204+
// The instance that we are constraining is passed as the first argument.
205+
let self_value = block.argument(0)?;
206+
// And then read the witness output from the instance.
207+
let c = block
208+
.insert_operation_before(
209+
ret_op,
210+
r#struct::readf(
211+
&builder,
212+
location,
213+
FeltType::new(context).into(),
214+
self_value.into(),
215+
out_field.field_name(),
216+
)?,
217+
)
218+
.result(0)?;
219+
220+
// The constraint is c * b = a
221+
// We can use the `constrain.eq` operation for emitting equality constraints.
222+
let t = block
223+
.insert_operation_before(ret_op, felt::mul(location, c.into(), b.into())?)
224+
.result(0)?;
225+
block.insert_operation_before(ret_op, constrain::eq(location, t.into(), a.into()));
226+
227+
Ok(constrain_fn.into())
228+
}

0 commit comments

Comments
 (0)