Skip to content

Commit

Permalink
fix: short-return type for asserts! function
Browse files Browse the repository at this point in the history
  • Loading branch information
csgui committed Oct 3, 2024
1 parent 3d8adaa commit 97b229c
Show file tree
Hide file tree
Showing 7 changed files with 165 additions and 28 deletions.
62 changes: 49 additions & 13 deletions clar2wasm/src/error_mapping.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
use clarity::types::StacksEpochId;
use clarity::vm::errors::{CheckErrors, Error, RuntimeErrorType, ShortReturnType, WasmError};
use clarity::vm::types::ResponseData;
use clarity::vm::Value;
use clarity::vm::ClarityVersion;
use wasmtime::{AsContextMut, Instance, Trap};

use crate::wasm_utils::read_identifier_from_wasm;
use crate::wasm_utils::{
read_from_wasm_indirect, read_identifier_from_wasm, signature_from_string,
};

const LOG2_ERROR_MESSAGE: &str = "log2 must be passed a positive integer";
const SQRTI_ERROR_MESSAGE: &str = "sqrti must be passed a positive integer";
Expand Down Expand Up @@ -47,6 +49,8 @@ pub(crate) fn resolve_error(
e: wasmtime::Error,
instance: Instance,
mut store: impl AsContextMut,
epoch_id: &StacksEpochId,
clarity_version: &ClarityVersion,
) -> Error {
if let Some(vm_error) = e.root_cause().downcast_ref::<Error>() {
// SAFETY:
Expand Down Expand Up @@ -104,7 +108,7 @@ pub(crate) fn resolve_error(
// In this case, runtime errors are handled
// by being mapped to the corresponding ClarityWasm Errors.
if let Some(Trap::UnreachableCodeReached) = e.root_cause().downcast_ref::<Trap>() {
return from_runtime_error_code(instance, &mut store, e);
return from_runtime_error_code(instance, &mut store, e, epoch_id, clarity_version);
}

// All other errors are treated as general runtime errors.
Expand All @@ -115,6 +119,8 @@ fn from_runtime_error_code(
instance: Instance,
mut store: impl AsContextMut,
e: wasmtime::Error,
epoch_id: &StacksEpochId,
clarity_version: &ClarityVersion,
) -> Error {
let global = "runtime-error-code";
let runtime_error_code = instance
Expand Down Expand Up @@ -147,15 +153,45 @@ fn from_runtime_error_code(
ErrorMap::Panic => {
panic!("An error has been detected in the code")
}
// TODO: UInt(42) value below is just a placeholder.
// It should be replaced by the current "thrown-value" when issue #385 is resolved.
// Tests that reach this code are currently ignored.
ErrorMap::ShortReturnAssertionFailure => Error::ShortReturn(
ShortReturnType::AssertionFailed(Value::Response(ResponseData {
committed: false,
data: Box::new(Value::UInt(42)),
})),
),
ErrorMap::ShortReturnAssertionFailure => {
let val_offset = instance
.get_global(&mut store, "runtime-error-value-offset")
.and_then(|glob| glob.get(&mut store).i32())
.unwrap_or_else(|| {
panic!("Could not find $runtime-error-value-offset global with i32 value")
});

let type_ser_offset = instance
.get_global(&mut store, "runtime-error-type-ser-offset")
.and_then(|glob| glob.get(&mut store).i32())
.unwrap_or_else(|| {
panic!("Could not find $runtime-error-type-ser-offset global with i32 value")
});

let type_ser_len = instance
.get_global(&mut store, "runtime-error-type-ser-len")
.and_then(|glob| glob.get(&mut store).i32())
.unwrap_or_else(|| {
panic!("Could not find $runtime-error-type-ser-len global with i32 value")
});

let memory = instance
.get_memory(&mut store, "memory")
.unwrap_or_else(|| panic!("Could not find wasm instance memory"));

let type_ser_str =
read_identifier_from_wasm(memory, &mut store, type_ser_offset, type_ser_len)
.unwrap_or_else(|e| panic!("Could not recover stringified type: {e}"));

let value_ty = signature_from_string(&type_ser_str, *clarity_version, *epoch_id)
.unwrap_or_else(|e| panic!("Could not recover thrown value: {e}"));

let clarity_val =
read_from_wasm_indirect(memory, &mut store, &value_ty, val_offset, *epoch_id)
.unwrap_or_else(|e| panic!("Could not read thrown value from memory: {e}"));

Error::ShortReturn(ShortReturnType::AssertionFailed(clarity_val))
}
ErrorMap::ArithmeticPowError => Error::Runtime(
RuntimeErrorType::Arithmetic(POW_ERROR_MESSAGE.into()),
Some(Vec::new()),
Expand Down
5 changes: 4 additions & 1 deletion clar2wasm/src/initialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,7 @@ pub fn initialize_contract(

let mut call_stack = CallStack::new();
let epoch = global_context.epoch_id;
let clarity_version = *contract_context.get_clarity_version();
let init_context = ClarityWasmContext::new_init(
global_context,
contract_context,
Expand Down Expand Up @@ -367,7 +368,9 @@ pub fn initialize_contract(

top_level
.call(&mut store, &[], results.as_mut_slice())
.map_err(|e| error_mapping::resolve_error(e, instance, &mut store))?;
.map_err(|e| {
error_mapping::resolve_error(e, instance, &mut store, &epoch, &clarity_version)
})?;

// Save the compiled Wasm module into the contract context
store.data_mut().contract_context_mut()?.set_wasm_module(
Expand Down
6 changes: 6 additions & 0 deletions clar2wasm/src/standard/standard.wat
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,9 @@
(global $runtime-error-code (mut i32) (i32.const -1))
(global $runtime-error-arg-offset (mut i32) (i32.const -1))
(global $runtime-error-arg-len (mut i32) (i32.const -1))
(global $runtime-error-value-offset (mut i32) (i32.const -1))
(global $runtime-error-type-ser-offset (mut i32) (i32.const -1))
(global $runtime-error-type-ser-len (mut i32) (i32.const -1))

;; (sha256) initial hash values: first 32 bits of the fractional parts of the square roots of the first 8 primes 2..19
(data (i32.const 0) "\67\e6\09\6a\85\ae\67\bb\72\f3\6e\3c\3a\f5\4f\a5\7f\52\0e\51\8c\68\05\9b\ab\d9\83\1f\19\cd\e0\5b")
Expand Down Expand Up @@ -3884,6 +3887,9 @@
(export "runtime-error-code" (global $runtime-error-code))
(export "runtime-error-arg-offset" (global $runtime-error-arg-offset))
(export "runtime-error-arg-len" (global $runtime-error-arg-len))
(export "runtime-error-value-offset" (global $runtime-error-value-offset))
(export "runtime-error-type-ser-offset" (global $runtime-error-type-ser-offset))
(export "runtime-error-type-ser-len" (global $runtime-error-type-ser-len))

;; Functions
(export "stdlib.add-uint" (func $stdlib.add-uint))
Expand Down
96 changes: 92 additions & 4 deletions clar2wasm/src/wasm_generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ use clarity::vm::analysis::ContractAnalysis;
use clarity::vm::diagnostic::DiagnosableError;
use clarity::vm::types::signatures::{CallableSubtype, StringUTF8Length, BUFF_1};
use clarity::vm::types::{
ASCIIData, CharType, FixedFunction, FunctionType, PrincipalData, SequenceData, SequenceSubtype,
StringSubtype, TypeSignature,
ASCIIData, CharType, FixedFunction, FunctionType, ListTypeData, PrincipalData, SequenceData,
SequenceSubtype, StringSubtype, TupleTypeSignature, TypeSignature,
};
use clarity::vm::variables::NativeVariables;
use clarity::vm::{functions, variables, ClarityName, SymbolicExpression, SymbolicExpressionType};
Expand All @@ -25,7 +25,7 @@ use walrus::{
use crate::error_mapping::ErrorMap;
use crate::wasm_utils::{
get_type_in_memory_size, get_type_size, is_in_memory_type, ordered_tuple_signature,
owned_ordered_tuple_signature,
owned_ordered_tuple_signature, signature_from_string,
};
use crate::words;

Expand Down Expand Up @@ -628,16 +628,104 @@ impl WasmGenerator {
if let Some(block_id) = self.early_return_block_id {
builder.instr(walrus::ir::Br { block: block_id });
} else {
// This must be from a top-leve statement, so it should cause a runtime error
// This must be from a top-level statement, so it should cause a runtime error
builder
.i32_const(ErrorMap::ShortReturnAssertionFailure as i32)
.call(self.func_by_name("stdlib.runtime-error"));

builder.unreachable();
}

Ok(())
}

pub fn asserts_return_early(
&mut self,
builder: &mut InstrSeqBuilder,
expr: &SymbolicExpression,
) -> Result<(), GeneratorError> {
if let Some(block_id) = self.early_return_block_id {
builder.instr(walrus::ir::Br { block: block_id });
return Ok(());
}

let ty = self
.get_expr_type(expr)
.ok_or_else(|| {
GeneratorError::TypeError("asserts! thrown-value must be typed".to_owned())
})?
.clone();

let (val_offset, _) = self.create_call_stack_local(builder, &ty, false, true);
self.write_to_memory(builder, val_offset, 0, &ty)?;
let serialized_ty = Self::type_for_serialization(&ty).to_string();

// Check, at compile time, if the type can be deserialized.
signature_from_string(
&serialized_ty,
self.contract_analysis.clarity_version,
self.contract_analysis.epoch,
)
.map_err(|e| GeneratorError::TypeError(format!("type cannot be deserialized: {e:?}")))?;

let (type_ser_offset, type_ser_len) =
self.add_clarity_string_literal(&CharType::ASCII(ASCIIData {
data: serialized_ty.bytes().collect(),
}))?;

builder
.local_get(val_offset)
.global_set(get_global(&self.module, "runtime-error-value-offset")?)
.i32_const(type_ser_offset as i32)
.global_set(get_global(&self.module, "runtime-error-type-ser-offset")?)
.i32_const(type_ser_len as i32)
.global_set(get_global(&self.module, "runtime-error-type-ser-len")?)
.i32_const(ErrorMap::ShortReturnAssertionFailure as i32)
.call(self.func_by_name("stdlib.runtime-error"));

builder.unreachable();

Ok(())
}

fn type_for_serialization(ty: &TypeSignature) -> TypeSignature {
use clarity::vm::types::signatures::TypeSignature::*;
match ty {
// NoType and BoolType have the same size (both type and inner)
NoType => BoolType,
// Avoid serialization like `(list 2 <S1G2081040G2081040G2081040G208105NK8PE5.my-trait.my-trait>)`
CallableType(CallableSubtype::Trait(_)) => PrincipalType,
// Recursive types
ResponseType(types) => ResponseType(Box::new((
Self::type_for_serialization(&types.0),
Self::type_for_serialization(&types.1),
))),
OptionalType(value_ty) => {
OptionalType(Box::new(Self::type_for_serialization(value_ty)))
}
SequenceType(SequenceSubtype::ListType(list_ty)) => {
SequenceType(SequenceSubtype::ListType(
ListTypeData::new_list(
Self::type_for_serialization(list_ty.get_list_item_type()),
list_ty.get_max_len(),
)
.unwrap_or_else(|_| list_ty.clone()),
))
}
TupleType(tuple_ty) => TupleType(
TupleTypeSignature::try_from(
tuple_ty
.get_type_map()
.iter()
.map(|(k, v)| (k.clone(), Self::type_for_serialization(v)))
.collect::<Vec<_>>(),
)
.unwrap_or_else(|_| tuple_ty.clone()),
),
t => t.clone(),
}
}

/// Gets the result type of the given `SymbolicExpression`.
pub fn get_expr_type(&self, expr: &SymbolicExpression) -> Option<&TypeSignature> {
self.contract_analysis
Expand Down
5 changes: 4 additions & 1 deletion clar2wasm/src/wasm_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1246,6 +1246,7 @@ pub fn call_function<'a>(
sponsor: Option<PrincipalData>,
) -> Result<Value, Error> {
let epoch = global_context.epoch_id;
let clarity_version = *contract_context.get_clarity_version();
let context = ClarityWasmContext::new_run(
global_context,
contract_context,
Expand Down Expand Up @@ -1330,7 +1331,9 @@ pub fn call_function<'a>(

// Call the function
func.call(&mut store, &wasm_args, &mut results)
.map_err(|e| error_mapping::resolve_error(e, instance, &mut store))?;
.map_err(|e| {
error_mapping::resolve_error(e, instance, &mut store, &epoch, &clarity_version)
})?;

// If the function returns a value, translate it into a Clarity `Value`
wasm_to_clarity_value(&return_type, 0, &results, memory, &mut &mut store, epoch)
Expand Down
4 changes: 2 additions & 2 deletions clar2wasm/src/words/conditionals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -699,8 +699,9 @@ impl ComplexWord for Asserts {
if let Some(return_ty) = generator.get_current_function_return_type() {
generator.set_expr_type(throw, return_ty.clone())?;
}

generator.traverse_expr(&mut throw_branch, throw)?;
generator.return_early(&mut throw_branch)?;
generator.asserts_return_early(&mut throw_branch, throw)?;

let throw_branch_id = throw_branch.id();

Expand Down Expand Up @@ -1230,7 +1231,6 @@ mod tests {
crosscheck("(asserts! true (err u1))", Ok(Some(Value::Bool(true))));
}

#[ignore = "see issue: #385"]
#[test]
fn asserts_top_level_false() {
crosscheck(
Expand Down
15 changes: 8 additions & 7 deletions clar2wasm/tests/wasm-generation/conditionals.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use clar2wasm::tools::{crosscheck, crosscheck_compare_only};
use clarity::vm::errors::{Error, ShortReturnType};
use clarity::vm::types::{ListTypeData, SequenceData, SequenceSubtype, TypeSignature};
use clarity::vm::Value;
use proptest::prelude::any;
use proptest::proptest;
use proptest::strategy::{Just, Strategy};

Expand Down Expand Up @@ -191,18 +193,17 @@ proptest! {
proptest! {
#![proptest_config(super::runtime_config())]

#[ignore = "see issue: #385"]
#[test]
fn crosscheck_asserts_true(bool in bool(), val in PropValue::any()) {
let expected = match bool.to_string().as_str() {
"true" => PropValue::from(bool.clone()),
"false" => val.clone(),
_ => panic!("Invalid boolean string"),
fn crosscheck_asserts_boolean(bool in any::<bool>(), val in PropValue::any()) {
let expected = if bool {
Ok(Some(Value::Bool(bool)))
} else {
Err(Error::ShortReturn(ShortReturnType::AssertionFailed(Value::from(val.clone()))))
};

crosscheck(
&format!("(asserts! {bool} {val})"),
Ok(Some(expected.into()))
expected
);
}
}
Expand Down

0 comments on commit 97b229c

Please sign in to comment.