Skip to content

Commit

Permalink
update implementing libfuncs doc (#856)
Browse files Browse the repository at this point in the history
  • Loading branch information
edg-l authored Oct 17, 2024
1 parent 9db1175 commit 0e251c5
Show file tree
Hide file tree
Showing 3 changed files with 41 additions and 53 deletions.
78 changes: 33 additions & 45 deletions docs/implementing_libfuncs.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,46 +19,45 @@ A type that doesn't have size would be `Layout::new::<()>()`, or if the
type is a pointer like box: `Layout::new::<*mut ()>()`

When adding a type, we also need to add the **serialization** and
**deserialization** functionality, so we can use it with the JIT runner.
**deserialization** functionality to convert the value to a memory representation that works with cairo-native.

You can find this functionality under `src/values.rs` and
`src/values/{typename}.rs`. As you can see, the project is quite organized
if you have a feel of its layout.
You can find this functionality under `src/values.rs`.

Serialization is done using `Serde`, and each type provides a `deserialize`
and `serialize` function. The inner workings of such functions can be a bit
complex due to how the JIT runner works. You need to work with pointers and
unsafe rust.
There is a `Value` enum with all the possible values that can be passed as input/output.

In `values.rs` we should also declare whether the type is complex under
`is_complex` in the `ValueBuilder` trait implementation.
This enum has a `impl` block with 2 important methods `to_ptr` and `from_ptr` which convert the Value into and from said
memory representation, held behind a pointer.

When passing the values as inputs, there is one more required step, that is to pass the bytes of those values in the
target platform ABI compatible way, this is done with the `AbiArgument` trait and the `to_bytes` method.

This trait, located in `src/arch.rs` is implemented currently for aarch64 and x86_64 (depending on the host platform) for some basic types, such as u64, u128, pointers, etc. Most importantly, it is also implemented for `ValueWithInfoWrapper` which allows to convert the Value using it's `to_ptr` method and correctly passing it as an argument in the given `buffer: &mut Vec<u8>`.

In `types.rs` we should also declare whether the type is complex under
`is_complex`, whether its a builtin in `is_builtin`, a zst in `is_zst` and define it's layout in the `TypeBuilder` trait implementation.

> Complex types are always passed by pointer (both as params and return
> values) and require a stack allocation. Examples of complex values include
> structs and enums, but not felts since LLVM considers them integers.
### Deserializing a type
When **deserializing** (a.k.a converting the inputs so the JIT runner
When **deserializing** (a.k.a converting the inputs so the runner
accepts them), you are passed a bump allocator arena from `Bumpalo`, the
general idea is to get the layout and size of the type, allocate it under
the arena, get a pointer, and return it. Which will later be passed to the
MLIR JIT runner. It is important the pointers passed are allocated by the
the arena, get a pointer, and return it (Some types require additional data on the heap, such as arrays, such allocations should be done with libc's malloc.). Which will later be passed to the runner. It is important the pointers passed are allocated by the
arena and not Rust itself.

Then we need to hookup de `deserialize` method in `values.rs` `deserialize`
method.
This is done in the `to_ptr` method.

### Serializing a type
When **serializing** a type, you will get a `ptr: NonNull<()>` (non null
pointer), which you will have to cast, dereference and then deserialize.

For a simple type to learn how it works, we recommend checking
`src/values/uint8.rs`, for more complex types, check `src/values/felt252.rs`.
`src/values.rs`, in the `from_ptr` method, look the u8 type in the match, for more complex types, check the felt252 type.
The hardest types to understand are the enums, dictionaries and arrays,
since they are complex types.

Then we need to hookup de `serialize` method in `values.rs` `serialize` method.

### Implementing the library function
Libfuncs are implemented under `src/libfuncs.rs` and
`src/libfuncs/{libfunc_name}.rs`. Just like types.
Expand All @@ -67,21 +66,15 @@ Using the `src/libfuncs/felt252.rs` libfuncs as a aid:

```rust,ignore
/// Select and call the correct libfunc builder function from the selector.
pub fn build<'ctx, 'this, TType, TLibfunc>(
pub fn build<'ctx, 'this>(
context: &'ctx Context,
registry: &ProgramRegistry<TType, TLibfunc>,
registry: &ProgramRegistry<CoreType, CoreLibfunc>,
entry: &'this Block<'ctx>,
location: Location<'ctx>,
helper: &LibfuncHelper<'ctx, 'this>,
metadata: &mut MetadataStorage,
selector: &Felt252Concrete,
) -> Result<()>
where
TType: GenericType,
TLibfunc: GenericLibfunc,
<TType as GenericType>::Concrete: TypeBuilder<TType, TLibfunc, Error = CoreTypeBuilderError>,
<TLibfunc as GenericLibfunc>::Concrete: LibfuncBuilder<TType, TLibfunc, Error = Error>,
{
) -> Result<()> {
match selector {
Felt252Concrete::BinaryOperation(info) => {
build_binary_operation(context, registry, entry, location, helper, metadata, info)
Expand Down Expand Up @@ -109,11 +102,11 @@ An example libfunc, converting a u8 to a felt252, extensively commented:

```rust,ignore
/// Generate MLIR operations for the `u8_to_felt252` libfunc.
pub fn build_to_felt252<'ctx, 'this, TType, TLibfunc>(
pub fn build_to_felt252<'ctx, 'this>(
// The Context from MLIR, this is like the heart of the MLIR API, its required to create most stuff like types.
context: &'ctx Context,
// This is the sierra program registry, it aids us at finding types, functions, etc.
registry: &ProgramRegistry<TType, TLibfunc>,
registry: &ProgramRegistry<CoreType, CoreLibfunc>,
// This is the MLIR entry block for this libfunc. Remember we append operations to blocks.
entry: &'this Block<'ctx>,
// The already created MLIR location for this libfunc, we need to pass this to all the MLIR operations.
Expand All @@ -125,28 +118,24 @@ pub fn build_to_felt252<'ctx, 'this, TType, TLibfunc>(
// The sierra information for this specific library function. This libfunc only contains signature information, but
// others which are generic over a type will contain information about that type, for example array related libfuncs.
info: &SignatureOnlyConcreteLibfunc,
) -> Result<()>
where
TType: GenericType,
TLibfunc: GenericLibfunc,
<TType as GenericType>::Concrete: TypeBuilder<TType, TLibfunc, Error = CoreTypeBuilderError>,
<TLibfunc as GenericLibfunc>::Concrete: LibfuncBuilder<TType, TLibfunc, Error = Error>,
{
) -> Result<()> {
// We retrieve the felt252 type from the registry and call the "build" method to create the MLIR type.
// We could also just call get_type() to hold on to the sierra type, and then `.layout(registry)` to get the type layout,
// which is needed in some libfuncs doing more complex stuff.
let felt252_ty = registry
.get_type(&info.branch_signatures()[0].vars[0].ty)?
.build(context, helper, registry, metadata)?;
let felt252_ty = registry.build_type(
context,
helper,
registry,
metadata,
&info.branch_signatures()[0].vars[0].ty,
)?;
// Retrieve the first argument passed to this library function, in this case its the u8 value we need to convert.
let value: Value = entry.argument(0)?.into();
// We create a "extui" operation from the "arith" dialect, which basically zero extends the value to have the same bits as the given type.
let op = entry.append_operation(arith::extui(value, felt252_ty, location));
// Get the result from the operation, in this case it's the extended value
let result = op.result(0)?.into();
// We create a "extui" operation from the "arith" dialect, which basically
// zero extends the value to have the same bits as the given type.
let result = entry.append_op_result(arith::extui(value, felt252_ty, location))?;
// Using the helper argument, append the branching operation to the next statement, passing result as our output variable.
entry.append_operation(helper.br(0, &[result], location));
Expand All @@ -156,4 +145,3 @@ where
```

More info on the `extui` operation: <https://mlir.llvm.org/docs/Dialects/ArithOps/#arithextui-arithextuiop>

12 changes: 6 additions & 6 deletions src/arch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ pub trait AbiArgument {
fn to_bytes(&self, buffer: &mut Vec<u8>) -> Result<(), error::Error>;
}

/// A wrapper that implements `AbiArgument` for `JitValue`s. It contains all the required stuff to
/// serialize all possible `JitValue`s.
pub struct JitValueWithInfoWrapper<'a> {
/// A wrapper that implements `AbiArgument` for `Value`s. It contains all the required stuff to
/// serialize all possible `Value`s.
pub struct ValueWithInfoWrapper<'a> {
pub value: &'a Value,
pub type_id: &'a ConcreteTypeId,
pub info: &'a CoreTypeConcrete,
Expand All @@ -37,12 +37,12 @@ pub struct JitValueWithInfoWrapper<'a> {
pub registry: &'a ProgramRegistry<CoreType, CoreLibfunc>,
}

impl<'a> JitValueWithInfoWrapper<'a> {
impl<'a> ValueWithInfoWrapper<'a> {
fn map<'b>(
&'b self,
value: &'b Value,
type_id: &'b ConcreteTypeId,
) -> Result<JitValueWithInfoWrapper<'b>, error::Error>
) -> Result<ValueWithInfoWrapper<'b>, error::Error>
where
'b: 'a,
{
Expand All @@ -56,7 +56,7 @@ impl<'a> JitValueWithInfoWrapper<'a> {
}
}

impl<'a> AbiArgument for JitValueWithInfoWrapper<'a> {
impl<'a> AbiArgument for ValueWithInfoWrapper<'a> {
fn to_bytes(&self, buffer: &mut Vec<u8>) -> Result<(), error::Error> {
match (self.value, self.info) {
(value, CoreTypeConcrete::Box(info)) => {
Expand Down
4 changes: 2 additions & 2 deletions src/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

pub use self::{aot::AotNativeExecutor, contract::AotContractExecutor, jit::JitNativeExecutor};
use crate::{
arch::{AbiArgument, JitValueWithInfoWrapper},
arch::{AbiArgument, ValueWithInfoWrapper},
error::Error,
execution_result::{BuiltinStats, ExecutionResult},
starknet::{handler::StarknetSyscallHandlerCallbacks, StarknetSyscallHandler},
Expand Down Expand Up @@ -184,7 +184,7 @@ fn invoke_dynamic(
}
}
type_info if type_info.is_builtin() => 0u64.to_bytes(&mut invoke_data)?,
type_info => JitValueWithInfoWrapper {
type_info => ValueWithInfoWrapper {
value: iter.next().unwrap(),
type_id,
info: type_info,
Expand Down

0 comments on commit 0e251c5

Please sign in to comment.