Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

runtime error handling #379

Merged
merged 32 commits into from
Jun 27, 2024
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
d0068ba
feat: runtime error handling
csgui Apr 11, 2024
2fb9680
tests: runtime error handling
csgui Apr 12, 2024
5a81bb7
fix: unary uint subtraction
csgui Apr 15, 2024
582b9e2
tests: ignoring tests with issues identified by the runtime error han…
csgui Apr 17, 2024
0960e8d
feat: create a separete module for error mapping
csgui Apr 19, 2024
006e24a
tests: runtime error
csgui Apr 19, 2024
eb50ba9
tests: should return an Error type
csgui Apr 24, 2024
7f5a212
tests: add generic assertion for compile-time errors
csgui Jun 5, 2024
928ca33
tests: add crosscheck_expect_failure assertion function
csgui Jun 11, 2024
767a6e1
chore: addressed comments
csgui Jun 17, 2024
8b17a6b
tests: aliases functions test
csgui Jun 17, 2024
8c21687
fix: change unary subtraction implementation
csgui Jun 17, 2024
69b1a4a
chore: change variable case
csgui Jun 17, 2024
cea5ea2
chore: ignoring tests related to issue 385
csgui Jun 17, 2024
b208c0d
feat: map runtime errors to ErrorMap
csgui Jun 18, 2024
ed22e24
tests: validate thrown panic!
csgui Jun 18, 2024
6de16b5
chore: code formatting
csgui Jun 18, 2024
5d169c2
tests: expect failure
csgui Jun 19, 2024
89481fb
chore: addressed comments
csgui Jun 20, 2024
4c2e197
chore: function renaming
csgui Jun 20, 2024
6945d23
Merge branch 'main' into feat/runtime-error-handling
csgui Jun 20, 2024
e06aae3
feat: check for Clarity errors
csgui Jun 21, 2024
165c784
feat: better handling of Clarity errors
csgui Jun 24, 2024
932db01
feat: handling wasm traps
csgui Jun 24, 2024
f9c47ef
feat: error handling in a custom function
csgui Jun 25, 2024
56f600c
addressing some previously ignored tests
csgui Jun 25, 2024
037917d
feat: better handling of default wasm runtime error
csgui Jun 25, 2024
ff79e4f
feat: error handling for non-wasm errors
csgui Jun 26, 2024
41287a9
chore: formatting
csgui Jun 26, 2024
2163125
chore: change enum variant name
csgui Jun 26, 2024
d90f5d1
Merge branch 'main' into feat/runtime-error-handling
csgui Jun 26, 2024
c9caf89
tests: adjustments after updating branch
csgui Jun 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions clar2wasm/src/error_mapping.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
use clarity::vm::errors::{Error, RuntimeErrorType, ShortReturnType};
use clarity::vm::types::ResponseData;
use clarity::vm::Value;
use wasmtime::{AsContextMut, Instance};

const LOG2_ERROR_MESSAGE: &str = "log2 must be passed a positive integer";
const SQRTI_ERROR_MESSAGE: &str = "sqrti must be passed a positive integer";
const POW_ERROR_MESSAGE: &str = "Power argument to (pow ...) must be a u32 integer";

pub enum ErrorMap {
ArithmeticOverflow = 0,
ArithmeticUnderflow = 1,
DivisionByZero = 2,
ArithmeticLog2Error = 3,
ArithmeticSqrtiError = 4,
UnwrapFailure = 5,
Panic = 6,
ShortReturnAssertionFailure = 7,
ArithmeticPowError = 8,
NotMapped = 99,
}

impl ErrorMap {
pub fn from(error_code: i32) -> ErrorMap {
match error_code {
0 => ErrorMap::ArithmeticOverflow,
1 => ErrorMap::ArithmeticUnderflow,
2 => ErrorMap::DivisionByZero,
3 => ErrorMap::ArithmeticLog2Error,
4 => ErrorMap::ArithmeticSqrtiError,
5 => ErrorMap::UnwrapFailure,
6 => ErrorMap::Panic,
7 => ErrorMap::ShortReturnAssertionFailure,
8 => ErrorMap::ArithmeticPowError,
_ => ErrorMap::NotMapped,
}
}
csgui marked this conversation as resolved.
Show resolved Hide resolved
}

pub(crate) fn from_runtime_error_code(instance: Instance, mut store: impl AsContextMut) -> Error {
let global = "runtime-error-code";
let runtime_error_code = instance
.get_global(&mut store, global)
.and_then(|glob| glob.get(&mut store).i32())
// TODO: change that to a proper error when PR below is merged on stacks-core.
// https://github.com/stacks-network/stacks-core/pull/4878 introduces a
// generic error handling for global variables.
.unwrap_or_else(|| panic!("Could not find {global} global with i32 value"));

match ErrorMap::from(runtime_error_code) {
ErrorMap::ArithmeticOverflow => {
Error::Runtime(RuntimeErrorType::ArithmeticOverflow, Some(Vec::new()))
}
ErrorMap::ArithmeticUnderflow => {
Error::Runtime(RuntimeErrorType::ArithmeticUnderflow, Some(Vec::new()))
}
ErrorMap::DivisionByZero => {
Error::Runtime(RuntimeErrorType::DivisionByZero, Some(Vec::new()))
}
ErrorMap::ArithmeticLog2Error => Error::Runtime(
RuntimeErrorType::Arithmetic(LOG2_ERROR_MESSAGE.into()),
Some(Vec::new()),
),
ErrorMap::ArithmeticSqrtiError => Error::Runtime(
RuntimeErrorType::Arithmetic(SQRTI_ERROR_MESSAGE.into()),
Some(Vec::new()),
),
ErrorMap::UnwrapFailure => {
Error::Runtime(RuntimeErrorType::UnwrapFailure, Some(Vec::new()))
}
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)),
krl marked this conversation as resolved.
Show resolved Hide resolved
})),
),
ErrorMap::ArithmeticPowError => Error::Runtime(
RuntimeErrorType::Arithmetic(POW_ERROR_MESSAGE.into()),
Some(Vec::new()),
),
_ => panic!("Runtime error code {} not supported", runtime_error_code),
}
}
3 changes: 2 additions & 1 deletion clar2wasm/src/initialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use clarity::vm::{CallStack, ContractContext, Value};
use stacks_common::types::chainstate::StacksBlockId;
use wasmtime::{Engine, Linker, Module, Store};

use crate::error_mapping;
use crate::linker::link_host_functions;
use crate::wasm_utils::*;

Expand Down Expand Up @@ -368,7 +369,7 @@ pub fn initialize_contract(

top_level
.call(&mut store, &[], results.as_mut_slice())
.map_err(|e| Error::Wasm(WasmError::Runtime(e)))?;
.map_err(|_| error_mapping::from_runtime_error_code(instance, &mut store))?;

// Save the compiled Wasm module into the contract context
store.data_mut().contract_context_mut()?.set_wasm_module(
Expand Down
2 changes: 2 additions & 0 deletions clar2wasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ mod words;
pub mod datastore;
pub mod tools;

mod error_mapping;

// FIXME: This is copied from stacks-blockchain
// Block limit in Stacks 2.1
pub const BLOCK_LIMIT_MAINNET_21: ExecutionCost = ExecutionCost {
Expand Down
41 changes: 31 additions & 10 deletions clar2wasm/src/standard/standard.wat
Original file line number Diff line number Diff line change
Expand Up @@ -231,9 +231,10 @@
;; Useful for debugging, just prints the value
(import "" "log" (func $log (param $value i64)))

;;
;; Global definitions
(global $stack-pointer (mut i32) (i32.const 0))
(export "stack-pointer" (global $stack-pointer))
(memory (export "memory") 10)
(global $runtime-error-code (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 @@ -279,9 +280,10 @@
;; 5: buffer to integer expects a buffer length <= 16
;; 6: panic
;; 7: short return
(func $stdlib.runtime-error (param $error-code i32)
;; TODO: Implement runtime error
unreachable
(func $stdlib.runtime-error (param $error_code i32)
(global.set $runtime-error-code (local.get $error_code))

(unreachable)
)

;; This function can be used to add either signed or unsigned integers
Expand Down Expand Up @@ -1279,7 +1281,11 @@
(i64.gt_u (local.get $b_lo) (i64.const 127))
(i64.ne (local.get $b_hi) (i64.const 0))
)
(then (call $stdlib.runtime-error (i32.const 0)))
;; In the interpreter, overflows for the power operation throw a specific error,
;; not a general ArithmeticOverflow error.
;; Therefore, the runtime error code should be 8,
;; which maps to the specific error.
(then (call $stdlib.runtime-error (i32.const 8)))
)

;; shortcut if a == 2
Expand Down Expand Up @@ -1321,7 +1327,7 @@

;; otherwise, if b < 0 => runtime error
(if (i64.lt_s (local.get $b_hi) (i64.const 0))
(then (call $stdlib.runtime-error (i32.const 4)))
(then (call $stdlib.runtime-error (i32.const 8)))
)

;; if b > (a >= 0 ? 126 : 127) -> runtime error: overflow (since the biggest b that doesn't
Expand All @@ -1333,7 +1339,11 @@
)
(i64.ne (local.get $b_hi) (i64.const 0))
)
(then (call $stdlib.runtime-error (i32.const 0)))
;; In the interpreter, overflows for the power operation throw a specific error,
;; not a general ArithmeticOverflow error.
;; Therefore, the runtime error code should be 8,
;; which maps to the specific error.
(then (call $stdlib.runtime-error (i32.const 8)))
)

;; shortcut if a == 2
Expand Down Expand Up @@ -2765,7 +2775,7 @@
;;
(func $stdlib.to-uint (param $lo i64) (param $hi i64) (result i64 i64)
(if (i64.lt_s (local.get $hi) (i64.const 0))
(then (call $stdlib.runtime-error (i32.const 4)))
(then (call $stdlib.runtime-error (i32.const 1)))
)
(local.get $lo)
(local.get $hi)
Expand All @@ -2784,7 +2794,7 @@
;; Thus, if $hi >= 2^63 the argument is >= 2^127,
;; no matter what is present in $lo.
(if (i64.ge_u (local.get $hi) (i64.const 9223372036854775808))
(then (call $stdlib.runtime-error (i32.const 4)))
(then (call $stdlib.runtime-error (i32.const 0)))
)
(local.get $lo)
(local.get $hi)
Expand Down Expand Up @@ -3405,6 +3415,17 @@
(i32.const 1) (local.get $output-offset) (i32.sub (local.get $writeptr) (local.get $output-offset))
)

;;
;; Export section

;; Memory
(memory (export "memory") 10)

;; Globals
(export "stack-pointer" (global $stack-pointer))
(export "runtime-error-code" (global $runtime-error-code))

;; Functions
(export "stdlib.add-uint" (func $stdlib.add-uint))
(export "stdlib.add-int" (func $stdlib.add-int))
(export "stdlib.sub-uint" (func $stdlib.sub-uint))
Expand Down
56 changes: 30 additions & 26 deletions clar2wasm/src/tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -280,8 +280,8 @@ pub fn evaluate_at_with_amount(
/// Evaluate a Clarity snippet at the latest epoch and clarity version.
/// Returns an optional value -- the result of the evaluation.
#[allow(clippy::result_unit_err)]
pub fn evaluate(snippet: &str) -> Result<Option<Value>, ()> {
evaluate_at(snippet, StacksEpochId::latest(), ClarityVersion::latest()).map_err(|_| ())
pub fn evaluate(snippet: &str) -> Result<Option<Value>, Error> {
evaluate_at(snippet, StacksEpochId::latest(), ClarityVersion::latest())
}

/// Interpret a Clarity snippet at a specific epoch and version.
Expand Down Expand Up @@ -311,25 +311,22 @@ pub fn interpret_at_with_amount(
/// Interprets a Clarity snippet at the latest epoch and clarity version.
/// Returns an optional value -- the result of the evaluation.
#[allow(clippy::result_unit_err)]
pub fn interpret(snippet: &str) -> Result<Option<Value>, ()> {
interpret_at(snippet, StacksEpochId::latest(), ClarityVersion::latest()).map_err(|_| ())
pub fn interpret(snippet: &str) -> Result<Option<Value>, Error> {
interpret_at(snippet, StacksEpochId::latest(), ClarityVersion::latest())
}

pub fn crosscheck(snippet: &str, expected: Result<Option<Value>, ()>) {
pub fn crosscheck(snippet: &str, expected: Result<Option<Value>, Error>) {
let compiled = evaluate_at(snippet, StacksEpochId::latest(), ClarityVersion::latest());
let interpreted = interpret(snippet);

assert_eq!(
compiled.as_ref().map_err(|_| &()),
interpreted.as_ref().map_err(|_| &()),
compiled, interpreted,
"Compiled and interpreted results diverge!\ncompiled: {:?}\ninterpreted: {:?}",
&compiled,
&interpreted
&compiled, &interpreted
);

assert_eq!(
compiled.as_ref().map_err(|_| &()),
expected.as_ref(),
compiled, expected,
"value is not the expected {:?}",
compiled
);
Expand Down Expand Up @@ -370,12 +367,9 @@ pub fn crosscheck_compare_only(snippet: &str) {
let interpreted = interpret(snippet);

assert_eq!(
compiled.as_ref().map_err(|_| &()),
interpreted.as_ref().map_err(|_| &()),
compiled, interpreted,
"Compiled and interpreted results diverge! {}\ncompiled: {:?}\ninterpreted: {:?}",
snippet,
&compiled,
&interpreted
snippet, &compiled, &interpreted
);
}

Expand All @@ -391,12 +385,9 @@ pub fn crosscheck_compare_only_advancing_tip(snippet: &str, count: u32) {
let interpreted = interpreter_env.interpret(snippet).map_err(|_| ());

assert_eq!(
compiled.as_ref().map_err(|_| &()),
interpreted.as_ref().map_err(|_| &()),
compiled, interpreted,
"Compiled and interpreted results diverge! {}\ncompiled: {:?}\ninterpreted: {:?}",
snippet,
&compiled,
&interpreted
snippet, &compiled, &interpreted
);
}

Expand Down Expand Up @@ -430,18 +421,31 @@ pub fn crosscheck_validate<V: Fn(Value)>(snippet: &str, validator: V) {
let interpreted = interpret(snippet);

assert_eq!(
compiled.as_ref().map_err(|_| &()),
interpreted.as_ref().map_err(|_| &()),
compiled, interpreted,
"Compiled and interpreted results diverge! {}\ncompiled: {:?}\ninterpreted: {:?}",
snippet,
&compiled,
&interpreted
snippet, &compiled, &interpreted
);

let value = compiled.unwrap().unwrap();
validator(value)
}

// TODO: After issue #421 is complete,
// several tests that call this function will need to be adjusted.
pub fn crosscheck_expect_failure(snippet: &str) {
let compiled = evaluate(snippet);
let interpreted = interpret(snippet);

assert_eq!(
compiled.is_err(),
interpreted.is_err(),
"Compiled and interpreted results diverge! {}\ncompiled: {:?}\ninterpreted: {:?}",
snippet,
&compiled,
&interpreted
);
}
Acaccia marked this conversation as resolved.
Show resolved Hide resolved

#[test]
fn test_evaluate_snippet() {
assert_eq!(evaluate("(+ 1 2)"), Ok(Some(Value::Int(3))));
Expand Down
4 changes: 3 additions & 1 deletion clar2wasm/src/wasm_generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use walrus::{
MemoryId, Module, ValType,
};

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,
Expand Down Expand Up @@ -182,6 +183,7 @@ pub(crate) fn clar2wasm_ty(ty: &TypeSignature) -> Vec<ValType> {
}
}

#[derive(Debug)]
pub enum SequenceElementType {
/// A byte, from a string-ascii or buffer.
Byte,
Expand Down Expand Up @@ -529,7 +531,7 @@ impl WasmGenerator {
} else {
// This must be from a top-leve statement, so it should cause a runtime error
builder
.i32_const(7)
.i32_const(ErrorMap::ShortReturnAssertionFailure as i32)
.call(self.func_by_name("stdlib.runtime-error"));
builder.unreachable();
}
Expand Down
Loading
Loading