diff --git a/clarity-types/src/tests/errors.rs b/clarity-types/src/tests/errors.rs
new file mode 100644
index 00000000000..52e49d95ee2
--- /dev/null
+++ b/clarity-types/src/tests/errors.rs
@@ -0,0 +1,561 @@
+// Copyright (C) 2026 Stacks Open Internet Foundation
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+use crate::errors::analysis::{CheckErrorKind, StaticCheckError, SyntaxBindingError};
+use crate::errors::ast::{ParseError, ParseErrorKind};
+use crate::errors::cost::CostErrors;
+use crate::errors::{EarlyReturnError, RuntimeError, VmExecutionError, VmInternalError};
+use crate::execution_cost::ExecutionCost;
+use crate::types::{TypeSignature, Value};
+
+/// Test that CostErrors and CheckErrors are properly separated.
+/// Tests all CostErrors variants convert correctly to CheckErrorKind.
+#[test]
+fn test_cost_error_to_check_error_conversion() {
+ // CostOverflow
+ let cost_overflow = CostErrors::CostOverflow;
+ let check_err: CheckErrorKind = cost_overflow.into();
+ assert!(matches!(check_err, CheckErrorKind::CostOverflow));
+
+ // CostBalanceExceeded
+ let cost_balance = CostErrors::CostBalanceExceeded(
+ ExecutionCost::ZERO,
+ ExecutionCost {
+ write_length: 100,
+ write_count: 10,
+ read_length: 200,
+ read_count: 20,
+ runtime: 1000,
+ },
+ );
+ let check_err: CheckErrorKind = cost_balance.into();
+ assert!(matches!(
+ check_err,
+ CheckErrorKind::CostBalanceExceeded(
+ ExecutionCost::ZERO,
+ ExecutionCost {
+ write_length: 100,
+ write_count: 10,
+ read_length: 200,
+ read_count: 20,
+ runtime: 1000,
+ }
+ )
+ ));
+
+ // MemoryBalanceExceeded
+ let mem_balance = CostErrors::MemoryBalanceExceeded(1000, 500);
+ let check_err: CheckErrorKind = mem_balance.into();
+ assert!(matches!(
+ check_err,
+ CheckErrorKind::MemoryBalanceExceeded(1000, 500)
+ ));
+
+ // CostContractLoadFailure
+ let cost_contract_load = CostErrors::CostContractLoadFailure;
+ let check_err: CheckErrorKind = cost_contract_load.into();
+ assert!(matches!(
+ check_err,
+ CheckErrorKind::CostComputationFailed(ref msg) if msg == "Failed to load cost contract"
+ ));
+
+ // CostComputationFailed
+ let cost_computation = CostErrors::CostComputationFailed("test failure".into());
+ let check_err: CheckErrorKind = cost_computation.into();
+ assert!(matches!(
+ check_err,
+ CheckErrorKind::CostComputationFailed(ref msg) if msg == "test failure"
+ ));
+
+ // ExecutionTimeExpired
+ let time_expired = CostErrors::ExecutionTimeExpired;
+ let check_err: CheckErrorKind = time_expired.into();
+ assert!(matches!(check_err, CheckErrorKind::ExecutionTimeExpired));
+
+ // InterpreterFailure
+ let interpreter_failure = CostErrors::InterpreterFailure;
+ let check_err: CheckErrorKind = interpreter_failure.into();
+ assert!(matches!(
+ check_err,
+ CheckErrorKind::Expects(ref msg) if msg == "Unexpected interpreter failure in cost computation"
+ ));
+
+ // Expect
+ let expect_err = CostErrors::Expect("unexpected condition".into());
+ let check_err: CheckErrorKind = expect_err.into();
+ assert!(matches!(
+ check_err,
+ CheckErrorKind::Expects(ref msg) if msg == "unexpected condition"
+ ));
+}
+
+/// Test that CostErrors and ParseErrors are properly separated.
+/// Tests all CostErrors variants convert correctly to ParseErrorKind.
+#[test]
+fn test_cost_error_to_parse_error_conversion() {
+ // CostOverflow
+ let cost_overflow = CostErrors::CostOverflow;
+ let parse_err: ParseError = cost_overflow.into();
+ assert!(matches!(*parse_err.err, ParseErrorKind::CostOverflow));
+
+ // CostBalanceExceeded
+ let cost_balance = CostErrors::CostBalanceExceeded(
+ ExecutionCost::ZERO,
+ ExecutionCost {
+ write_length: 100,
+ write_count: 10,
+ read_length: 200,
+ read_count: 20,
+ runtime: 1000,
+ },
+ );
+ let parse_err: ParseError = cost_balance.into();
+ assert!(matches!(
+ *parse_err.err,
+ ParseErrorKind::CostBalanceExceeded(
+ ExecutionCost::ZERO,
+ ExecutionCost {
+ write_length: 100,
+ write_count: 10,
+ read_length: 200,
+ read_count: 20,
+ runtime: 1000,
+ }
+ )
+ ));
+
+ // MemoryBalanceExceeded
+ let mem_balance = CostErrors::MemoryBalanceExceeded(2000, 1000);
+ let parse_err: ParseError = mem_balance.into();
+ assert!(matches!(
+ *parse_err.err,
+ ParseErrorKind::MemoryBalanceExceeded(2000, 1000)
+ ));
+
+ // CostContractLoadFailure
+ let cost_contract_load = CostErrors::CostContractLoadFailure;
+ let parse_err: ParseError = cost_contract_load.into();
+ assert!(matches!(
+ *parse_err.err,
+ ParseErrorKind::CostComputationFailed(ref msg) if msg == "Failed to load cost contract"
+ ));
+
+ // CostComputationFailed
+ let cost_computation = CostErrors::CostComputationFailed("parse test failure".into());
+ let parse_err: ParseError = cost_computation.into();
+ assert!(matches!(
+ *parse_err.err,
+ ParseErrorKind::CostComputationFailed(ref msg) if msg == "parse test failure"
+ ));
+
+ // ExecutionTimeExpired
+ let time_expired = CostErrors::ExecutionTimeExpired;
+ let parse_err: ParseError = time_expired.into();
+ assert!(matches!(
+ *parse_err.err,
+ ParseErrorKind::ExecutionTimeExpired
+ ));
+
+ // InterpreterFailure
+ let interpreter_failure = CostErrors::InterpreterFailure;
+ let parse_err: ParseError = interpreter_failure.into();
+ assert!(matches!(*parse_err.err, ParseErrorKind::InterpreterFailure));
+
+ // Expect
+ let expect_err = CostErrors::Expect("parse unexpected condition".into());
+ let parse_err: ParseError = expect_err.into();
+ assert!(matches!(*parse_err.err, ParseErrorKind::InterpreterFailure));
+}
+
+/// Test that rejectable() method correctly identifies consensus-critical errors.
+/// Only specific errors should be able to invalidate blocks.
+/// This test comprehensively covers all rejectable and non-rejectable variants.
+#[test]
+fn test_rejectable_errors_consensus_critical() {
+ // CheckErrorKind - ALL rejectable variants
+ assert!(CheckErrorKind::SupertypeTooLarge.rejectable());
+ assert!(CheckErrorKind::Expects("test".into()).rejectable());
+
+ // CheckErrorKind - Representative non-rejectable variants
+ assert!(!CheckErrorKind::CostOverflow.rejectable());
+ assert!(
+ !CheckErrorKind::CostBalanceExceeded(ExecutionCost::ZERO, ExecutionCost::ZERO).rejectable()
+ );
+ assert!(!CheckErrorKind::MemoryBalanceExceeded(100, 50).rejectable());
+ assert!(!CheckErrorKind::CostComputationFailed("test".into()).rejectable());
+ assert!(!CheckErrorKind::ExecutionTimeExpired.rejectable());
+ assert!(
+ !CheckErrorKind::TypeError(
+ Box::new(TypeSignature::IntType),
+ Box::new(TypeSignature::BoolType)
+ )
+ .rejectable()
+ );
+ assert!(!CheckErrorKind::ValueTooLarge.rejectable());
+ assert!(!CheckErrorKind::TypeSignatureTooDeep.rejectable());
+
+ // CostErrors - ALL rejectable variants
+ assert!(CostErrors::InterpreterFailure.rejectable());
+ assert!(CostErrors::Expect("test".into()).rejectable());
+
+ // CostErrors - ALL non-rejectable variants
+ assert!(!CostErrors::CostOverflow.rejectable());
+ assert!(
+ !CostErrors::CostBalanceExceeded(ExecutionCost::ZERO, ExecutionCost::ZERO).rejectable()
+ );
+ assert!(!CostErrors::MemoryBalanceExceeded(100, 50).rejectable());
+ assert!(!CostErrors::CostContractLoadFailure.rejectable());
+ assert!(!CostErrors::CostComputationFailed("test".into()).rejectable());
+ assert!(!CostErrors::ExecutionTimeExpired.rejectable());
+
+ // ParseError - ALL rejectable variants
+ assert!(ParseError::new(ParseErrorKind::InterpreterFailure).rejectable());
+ assert!(ParseError::new(ParseErrorKind::ExpressionStackDepthTooDeep).rejectable());
+ assert!(ParseError::new(ParseErrorKind::VaryExpressionStackDepthTooDeep).rejectable());
+
+ // ParseError - Representative non-rejectable variants
+ assert!(!ParseError::new(ParseErrorKind::CostOverflow).rejectable());
+ assert!(
+ !ParseError::new(ParseErrorKind::CostBalanceExceeded(
+ ExecutionCost::ZERO,
+ ExecutionCost::ZERO
+ ))
+ .rejectable()
+ );
+ assert!(!ParseError::new(ParseErrorKind::MemoryBalanceExceeded(100, 50)).rejectable());
+ assert!(!ParseError::new(ParseErrorKind::ExecutionTimeExpired).rejectable());
+ assert!(!ParseError::new(ParseErrorKind::TooManyExpressions).rejectable());
+ assert!(!ParseError::new(ParseErrorKind::ProgramTooLarge).rejectable());
+
+ // StaticCheckError - Inherits rejectable status from CheckErrorKind
+ let rejectable_static = StaticCheckError::new(CheckErrorKind::SupertypeTooLarge);
+ assert!(rejectable_static.err.rejectable());
+ let non_rejectable_static = StaticCheckError::new(CheckErrorKind::CostOverflow);
+ assert!(!non_rejectable_static.err.rejectable());
+}
+
+/// Test that CostErrors to CheckError conversions preserve rejectable status.
+/// This is critical for consensus - we must not accidentally make
+/// a non-rejectable error rejectable or vice versa.
+#[test]
+fn test_cost_error_conversion_check_error_preserves_rejectable_status() {
+ // Rejectable CostErrors should remain rejectable after conversion to CheckError
+ let cost_expect = CostErrors::Expect("test".into());
+ assert!(cost_expect.rejectable());
+ let check_err: CheckErrorKind = cost_expect.into();
+ assert!(check_err.rejectable());
+
+ let cost_interpreter = CostErrors::InterpreterFailure;
+ assert!(cost_interpreter.rejectable());
+ // InterpreterFailure converts to Expects variant in CheckErrorKind
+ let check_err: CheckErrorKind = cost_interpreter.into();
+ assert!(check_err.rejectable());
+
+ // Non-rejectable CostErrors should remain non-rejectable
+ let cost_overflow = CostErrors::CostOverflow;
+ assert!(!cost_overflow.rejectable());
+ let check_err: CheckErrorKind = cost_overflow.into();
+ assert!(!check_err.rejectable());
+}
+
+/// Test that CostErrors to ParseError conversions preserve rejectable status.
+#[test]
+fn test_cost_error_conversion_parse_error_preserves_rejectable_status() {
+ // Rejectable CostErrors should remain rejectable after conversion to ParseError
+ let cost_expect = CostErrors::Expect("test".into());
+ assert!(cost_expect.rejectable());
+ let parse_err: ParseError = cost_expect.into();
+ assert!(parse_err.rejectable());
+
+ let cost_interpreter = CostErrors::InterpreterFailure;
+ assert!(cost_interpreter.rejectable());
+ let parse_err: ParseError = cost_interpreter.into();
+ assert!(parse_err.rejectable());
+
+ // Non-rejectable CostErrors should remain non-rejectable
+ let cost_overflow = CostErrors::CostOverflow;
+ assert!(!cost_overflow.rejectable());
+ let parse_err: ParseError = cost_overflow.into();
+ assert!(!parse_err.rejectable());
+}
+
+/// Test that SyntaxBindingError properly converts to CheckErrorKind.
+/// Ensures binding errors are categorized correctly.
+#[test]
+fn test_syntax_binding_error_conversion() {
+ let let_binding_err = SyntaxBindingError::let_binding_not_list(0);
+ let check_err: CheckErrorKind = let_binding_err.into();
+ assert!(matches!(check_err, CheckErrorKind::BadSyntaxBinding(_)));
+
+ let eval_binding_err = SyntaxBindingError::eval_binding_invalid_length(1);
+ let check_err: CheckErrorKind = eval_binding_err.into();
+ assert!(matches!(check_err, CheckErrorKind::BadSyntaxBinding(_)));
+
+ let tuple_cons_err = SyntaxBindingError::tuple_cons_not_atom(2);
+ let check_err: CheckErrorKind = tuple_cons_err.into();
+ assert!(matches!(check_err, CheckErrorKind::BadSyntaxBinding(_)));
+}
+
+/// Test that VmExecutionError conversions are correct.
+/// Different error types should convert to the appropriate VmExecutionError variant.
+#[test]
+fn test_vm_execution_error_conversions() {
+ // RuntimeError should convert to VmExecutionError::Runtime
+ let runtime_err = RuntimeError::DivisionByZero;
+ let vm_err: VmExecutionError = runtime_err.into();
+ assert!(matches!(
+ vm_err,
+ VmExecutionError::Runtime(RuntimeError::DivisionByZero, None)
+ ));
+
+ // CheckErrorKind should convert to VmExecutionError::Unchecked
+ let check_err = CheckErrorKind::CostOverflow;
+ let vm_err: VmExecutionError = check_err.into();
+ assert!(matches!(
+ vm_err,
+ VmExecutionError::Unchecked(CheckErrorKind::CostOverflow)
+ ));
+
+ // EarlyReturnError should convert to VmExecutionError::EarlyReturn
+ let early_err = EarlyReturnError::UnwrapFailed(Box::new(Value::none()));
+ let vm_err: VmExecutionError = early_err.into();
+ assert!(matches!(vm_err, VmExecutionError::EarlyReturn(_)));
+
+ // VmInternalError should convert to VmExecutionError::Internal
+ let internal_err = VmInternalError::InvariantViolation("test".into());
+ let vm_err: VmExecutionError = internal_err.into();
+ assert!(matches!(vm_err, VmExecutionError::Internal(_)));
+}
+
+/// Test that ParseError from CostErrors maintains proper structure.
+#[test]
+fn test_parse_error_from_cost_errors_structure() {
+ let cost_computation_failed = CostErrors::CostComputationFailed("test failure".into());
+ let parse_err: ParseError = cost_computation_failed.into();
+
+ assert!(
+ matches!(*parse_err.err, ParseErrorKind::CostComputationFailed(ref msg) if msg == "test failure"),
+ "Expected CostComputationFailed with correct message"
+ );
+}
+
+/// Test that StaticCheckError from CostErrors maintains diagnostic info.
+#[test]
+fn test_static_check_error_from_cost_errors() {
+ let cost_overflow = CostErrors::CostOverflow;
+ let static_err: StaticCheckError = cost_overflow.into();
+
+ assert!(matches!(*static_err.err, CheckErrorKind::CostOverflow));
+ assert!(!static_err.has_expression());
+}
+
+/// Test that VmExecutionError equality ignores stack traces.
+#[test]
+fn vm_execution_error_equality_ignores_stack_traces() {
+ // Runtime ignores stack traces
+ assert_eq!(
+ VmExecutionError::Runtime(RuntimeError::DivisionByZero, None),
+ VmExecutionError::Runtime(RuntimeError::DivisionByZero, Some(vec![])),
+ );
+
+ // But the underlying runtime error still matters
+ assert_ne!(
+ VmExecutionError::Runtime(RuntimeError::DivisionByZero, None),
+ VmExecutionError::Runtime(RuntimeError::ArithmeticOverflow, None),
+ );
+
+ // And variants still matter
+ assert_ne!(
+ VmExecutionError::Runtime(RuntimeError::DivisionByZero, None),
+ VmExecutionError::Unchecked(CheckErrorKind::CostOverflow),
+ );
+}
+
+/// Test that execution time expiry is consistently handled across error types.
+#[test]
+fn test_execution_time_expiry_consistency() {
+ // CostErrors::ExecutionTimeExpired
+ let cost_err = CostErrors::ExecutionTimeExpired;
+ assert!(!cost_err.rejectable()); // Time expiry shouldn't invalidate blocks
+
+ // Convert to CheckErrorKind
+ let check_err: CheckErrorKind = cost_err.into();
+ assert!(matches!(check_err, CheckErrorKind::ExecutionTimeExpired));
+ assert!(!check_err.rejectable());
+
+ // Convert to ParseError
+ let parse_err: ParseError = CostErrors::ExecutionTimeExpired.into();
+ assert!(matches!(
+ *parse_err.err,
+ ParseErrorKind::ExecutionTimeExpired
+ ));
+ assert!(!parse_err.rejectable());
+}
+
+/// Test that memory balance exceeded errors are handled consistently.
+#[test]
+fn test_memory_balance_exceeded_consistency() {
+ let used = 2000u64;
+ let limit = 1000u64;
+
+ // In CostErrors
+ let cost_err = CostErrors::MemoryBalanceExceeded(used, limit);
+ assert!(!cost_err.rejectable());
+
+ // Convert to CheckErrorKind
+ let check_err: CheckErrorKind = cost_err.into();
+ assert!(
+ matches!(check_err, CheckErrorKind::MemoryBalanceExceeded(u, l) if u == used && l == limit),
+ "Expected MemoryBalanceExceeded variant with correct values"
+ );
+
+ // Convert to ParseError
+ let parse_err: ParseError = CostErrors::MemoryBalanceExceeded(used, limit).into();
+ assert!(
+ matches!(*parse_err.err, ParseErrorKind::MemoryBalanceExceeded(u, l) if u == used && l == limit),
+ "Expected MemoryBalanceExceeded variant with correct values"
+ );
+}
+
+/// Test that error conversions from Expect variants are properly handled.
+#[test]
+fn test_expect_error_handling() {
+ // CostErrors::Expect should be rejectable (indicates a bug)
+ let cost_expect = CostErrors::Expect("unexpected condition".into());
+ assert!(cost_expect.rejectable());
+
+ // CheckErrorKind::Expects should be rejectable (indicates a bug)
+ let check_expect = CheckErrorKind::Expects("unexpected condition".into());
+ assert!(check_expect.rejectable());
+
+ // Conversion to VmExecutionError
+ let vm_err: VmExecutionError = cost_expect.into();
+ assert!(
+ matches!(
+ vm_err,
+ VmExecutionError::Internal(VmInternalError::Expect(_))
+ ),
+ "Expect errors should convert to Internal VmExecutionError"
+ );
+}
+
+/// Test that cost computation failures are properly categorized.
+#[test]
+fn test_cost_computation_failure_categorization() {
+ let failure_msg = "cost computation failed";
+
+ // In CostErrors
+ let cost_err = CostErrors::CostComputationFailed(failure_msg.into());
+ assert!(!cost_err.rejectable()); // Computation failures shouldn't invalidate blocks
+
+ // Convert to CheckErrorKind
+ let check_err: CheckErrorKind = cost_err.into();
+ assert!(
+ matches!(check_err, CheckErrorKind::CostComputationFailed(ref msg) if msg == failure_msg),
+ "Expected CostComputationFailed variant with correct message"
+ );
+}
+
+/// Test boundary conditions for argument count errors.
+#[test]
+fn test_argument_count_error_boundaries() {
+ use crate::errors::analysis::{
+ check_argument_count, check_arguments_at_least, check_arguments_at_most,
+ };
+
+ // Exact count
+ let args = vec![1, 2, 3];
+ assert!(check_argument_count(3, &args).is_ok());
+ assert!(check_argument_count(2, &args).is_err());
+ assert!(check_argument_count(4, &args).is_err());
+
+ // At least
+ assert!(check_arguments_at_least(2, &args).is_ok());
+ assert!(check_arguments_at_least(3, &args).is_ok());
+ assert!(check_arguments_at_least(4, &args).is_err());
+
+ // At most
+ assert!(check_arguments_at_most(4, &args).is_ok());
+ assert!(check_arguments_at_most(3, &args).is_ok());
+ assert!(check_arguments_at_most(2, &args).is_err());
+}
+
+/// Test cost balance exceeded with various execution costs.
+/// Tests that all cost fields are preserved correctly, not just runtime.
+#[test]
+fn test_cost_balance_exceeded_variants() {
+ // Test where only runtime exceeds (other fields are low)
+ let runtime_exceeded = ExecutionCost {
+ write_length: 10,
+ write_count: 5,
+ read_length: 20,
+ read_count: 8,
+ runtime: 1000,
+ };
+ let runtime_limit = ExecutionCost {
+ write_length: 100,
+ write_count: 50,
+ read_length: 200,
+ read_count: 80,
+ runtime: 500,
+ };
+ let err = CostErrors::CostBalanceExceeded(runtime_exceeded, runtime_limit);
+ assert!(!err.rejectable());
+ let check_err: CheckErrorKind = err.into();
+ assert!(
+ matches!(
+ check_err,
+ CheckErrorKind::CostBalanceExceeded(ref used, ref limit)
+ if used.write_length == 10 && used.write_count == 5
+ && used.read_length == 20 && used.read_count == 8
+ && used.runtime == 1000
+ && limit.write_length == 100 && limit.write_count == 50
+ && limit.read_length == 200 && limit.read_count == 80
+ && limit.runtime == 500
+ ),
+ "Expected CostBalanceExceeded with all cost fields preserved"
+ );
+
+ // Test where only write_length exceeds (other fields are low)
+ let write_exceeded = ExecutionCost {
+ write_length: 5000,
+ write_count: 2,
+ read_length: 10,
+ read_count: 3,
+ runtime: 100,
+ };
+ let write_limit = ExecutionCost {
+ write_length: 1000,
+ write_count: 50,
+ read_length: 1000,
+ read_count: 50,
+ runtime: 10000,
+ };
+ let err = CostErrors::CostBalanceExceeded(write_exceeded, write_limit);
+ let check_err: CheckErrorKind = err.into();
+ assert!(
+ matches!(
+ check_err,
+ CheckErrorKind::CostBalanceExceeded(ref used, ref limit)
+ if used.write_length == 5000 && used.write_count == 2
+ && used.read_length == 10 && used.read_count == 3
+ && used.runtime == 100
+ && limit.write_length == 1000 && limit.write_count == 50
+ && limit.read_length == 1000 && limit.read_count == 50
+ && limit.runtime == 10000
+ ),
+ "Expected CostBalanceExceeded with all cost fields preserved"
+ );
+}
diff --git a/clarity-types/src/tests/mod.rs b/clarity-types/src/tests/mod.rs
index a84ce43ef50..2de44799161 100644
--- a/clarity-types/src/tests/mod.rs
+++ b/clarity-types/src/tests/mod.rs
@@ -12,6 +12,7 @@
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
+mod errors;
mod representations;
mod types;