Skip to content

Commit

Permalink
feat: add support for (list ...) and (fold ...)
Browse files Browse the repository at this point in the history
Also adds a benchmark to track the speed.
  • Loading branch information
obycode committed Aug 17, 2023
1 parent 572807e commit 4000f5c
Show file tree
Hide file tree
Showing 9 changed files with 320 additions and 44 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion clar2wasm/src/standard/standard.wat
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
(import "clarity" "set_variable" (func $set_variable (param i32 i32 i32)))

(global $stack-pointer (mut i32) (i32.const 0))
(memory (export "memory") 1)
(memory (export "memory") 10)

;; The error code is one of:
;; 0: overflow
Expand Down
212 changes: 182 additions & 30 deletions clar2wasm/src/wasm_generator.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::collections::HashMap;
use std::{borrow::BorrowMut, collections::HashMap};

use clarity::vm::{
analysis::ContractAnalysis,
Expand Down Expand Up @@ -64,6 +64,19 @@ enum FunctionKind {
ReadOnly,
}

fn get_type_size(ty: &TypeSignature) -> u32 {
match ty {
TypeSignature::IntType | TypeSignature::UIntType => 16,
TypeSignature::SequenceType(SequenceSubtype::StringType(StringSubtype::ASCII(length))) => {
u32::from(length.clone())
}
TypeSignature::SequenceType(SequenceSubtype::ListType(list_data)) => {
list_data.get_max_len() * get_type_size(list_data.get_list_item_type())
}
_ => unimplemented!("Unsupported type for stack local"),
}
}

impl WasmGenerator {
pub fn new(contract_analysis: ContractAnalysis) -> WasmGenerator {
let standard_lib_wasm: &[u8] = include_bytes!("standard/standard.wasm");
Expand Down Expand Up @@ -291,16 +304,9 @@ impl WasmGenerator {
&mut self,
mut builder: InstrSeqBuilder<'b>,
stack_pointer: GlobalId,
// module: &mut Module,
ty: &TypeSignature,
) -> (InstrSeqBuilder<'b>, LocalId, i32) {
let size = match ty {
TypeSignature::IntType | TypeSignature::UIntType => 16,
TypeSignature::SequenceType(SequenceSubtype::StringType(StringSubtype::ASCII(
length,
))) => u32::from(length.clone()) as i32,
_ => unimplemented!("Unsupported type for stack local"),
};
let size = get_type_size(ty) as i32;

// Save the offset (current stack pointer) into a local
let offset = self.module.locals.add(ValType::I32);
Expand All @@ -320,13 +326,15 @@ impl WasmGenerator {
}

/// Write the value on the top of the stack, which has type `ty`, to the
/// memory, at offset stored in local variable, `offset`.
fn write_to_memory<'b>(
/// memory, at offset stored in local variable, `offset_local`, plus
/// constant offset `offset`.
fn write_to_memory(
&mut self,
mut builder: InstrSeqBuilder<'b>,
offset: LocalId,
builder: &mut InstrSeqBuilder,
offset_local: LocalId,
offset: u32,
ty: &TypeSignature,
) -> (InstrSeqBuilder<'b>, i32) {
) -> i32 {
let memory = self.module.memories.iter().next().expect("no memory found");
let size = match ty {
TypeSignature::IntType | TypeSignature::UIntType => {
Expand All @@ -337,37 +345,34 @@ impl WasmGenerator {
builder.local_set(low).local_set(high);

// Store the high/low to memory.
builder.local_get(offset).local_get(high).store(
builder.local_get(offset_local).local_get(high).store(
memory.id(),
StoreKind::I64 { atomic: false },
MemArg {
align: 8,
offset: 0,
},
MemArg { align: 8, offset },
);
builder.local_get(offset).local_get(low).store(
builder.local_get(offset_local).local_get(low).store(
memory.id(),
StoreKind::I64 { atomic: false },
MemArg {
align: 8,
offset: 8,
offset: offset + 8,
},
);
16
}
_ => unimplemented!("Type not yet supported for writing to memory: {ty}"),
};
(builder, size)
size
}

/// Read a value from memory at offset stored in local variable `offset`,
/// with type `ty`, and load it onto the top of the stack.
fn read_from_memory<'b>(
fn read_from_memory(
&mut self,
mut builder: InstrSeqBuilder<'b>,
builder: &mut InstrSeqBuilder,
offset: LocalId,
ty: &TypeSignature,
) -> (InstrSeqBuilder<'b>, i32) {
) -> i32 {
let memory = self.module.memories.iter().next().expect("no memory found");
let size = match ty {
TypeSignature::IntType | TypeSignature::UIntType => {
Expand All @@ -392,7 +397,7 @@ impl WasmGenerator {
}
_ => unimplemented!("Type not yet supported for writing to memory: {ty}"),
};
(builder, size)
size
}

/// Return a unique identifier, used to identify a contract constant,
Expand Down Expand Up @@ -600,8 +605,7 @@ impl<'a> ASTVisitor<'a> for WasmGenerator {
builder = self.traverse_expr(builder, initial)?;

// Write the initial value to the memory, to be read by the host.
let size;
(builder, size) = self.write_to_memory(builder, offset, &ty);
let size = self.write_to_memory(builder.borrow_mut(), offset, 0, &ty);

// Increment the literal memory end
// FIXME: These initial values do not need to be saved in the literal
Expand Down Expand Up @@ -774,7 +778,7 @@ impl<'a> ASTVisitor<'a> for WasmGenerator {

// Host interface fills the result into the specified memory. Read it
// back out, and place the value on the stack.
(builder, _) = self.read_from_memory(builder, offset, &ty);
self.read_from_memory(builder.borrow_mut(), offset, &ty);

Ok(builder)
}
Expand All @@ -798,7 +802,7 @@ impl<'a> ASTVisitor<'a> for WasmGenerator {
(builder, offset, size) = self.create_stack_local(builder, self.stack_pointer, &ty);

// Write the value to the memory (it's already on the stack)
(builder, _) = self.write_to_memory(builder, offset, &ty);
self.write_to_memory(builder.borrow_mut(), offset, 0, &ty);

// Push the variable identifier onto the stack
builder.i32_const(var_id);
Expand All @@ -821,6 +825,154 @@ impl<'a> ASTVisitor<'a> for WasmGenerator {

Ok(builder)
}

fn traverse_list_cons<'b>(
&mut self,
mut builder: InstrSeqBuilder<'b>,
expr: &'a SymbolicExpression,
list: &'a [SymbolicExpression],
) -> Result<InstrSeqBuilder<'b>, InstrSeqBuilder<'b>> {
let ty = self.get_expr_type(expr).clone();
let (elem_ty, num_elem) =
if let TypeSignature::SequenceType(SequenceSubtype::ListType(list_type)) = &ty {
(list_type.get_list_item_type(), list_type.get_max_len())
} else {
panic!(
"Expected list type for list expression, but found: {:?}",
ty
);
};

assert_eq!(num_elem as usize, list.len(), "list size mismatch");

// Allocate space on the data stack for the entire list
let (offset, size);
(builder, offset, size) = self.create_stack_local(builder, self.stack_pointer, &ty);

// Loop through the expressions in the list and store them onto the
// data stack.
let mut total_size = 0;
for expr in list.iter() {
builder = self.traverse_expr(builder, expr)?;
let elem_size = self.write_to_memory(builder.borrow_mut(), offset, total_size, elem_ty);
total_size += elem_size as u32;
}
assert_eq!(total_size, size as u32, "list size mismatch");

// Push the offset and size to the instruction stack
builder.local_get(offset).i32_const(size);

Ok(builder)
}

fn traverse_fold<'b>(
&mut self,
mut builder: InstrSeqBuilder<'b>,
_expr: &'a SymbolicExpression,
func: &'a clarity::vm::ClarityName,
sequence: &'a SymbolicExpression,
initial: &'a SymbolicExpression,
) -> Result<InstrSeqBuilder<'b>, InstrSeqBuilder<'b>> {
// Fold takes an initial value, and a sequence, and applies a function
// to the output of the previous call, or the initial value in the case
// of the first call, and each element of the sequence.
// ```
// (fold - (list 2 4 6) 0)
// ```
// is equivalent to
// ```
// (- 6 (- 4 (- 2 0)))
// ```

// The result type must match the type of the initial value
let result_clar_ty = self.get_expr_type(initial);
let result_ty = clar2wasm_ty(result_clar_ty);
let loop_body_ty = InstrSeqType::new(
&mut self.module.types,
result_ty.as_slice(),
result_ty.as_slice(),
);

// Get the type of the sequence
let seq_ty = match self.get_expr_type(sequence) {
TypeSignature::SequenceType(seq_ty) => seq_ty.clone(),
_ => {
self.error = Some(GeneratorError::InternalError(
"expected sequence type".to_string(),
));
return Err(builder);
}
};

let (seq_len, elem_ty) = match &seq_ty {
SequenceSubtype::ListType(list_type) => {
(list_type.get_max_len(), list_type.get_list_item_type())
}
_ => unimplemented!("Unsupported sequence type"),
};

// Evaluate the sequence, which will load it onto the memory stack,
// leaving the offset and size on the stack.
builder = self.traverse_expr(builder, sequence)?;

// Drop the size, since we don't need it
builder.drop();

// Store the offset into a local
let offset = self.module.locals.add(ValType::I32);
builder.local_set(offset);

let elem_size = get_type_size(elem_ty);

// Store the end of the sequence into a local
let end_offset = self.module.locals.add(ValType::I32);
builder
.local_get(offset)
.i32_const((seq_len * elem_size) as i32)
.binop(BinaryOp::I32Add)
.local_set(end_offset);

// Evaluate the initial value, so that its result is on the stack
builder = self.traverse_expr(builder, initial)?;

if seq_len == 0 {
// If the sequence is empty, just return the initial value
return Ok(builder);
}

// Define the body of a loop, to loop over the sequence and make the
// function call.
builder.loop_(loop_body_ty, |loop_| {
let loop_id = loop_.id();

// Load the element from the sequence
let elem_size = self.read_from_memory(loop_, offset, elem_ty);

// Call the function
loop_.call(
self.module
.funcs
.by_name(func.as_str())
.expect("function not found"),
);

// Increment the offset by the size of the element
loop_
.local_get(offset)
.i32_const(elem_size)
.binop(BinaryOp::I32Add)
.local_set(offset);

// Loop if we haven't reached the end of the sequence
loop_
.local_get(offset)
.local_get(end_offset)
.binop(BinaryOp::I32LtU)
.br_if(loop_id);
});

Ok(builder)
}
}

fn clar2wasm_ty(ty: &TypeSignature) -> Vec<ValType> {
Expand Down
7 changes: 6 additions & 1 deletion tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,9 @@ clar2wasm = { path = "../clar2wasm" }
wasmtime = "11.0.1"
sha2 = "0.10.7"
chrono = "0.4.20"
rusqlite = "0.24.2"
rusqlite = "0.24.2"
criterion = "0.5.1"

[[bench]]
name = "benchmark"
harness = false
50 changes: 50 additions & 0 deletions tests/benches/benchmark.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
use clarity::{vm::{types::{QualifiedContractIdentifier, StandardPrincipalData}, ContractName, database::ClarityDatabase, costs::LimitedCostTracker, contexts::GlobalContext, ContractContext, ClarityVersion}, types::StacksEpochId, consts::CHAIN_ID_TESTNET};
use criterion::{criterion_group, criterion_main, Criterion};
use clar2wasm_tests::datastore::{Datastore, StacksConstants, BurnDatastore};
use clar2wasm_tests::util::WasmtimeHelper;

fn fold_add(c: &mut Criterion) {
c.bench_function("fold_add", |b| {
let contract_id = QualifiedContractIdentifier::new(
StandardPrincipalData::transient(),
ContractName::from("fold-bench"),
);
let mut datastore = Datastore::new();
let constants = StacksConstants {
burn_start_height: 0,
pox_prepare_length: 0,
pox_reward_cycle_length: 0,
pox_rejection_fraction: 0,
epoch_21_start_height: 0,
};
let burn_datastore = BurnDatastore::new(constants);
let mut conn = ClarityDatabase::new(&mut datastore, &burn_datastore, &burn_datastore);
conn.begin();
conn.set_clarity_epoch_version(StacksEpochId::Epoch24);
conn.commit();
let cost_tracker = LimitedCostTracker::new_free();
let mut global_context = GlobalContext::new(
false,
CHAIN_ID_TESTNET,
conn,
cost_tracker,
StacksEpochId::Epoch24,
);
let mut contract_context =
ContractContext::new(contract_id.clone(), ClarityVersion::Clarity2);

global_context.begin();
{
let mut helper =
WasmtimeHelper::new(contract_id, &mut global_context, &mut contract_context);

b.iter(|| {
helper.call_public_function("fold-add", &[]);
});
}
global_context.commit().unwrap();
});
}

criterion_group!(all, fold_add);
criterion_main!(all);
15 changes: 15 additions & 0 deletions tests/contracts/fold-bench.clar

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions tests/contracts/fold.clar
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
(define-private (add (x int) (y int))
(+ x y)
)

(define-public (fold-add)
(ok (fold add (list 1 2 3 4) 0))
)
Loading

0 comments on commit 4000f5c

Please sign in to comment.