diff --git a/.github/workflows/cargo-hack-check.yml b/.github/workflows/cargo-hack-check.yml
index c1f208562d..7fff9a2873 100644
--- a/.github/workflows/cargo-hack-check.yml
+++ b/.github/workflows/cargo-hack-check.yml
@@ -78,11 +78,12 @@ jobs:
- name: "Clarity & Stacks-Common WASM Web"
command: |
cargo hack check \
+ -p clarity \
-p clarity-serialization \
-p stacks-common \
--each-feature \
--no-dev-deps \
- --exclude-features=default,rusqlite,ctrlc-handler,wasm-deterministic \
+ --exclude-features=default,rusqlite,ctrlc-handler,wasm-deterministic,testing \
--features=wasm-web
- name: "Clarity & Stacks-Common WASM Deterministic"
diff --git a/Cargo.lock b/Cargo.lock
index f7c0905f54..0ab32a2dec 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -605,7 +605,7 @@ name = "clarity"
version = "0.0.1"
dependencies = [
"assert-json-diff 1.1.0",
- "hashbrown 0.15.2",
+ "clarity-serialization",
"integer-sqrt",
"lazy_static",
"mutants",
@@ -652,11 +652,12 @@ dependencies = [
"mutants",
"regex",
"rstest",
+ "rusqlite",
"serde",
"serde_derive",
+ "serde_json",
"slog",
"stacks-common 0.0.1",
- "thiserror",
]
[[package]]
@@ -3356,7 +3357,6 @@ dependencies = [
"chrono",
"clarity 0.0.1",
"ed25519-dalek",
- "hashbrown 0.15.2",
"lazy_static",
"libstackerdb 0.0.1",
"mio 0.6.23",
diff --git a/clarity-serialization/Cargo.toml b/clarity-serialization/Cargo.toml
index 341d8c0c29..f7518e3a18 100644
--- a/clarity-serialization/Cargo.toml
+++ b/clarity-serialization/Cargo.toml
@@ -12,11 +12,12 @@ readme = "README.md"
[dependencies]
lazy_static = { workspace = true }
regex = { version = "1", default-features = false }
+rusqlite = { workspace = true, optional = true }
serde = { workspace = true }
+serde_json = { version = "1.0", default-features = false }
serde_derive = { workspace = true }
slog = { workspace = true }
stacks_common = { package = "stacks-common", path = "../stacks-common", default-features = false }
-thiserror = { workspace = true }
[dev-dependencies]
mutants = "0.0.3"
@@ -25,7 +26,9 @@ rstest = "0.17.0"
[features]
default = []
testing = []
+developer-mode = ["stacks_common/developer-mode"]
slog_json = ["stacks_common/slog_json"]
+rusqlite = ["stacks_common/rusqlite", "dep:rusqlite"]
# Wasm-specific features for easier configuration
wasm-web = ["stacks_common/wasm-web"]
diff --git a/clarity-serialization/src/diagnostic.rs b/clarity-serialization/src/diagnostic.rs
new file mode 100644
index 0000000000..c8aeda8e6c
--- /dev/null
+++ b/clarity-serialization/src/diagnostic.rs
@@ -0,0 +1,90 @@
+// Copyright (C) 2025 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 std::fmt;
+
+use crate::representations::Span;
+
+/// In a near future, we can go further in our static analysis and provide different levels
+/// of diagnostics, such as warnings, hints, best practices, etc.
+#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
+pub enum Level {
+ Note,
+ Warning,
+ Error,
+}
+
+pub trait DiagnosableError {
+ fn message(&self) -> String;
+ fn suggestion(&self) -> Option;
+ fn level(&self) -> Level {
+ Level::Error
+ }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
+pub struct Diagnostic {
+ pub level: Level,
+ pub message: String,
+ pub spans: Vec,
+ pub suggestion: Option,
+}
+
+impl Diagnostic {
+ pub fn err(error: &dyn DiagnosableError) -> Diagnostic {
+ Diagnostic {
+ spans: vec![],
+ level: Level::Error,
+ message: error.message(),
+ suggestion: error.suggestion(),
+ }
+ }
+
+ pub fn add_span(&mut self, start_line: u32, start_column: u32, end_line: u32, end_column: u32) {
+ self.spans.push(Span {
+ start_line,
+ start_column,
+ end_line,
+ end_column,
+ });
+ }
+}
+
+impl fmt::Display for Diagnostic {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "{:?}", self.level)?;
+ match self.spans.len().cmp(&1) {
+ std::cmp::Ordering::Equal => write!(
+ f,
+ " (line {}, column {})",
+ self.spans[0].start_line, self.spans[0].start_column
+ )?,
+ std::cmp::Ordering::Greater => {
+ let lines: Vec = self
+ .spans
+ .iter()
+ .map(|s| format!("line: {}", s.start_line))
+ .collect();
+ write!(f, " ({})", lines.join(", "))?;
+ }
+ _ => {}
+ }
+ write!(f, ": {}.", &self.message)?;
+ if let Some(suggestion) = &self.suggestion {
+ write!(f, "\n{suggestion}")?;
+ }
+ writeln!(f)
+ }
+}
diff --git a/clarity-serialization/src/errors.rs b/clarity-serialization/src/errors.rs
deleted file mode 100644
index c0b6428a82..0000000000
--- a/clarity-serialization/src/errors.rs
+++ /dev/null
@@ -1,129 +0,0 @@
-// Copyright (C) 2025 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 std::io;
-
-use thiserror::Error;
-
-use crate::types::{TupleTypeSignature, TypeSignature, Value};
-
-/// The primary error type for the `clarity-codec` crate.
-///
-/// It represents all possible failures that can occur when encoding, decoding,
-/// or validating the structure and types of a Clarity value.
-#[derive(Error, Debug)]
-pub enum CodecError {
- #[error("I/O error during (de)serialization: {0}")]
- Io(#[from] io::Error),
-
- #[error("Serialization error caused by IO: {0}")]
- Serialization(String),
-
- #[error("Deserialization failed: {0}")]
- Deserialization(String),
-
- #[error("Deserialization expected the type of the input to be: {0}")]
- DeserializeExpected(Box),
-
- #[error("The serializer handled an input in an unexpected way")]
- UnexpectedSerialization,
-
- #[error("Deserialization finished but there were leftover bytes in the buffer")]
- LeftoverBytesInDeserialization,
-
- #[error("Parse error: {0}")]
- ParseError(String),
-
- #[error("Bad type construction.")]
- BadTypeConstruction,
-
- // --- Structural and Size Errors ---
- #[error("A value being constructed is larger than the 1MB Clarity limit")]
- ValueTooLarge,
-
- #[error("A value is out of its prescribed bounds")]
- ValueOutOfBounds,
-
- #[error("A type signature is deeper than the 32-level Clarity limit")]
- TypeSignatureTooDeep,
-
- #[error("The supertype of two types is too large to be represented")]
- SupertypeTooLarge,
-
- #[error("Empty tuples are not allowed")]
- EmptyTuplesNotAllowed,
-
- #[error("Failed to construct a tuple with the given type")]
- FailureConstructingTupleWithType,
-
- #[error("Failed to construct a list with the given type")]
- FailureConstructingListWithType,
-
- #[error("All elements in a list must have a compatible supertype")]
- ListTypesMustMatch,
-
- // --- Type Mismatch and Semantic Errors ---
- #[error("Expected a value of type '{expected}', but found a value of type '{found}'")]
- TypeError {
- expected: Box,
- found: Box,
- },
-
- #[error("Expected a value of type '{expected}', but found the value '{found}'")]
- TypeValueError {
- expected: Box,
- found: Box,
- },
-
- #[error("could not determine the input type for the serialization function")]
- CouldNotDetermineSerializationType,
-
- #[error("type of expression cannot be determined")]
- CouldNotDetermineType,
-
- // --- Naming and Identifier Errors ---
- #[error("Name '{0}' is already used in this tuple")]
- NameAlreadyUsedInTuple(String),
-
- #[error("Could not find field '{0}' in tuple '{1}'")]
- NoSuchTupleField(String, TupleTypeSignature),
-
- #[error("Failed to parse {0}: {1}")]
- InvalidClarityName(&'static str, String),
-
- #[error("Failed to parse {0}: {1}")]
- InvalidContractName(&'static str, String),
-
- // --- String/Buffer Content Errors ---
- #[error("Invalid characters detected in string")]
- InvalidStringCharacters,
-
- #[error("Invalid UTF-8 encoding in string")]
- InvalidUtf8Encoding,
-
- // --- Catch-all for internal logic errors ---
- #[error("An unexpected internal error occurred: {0}")]
- Expect(String),
-}
-
-// Implement PartialEq for testing and simple equality checks by comparing the
-// string representations of each error. This avoids requiring all wrapped
-// fields (like `std::io::Error`) to implement PartialEq.
-#[cfg(any(test, feature = "testing"))]
-impl PartialEq for CodecError {
- fn eq(&self, other: &Self) -> bool {
- self.to_string() == other.to_string()
- }
-}
diff --git a/clarity-serialization/src/errors/analysis.rs b/clarity-serialization/src/errors/analysis.rs
new file mode 100644
index 0000000000..8831198579
--- /dev/null
+++ b/clarity-serialization/src/errors/analysis.rs
@@ -0,0 +1,505 @@
+// Copyright (C) 2025 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 std::{error, fmt};
+
+use crate::diagnostic::{DiagnosableError, Diagnostic};
+use crate::errors::CostErrors;
+use crate::execution_cost::ExecutionCost;
+use crate::representations::SymbolicExpression;
+use crate::types::{TraitIdentifier, TupleTypeSignature, TypeSignature, Value};
+
+pub type CheckResult = Result;
+
+#[derive(Debug, PartialEq)]
+pub enum CheckErrors {
+ // cost checker errors
+ CostOverflow,
+ CostBalanceExceeded(ExecutionCost, ExecutionCost),
+ MemoryBalanceExceeded(u64, u64),
+ CostComputationFailed(String),
+
+ ValueTooLarge,
+ ValueOutOfBounds,
+ TypeSignatureTooDeep,
+ ExpectedName,
+ SupertypeTooLarge,
+
+ // unexpected interpreter behavior
+ Expects(String),
+
+ // match errors
+ BadMatchOptionSyntax(Box),
+ BadMatchResponseSyntax(Box),
+ BadMatchInput(TypeSignature),
+
+ // list typing errors
+ UnknownListConstructionFailure,
+ ListTypesMustMatch,
+ ConstructedListTooLarge,
+
+ // simple type expectation mismatch
+ TypeError(TypeSignature, TypeSignature),
+ TypeLiteralError(TypeSignature, TypeSignature),
+ TypeValueError(TypeSignature, Value),
+
+ NoSuperType(TypeSignature, TypeSignature),
+ InvalidTypeDescription,
+ UnknownTypeName(String),
+
+ // union type mismatch
+ UnionTypeError(Vec, TypeSignature),
+ UnionTypeValueError(Vec, Value),
+
+ ExpectedLiteral,
+ ExpectedOptionalType(TypeSignature),
+ ExpectedResponseType(TypeSignature),
+ ExpectedOptionalOrResponseType(TypeSignature),
+ ExpectedOptionalValue(Value),
+ ExpectedResponseValue(Value),
+ ExpectedOptionalOrResponseValue(Value),
+ CouldNotDetermineResponseOkType,
+ CouldNotDetermineResponseErrType,
+ CouldNotDetermineSerializationType,
+ UncheckedIntermediaryResponses,
+
+ CouldNotDetermineMatchTypes,
+ CouldNotDetermineType,
+
+ // Checker runtime failures
+ TypeAlreadyAnnotatedFailure,
+ TypeAnnotationExpectedFailure,
+ CheckerImplementationFailure,
+
+ // Assets
+ BadTokenName,
+ DefineFTBadSignature,
+ DefineNFTBadSignature,
+ NoSuchNFT(String),
+ NoSuchFT(String),
+
+ BadTransferSTXArguments,
+ BadTransferFTArguments,
+ BadTransferNFTArguments,
+ BadMintFTArguments,
+ BadBurnFTArguments,
+
+ // tuples
+ BadTupleFieldName,
+ ExpectedTuple(TypeSignature),
+ NoSuchTupleField(String, TupleTypeSignature),
+ EmptyTuplesNotAllowed,
+ BadTupleConstruction,
+ TupleExpectsPairs,
+
+ // variables
+ NoSuchDataVariable(String),
+
+ // data map
+ BadMapName,
+ NoSuchMap(String),
+
+ // defines
+ DefineFunctionBadSignature,
+ BadFunctionName,
+ BadMapTypeDefinition,
+ PublicFunctionMustReturnResponse(TypeSignature),
+ DefineVariableBadSignature,
+ ReturnTypesMustMatch(TypeSignature, TypeSignature),
+
+ CircularReference(Vec),
+
+ // contract-call errors
+ NoSuchContract(String),
+ NoSuchPublicFunction(String, String),
+ PublicFunctionNotReadOnly(String, String),
+ ContractAlreadyExists(String),
+ ContractCallExpectName,
+ ExpectedCallableType(TypeSignature),
+
+ // get-block-info? errors
+ NoSuchBlockInfoProperty(String),
+ NoSuchBurnBlockInfoProperty(String),
+ NoSuchStacksBlockInfoProperty(String),
+ NoSuchTenureInfoProperty(String),
+ GetBlockInfoExpectPropertyName,
+ GetBurnBlockInfoExpectPropertyName,
+ GetStacksBlockInfoExpectPropertyName,
+ GetTenureInfoExpectPropertyName,
+
+ NameAlreadyUsed(String),
+ ReservedWord(String),
+
+ // expect a function, or applying a function to a list
+ NonFunctionApplication,
+ ExpectedListApplication,
+ ExpectedSequence(TypeSignature),
+ MaxLengthOverflow,
+
+ // let syntax
+ BadLetSyntax,
+
+ // generic binding syntax
+ BadSyntaxBinding,
+ BadSyntaxExpectedListOfPairs,
+
+ MaxContextDepthReached,
+ UndefinedFunction(String),
+ UndefinedVariable(String),
+
+ // argument counts
+ RequiresAtLeastArguments(usize, usize),
+ RequiresAtMostArguments(usize, usize),
+ IncorrectArgumentCount(usize, usize),
+ IfArmsMustMatch(TypeSignature, TypeSignature),
+ MatchArmsMustMatch(TypeSignature, TypeSignature),
+ DefaultTypesMustMatch(TypeSignature, TypeSignature),
+ TooManyExpressions,
+ IllegalOrUnknownFunctionApplication(String),
+ UnknownFunction(String),
+
+ // traits
+ NoSuchTrait(String, String),
+ TraitReferenceUnknown(String),
+ TraitMethodUnknown(String, String),
+ ExpectedTraitIdentifier,
+ ImportTraitBadSignature,
+ TraitReferenceNotAllowed,
+ BadTraitImplementation(String, String),
+ DefineTraitBadSignature,
+ DefineTraitDuplicateMethod(String),
+ UnexpectedTraitOrFieldReference,
+ TraitBasedContractCallInReadOnly,
+ ContractOfExpectsTrait,
+ IncompatibleTrait(TraitIdentifier, TraitIdentifier),
+
+ // strings
+ InvalidCharactersDetected,
+ InvalidUTF8Encoding,
+
+ // secp256k1 signature
+ InvalidSecp65k1Signature,
+
+ WriteAttemptedInReadOnly,
+ AtBlockClosureMustBeReadOnly,
+
+ // time checker errors
+ ExecutionTimeExpired,
+}
+
+#[derive(Debug, PartialEq)]
+pub struct CheckError {
+ pub err: CheckErrors,
+ pub expressions: Option>,
+ pub diagnostic: Diagnostic,
+}
+
+impl CheckErrors {
+ /// Does this check error indicate that the transaction should be
+ /// rejected?
+ pub fn rejectable(&self) -> bool {
+ matches!(
+ self,
+ CheckErrors::SupertypeTooLarge | CheckErrors::Expects(_)
+ )
+ }
+}
+
+impl CheckError {
+ pub fn new(err: CheckErrors) -> CheckError {
+ let diagnostic = Diagnostic::err(&err);
+ CheckError {
+ err,
+ expressions: None,
+ diagnostic,
+ }
+ }
+
+ pub fn has_expression(&self) -> bool {
+ self.expressions.is_some()
+ }
+
+ pub fn set_expression(&mut self, expr: &SymbolicExpression) {
+ self.diagnostic.spans = vec![expr.span().clone()];
+ self.expressions.replace(vec![expr.clone()]);
+ }
+
+ pub fn set_expressions(&mut self, exprs: &[SymbolicExpression]) {
+ self.diagnostic.spans = exprs.iter().map(|e| e.span().clone()).collect();
+ self.expressions.replace(exprs.to_vec());
+ }
+}
+
+impl fmt::Display for CheckErrors {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "{self:?}")
+ }
+}
+
+impl fmt::Display for CheckError {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "{}", self.err)?;
+
+ if let Some(ref e) = self.expressions {
+ write!(f, "\nNear:\n{e:?}")?;
+ }
+
+ Ok(())
+ }
+}
+
+impl From for CheckError {
+ fn from(err: CostErrors) -> Self {
+ CheckError::from(CheckErrors::from(err))
+ }
+}
+
+impl From for CheckErrors {
+ fn from(err: CostErrors) -> Self {
+ match err {
+ CostErrors::CostOverflow => CheckErrors::CostOverflow,
+ CostErrors::CostBalanceExceeded(a, b) => CheckErrors::CostBalanceExceeded(a, b),
+ CostErrors::MemoryBalanceExceeded(a, b) => CheckErrors::MemoryBalanceExceeded(a, b),
+ CostErrors::CostComputationFailed(s) => CheckErrors::CostComputationFailed(s),
+ CostErrors::CostContractLoadFailure => {
+ CheckErrors::CostComputationFailed("Failed to load cost contract".into())
+ }
+ CostErrors::InterpreterFailure => {
+ CheckErrors::Expects("Unexpected interpreter failure in cost computation".into())
+ }
+ CostErrors::Expect(s) => CheckErrors::Expects(s),
+ CostErrors::ExecutionTimeExpired => CheckErrors::ExecutionTimeExpired,
+ }
+ }
+}
+
+impl error::Error for CheckError {
+ fn source(&self) -> Option<&(dyn error::Error + 'static)> {
+ None
+ }
+}
+
+impl error::Error for CheckErrors {
+ fn source(&self) -> Option<&(dyn error::Error + 'static)> {
+ None
+ }
+}
+
+impl From for CheckError {
+ fn from(err: CheckErrors) -> Self {
+ CheckError::new(err)
+ }
+}
+
+#[cfg(any(test, feature = "testing"))]
+impl From for String {
+ fn from(o: CheckErrors) -> Self {
+ o.to_string()
+ }
+}
+
+#[allow(clippy::result_large_err)]
+pub fn check_argument_count(expected: usize, args: &[T]) -> Result<(), CheckErrors> {
+ if args.len() != expected {
+ Err(CheckErrors::IncorrectArgumentCount(expected, args.len()))
+ } else {
+ Ok(())
+ }
+}
+
+#[allow(clippy::result_large_err)]
+pub fn check_arguments_at_least(expected: usize, args: &[T]) -> Result<(), CheckErrors> {
+ if args.len() < expected {
+ Err(CheckErrors::RequiresAtLeastArguments(expected, args.len()))
+ } else {
+ Ok(())
+ }
+}
+
+#[allow(clippy::result_large_err)]
+pub fn check_arguments_at_most(expected: usize, args: &[T]) -> Result<(), CheckErrors> {
+ if args.len() > expected {
+ Err(CheckErrors::RequiresAtMostArguments(expected, args.len()))
+ } else {
+ Ok(())
+ }
+}
+
+fn formatted_expected_types(expected_types: &[TypeSignature]) -> String {
+ let mut expected_types_joined = format!("'{}'", expected_types[0]);
+
+ if expected_types.len() > 2 {
+ for expected_type in expected_types[1..expected_types.len() - 1].iter() {
+ expected_types_joined.push_str(&format!(", '{expected_type}'"));
+ }
+ }
+ expected_types_joined.push_str(&format!(
+ " or '{}'",
+ expected_types[expected_types.len() - 1]
+ ));
+ expected_types_joined
+}
+
+impl DiagnosableError for CheckErrors {
+ fn message(&self) -> String {
+ match &self {
+ CheckErrors::ExpectedLiteral => "expected a literal argument".into(),
+ CheckErrors::SupertypeTooLarge => "supertype of two types is too large".into(),
+ CheckErrors::Expects(s) => format!("unexpected interpreter behavior: {s}"),
+ CheckErrors::BadMatchOptionSyntax(source) =>
+ format!("match on a optional type uses the following syntax: (match input some-name if-some-expression if-none-expression). Caused by: {}",
+ source.message()),
+ CheckErrors::BadMatchResponseSyntax(source) =>
+ format!("match on a result type uses the following syntax: (match input ok-name if-ok-expression err-name if-err-expression). Caused by: {}",
+ source.message()),
+ CheckErrors::BadMatchInput(t) =>
+ format!("match requires an input of either a response or optional, found input: '{t}'"),
+ CheckErrors::TypeAnnotationExpectedFailure => "analysis expected type to already be annotated for expression".into(),
+ CheckErrors::CostOverflow => "contract execution cost overflowed cost counter".into(),
+ CheckErrors::CostBalanceExceeded(a, b) => format!("contract execution cost exceeded budget: {a:?} > {b:?}"),
+ CheckErrors::MemoryBalanceExceeded(a, b) => format!("contract execution cost exceeded memory budget: {a:?} > {b:?}"),
+ CheckErrors::InvalidTypeDescription => "supplied type description is invalid".into(),
+ CheckErrors::EmptyTuplesNotAllowed => "tuple types may not be empty".into(),
+ CheckErrors::BadSyntaxExpectedListOfPairs => "bad syntax: function expects a list of pairs to bind names, e.g., ((name-0 a) (name-1 b) ...)".into(),
+ CheckErrors::UnknownTypeName(name) => format!("failed to parse type: '{name}'"),
+ CheckErrors::ValueTooLarge => "created a type which was greater than maximum allowed value size".into(),
+ CheckErrors::ValueOutOfBounds => "created a type which value size was out of defined bounds".into(),
+ CheckErrors::TypeSignatureTooDeep => "created a type which was deeper than maximum allowed type depth".into(),
+ CheckErrors::ExpectedName => "expected a name argument to this function".into(),
+ CheckErrors::NoSuperType(a, b) => format!("unable to create a supertype for the two types: '{a}' and '{b}'"),
+ CheckErrors::UnknownListConstructionFailure => "invalid syntax for list definition".into(),
+ CheckErrors::ListTypesMustMatch => "expecting elements of same type in a list".into(),
+ CheckErrors::ConstructedListTooLarge => "reached limit of elements in a sequence".into(),
+ CheckErrors::TypeError(expected_type, found_type) => format!("expecting expression of type '{expected_type}', found '{found_type}'"),
+ CheckErrors::TypeLiteralError(expected_type, found_type) => format!("expecting a literal of type '{expected_type}', found '{found_type}'"),
+ CheckErrors::TypeValueError(expected_type, found_value) => format!("expecting expression of type '{expected_type}', found '{found_value}'"),
+ CheckErrors::UnionTypeError(expected_types, found_type) => format!("expecting expression of type {}, found '{}'", formatted_expected_types(expected_types), found_type),
+ CheckErrors::UnionTypeValueError(expected_types, found_type) => format!("expecting expression of type {}, found '{}'", formatted_expected_types(expected_types), found_type),
+ CheckErrors::ExpectedOptionalType(found_type) => format!("expecting expression of type 'optional', found '{found_type}'"),
+ CheckErrors::ExpectedOptionalOrResponseType(found_type) => format!("expecting expression of type 'optional' or 'response', found '{found_type}'"),
+ CheckErrors::ExpectedOptionalOrResponseValue(found_type) => format!("expecting expression of type 'optional' or 'response', found '{found_type}'"),
+ CheckErrors::ExpectedResponseType(found_type) => format!("expecting expression of type 'response', found '{found_type}'"),
+ CheckErrors::ExpectedOptionalValue(found_type) => format!("expecting expression of type 'optional', found '{found_type}'"),
+ CheckErrors::ExpectedResponseValue(found_type) => format!("expecting expression of type 'response', found '{found_type}'"),
+ CheckErrors::CouldNotDetermineResponseOkType => "attempted to obtain 'ok' value from response, but 'ok' type is indeterminate".into(),
+ CheckErrors::CouldNotDetermineResponseErrType => "attempted to obtain 'err' value from response, but 'err' type is indeterminate".into(),
+ CheckErrors::CouldNotDetermineMatchTypes => "attempted to match on an (optional) or (response) type where either the some, ok, or err type is indeterminate. you may wish to use unwrap-panic or unwrap-err-panic instead.".into(),
+ CheckErrors::CouldNotDetermineType => "type of expression cannot be determined".into(),
+ CheckErrors::BadTupleFieldName => "invalid tuple field name".into(),
+ CheckErrors::ExpectedTuple(type_signature) => format!("expecting tuple, found '{type_signature}'"),
+ CheckErrors::NoSuchTupleField(field_name, tuple_signature) => format!("cannot find field '{field_name}' in tuple '{tuple_signature}'"),
+ CheckErrors::BadTupleConstruction => "invalid tuple syntax, expecting list of pair".into(),
+ CheckErrors::TupleExpectsPairs => "invalid tuple syntax, expecting pair".into(),
+ CheckErrors::NoSuchDataVariable(var_name) => format!("use of unresolved persisted variable '{var_name}'"),
+ CheckErrors::BadTransferSTXArguments => "STX transfer expects an int amount, from principal, to principal".into(),
+ CheckErrors::BadTransferFTArguments => "transfer expects an int amount, from principal, to principal".into(),
+ CheckErrors::BadTransferNFTArguments => "transfer expects an asset, from principal, to principal".into(),
+ CheckErrors::BadMintFTArguments => "mint expects a uint amount and from principal".into(),
+ CheckErrors::BadBurnFTArguments => "burn expects a uint amount and from principal".into(),
+ CheckErrors::BadMapName => "invalid map name".into(),
+ CheckErrors::NoSuchMap(map_name) => format!("use of unresolved map '{map_name}'"),
+ CheckErrors::DefineFunctionBadSignature => "invalid function definition".into(),
+ CheckErrors::BadFunctionName => "invalid function name".into(),
+ CheckErrors::BadMapTypeDefinition => "invalid map definition".into(),
+ CheckErrors::PublicFunctionMustReturnResponse(found_type) => format!("public functions must return an expression of type 'response', found '{found_type}'"),
+ CheckErrors::DefineVariableBadSignature => "invalid variable definition".into(),
+ CheckErrors::ReturnTypesMustMatch(type_1, type_2) => format!("detected two execution paths, returning two different expression types (got '{type_1}' and '{type_2}')"),
+ CheckErrors::NoSuchContract(contract_identifier) => format!("use of unresolved contract '{contract_identifier}'"),
+ CheckErrors::NoSuchPublicFunction(contract_identifier, function_name) => format!("contract '{contract_identifier}' has no public function '{function_name}'"),
+ CheckErrors::PublicFunctionNotReadOnly(contract_identifier, function_name) => format!("function '{contract_identifier}' in '{function_name}' is not read-only"),
+ CheckErrors::ContractAlreadyExists(contract_identifier) => format!("contract name '{contract_identifier}' conflicts with existing contract"),
+ CheckErrors::ContractCallExpectName => "missing contract name for call".into(),
+ CheckErrors::ExpectedCallableType(found_type) => format!("expected a callable contract, found {found_type}"),
+ CheckErrors::NoSuchBlockInfoProperty(property_name) => format!("use of block unknown property '{property_name}'"),
+ CheckErrors::NoSuchBurnBlockInfoProperty(property_name) => format!("use of burn block unknown property '{property_name}'"),
+ CheckErrors::NoSuchStacksBlockInfoProperty(property_name) => format!("use of unknown stacks block property '{property_name}'"),
+ CheckErrors::NoSuchTenureInfoProperty(property_name) => format!("use of unknown tenure property '{property_name}'"),
+ CheckErrors::GetBlockInfoExpectPropertyName => "missing property name for block info introspection".into(),
+ CheckErrors::GetBurnBlockInfoExpectPropertyName => "missing property name for burn block info introspection".into(),
+ CheckErrors::GetStacksBlockInfoExpectPropertyName => "missing property name for stacks block info introspection".into(),
+ CheckErrors::GetTenureInfoExpectPropertyName => "missing property name for tenure info introspection".into(),
+ CheckErrors::NameAlreadyUsed(name) => format!("defining '{name}' conflicts with previous value"),
+ CheckErrors::ReservedWord(name) => format!("{name} is a reserved word"),
+ CheckErrors::NonFunctionApplication => "expecting expression of type function".into(),
+ CheckErrors::ExpectedListApplication => "expecting expression of type list".into(),
+ CheckErrors::ExpectedSequence(found_type) => format!("expecting expression of type 'list', 'buff', 'string-ascii' or 'string-utf8' - found '{found_type}'"),
+ CheckErrors::MaxLengthOverflow => format!("expecting a value <= {}", u32::MAX),
+ CheckErrors::BadLetSyntax => "invalid syntax of 'let'".into(),
+ CheckErrors::CircularReference(references) => format!("detected circular reference: ({})", references.join(", ")),
+ CheckErrors::BadSyntaxBinding => "invalid syntax binding".into(),
+ CheckErrors::MaxContextDepthReached => "reached depth limit".into(),
+ CheckErrors::UndefinedVariable(var_name) => format!("use of unresolved variable '{var_name}'"),
+ CheckErrors::UndefinedFunction(var_name) => format!("use of unresolved function '{var_name}'"),
+ CheckErrors::RequiresAtLeastArguments(expected, found) => format!("expecting >= {expected} arguments, got {found}"),
+ CheckErrors::RequiresAtMostArguments(expected, found) => format!("expecting < {expected} arguments, got {found}"),
+ CheckErrors::IncorrectArgumentCount(expected_count, found_count) => format!("expecting {expected_count} arguments, got {found_count}"),
+ CheckErrors::IfArmsMustMatch(type_1, type_2) => format!("expression types returned by the arms of 'if' must match (got '{type_1}' and '{type_2}')"),
+ CheckErrors::MatchArmsMustMatch(type_1, type_2) => format!("expression types returned by the arms of 'match' must match (got '{type_1}' and '{type_2}')"),
+ CheckErrors::DefaultTypesMustMatch(type_1, type_2) => format!("expression types passed in 'default-to' must match (got '{type_1}' and '{type_2}')"),
+ CheckErrors::TooManyExpressions => "reached limit of expressions".into(),
+ CheckErrors::IllegalOrUnknownFunctionApplication(function_name) => format!("use of illegal / unresolved function '{function_name}"),
+ CheckErrors::UnknownFunction(function_name) => format!("use of unresolved function '{function_name}'"),
+ CheckErrors::TraitBasedContractCallInReadOnly => "use of trait based contract calls are not allowed in read-only context".into(),
+ CheckErrors::WriteAttemptedInReadOnly => "expecting read-only statements, detected a writing operation".into(),
+ CheckErrors::AtBlockClosureMustBeReadOnly => "(at-block ...) closures expect read-only statements, but detected a writing operation".into(),
+ CheckErrors::BadTokenName => "expecting an token name as an argument".into(),
+ CheckErrors::DefineFTBadSignature => "(define-token ...) expects a token name as an argument".into(),
+ CheckErrors::DefineNFTBadSignature => "(define-asset ...) expects an asset name and an asset identifier type signature as arguments".into(),
+ CheckErrors::NoSuchNFT(asset_name) => format!("tried to use asset function with a undefined asset ('{asset_name}')"),
+ CheckErrors::NoSuchFT(asset_name) => format!("tried to use token function with a undefined token ('{asset_name}')"),
+ CheckErrors::NoSuchTrait(contract_name, trait_name) => format!("use of unresolved trait {contract_name}.{trait_name}"),
+ CheckErrors::TraitReferenceUnknown(trait_name) => format!("use of undeclared trait <{trait_name}>"),
+ CheckErrors::TraitMethodUnknown(trait_name, func_name) => format!("method '{func_name}' unspecified in trait <{trait_name}>"),
+ CheckErrors::ImportTraitBadSignature => "(use-trait ...) expects a trait name and a trait identifier".into(),
+ CheckErrors::BadTraitImplementation(trait_name, func_name) => format!("invalid signature for method '{func_name}' regarding trait's specification <{trait_name}>"),
+ CheckErrors::ExpectedTraitIdentifier => "expecting expression of type trait identifier".into(),
+ CheckErrors::UnexpectedTraitOrFieldReference => "unexpected use of trait reference or field".into(),
+ CheckErrors::DefineTraitBadSignature => "invalid trait definition".into(),
+ CheckErrors::DefineTraitDuplicateMethod(method_name) => format!("duplicate method name '{method_name}' in trait definition"),
+ CheckErrors::TraitReferenceNotAllowed => "trait references can not be stored".into(),
+ CheckErrors::ContractOfExpectsTrait => "trait reference expected".into(),
+ CheckErrors::IncompatibleTrait(expected_trait, actual_trait) => format!("trait '{actual_trait}' is not a compatible with expected trait, '{expected_trait}'"),
+ CheckErrors::InvalidCharactersDetected => "invalid characters detected".into(),
+ CheckErrors::InvalidUTF8Encoding => "invalid UTF8 encoding".into(),
+ CheckErrors::InvalidSecp65k1Signature => "invalid seckp256k1 signature".into(),
+ CheckErrors::TypeAlreadyAnnotatedFailure | CheckErrors::CheckerImplementationFailure => {
+ "internal error - please file an issue on https://github.com/stacks-network/stacks-blockchain".into()
+ },
+ CheckErrors::UncheckedIntermediaryResponses => "intermediary responses in consecutive statements must be checked".into(),
+ CheckErrors::CostComputationFailed(s) => format!("contract cost computation failed: {s}"),
+ CheckErrors::CouldNotDetermineSerializationType => "could not determine the input type for the serialization function".into(),
+ CheckErrors::ExecutionTimeExpired => "execution time expired".into(),
+ }
+ }
+
+ fn suggestion(&self) -> Option {
+ match &self {
+ CheckErrors::BadSyntaxBinding => {
+ Some("binding syntax example: ((supply int) (ttl int))".into())
+ }
+ CheckErrors::BadLetSyntax => Some(
+ "'let' syntax example: (let ((supply 1000) (ttl 60)) )".into(),
+ ),
+ CheckErrors::TraitReferenceUnknown(_) => Some(
+ "traits should be either defined, with define-trait, or imported, with use-trait."
+ .into(),
+ ),
+ CheckErrors::NoSuchBlockInfoProperty(_) => Some(
+ "properties available: time, header-hash, burnchain-header-hash, vrf-seed".into(),
+ ),
+ _ => None,
+ }
+ }
+}
diff --git a/clarity-serialization/src/errors/ast.rs b/clarity-serialization/src/errors/ast.rs
new file mode 100644
index 0000000000..35ee8fca2d
--- /dev/null
+++ b/clarity-serialization/src/errors/ast.rs
@@ -0,0 +1,319 @@
+// Copyright (C) 2025 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 std::{error, fmt};
+
+use crate::MAX_CALL_STACK_DEPTH;
+use crate::diagnostic::{DiagnosableError, Diagnostic, Level};
+use crate::errors::{CostErrors, LexerError};
+use crate::execution_cost::ExecutionCost;
+use crate::representations::{PreSymbolicExpression, Span};
+use crate::token::Token;
+
+pub type ParseResult = Result;
+
+#[derive(Debug, PartialEq)]
+pub enum ParseErrors {
+ CostOverflow,
+ CostBalanceExceeded(ExecutionCost, ExecutionCost),
+ MemoryBalanceExceeded(u64, u64),
+ TooManyExpressions,
+ ExpressionStackDepthTooDeep,
+ VaryExpressionStackDepthTooDeep,
+ FailedCapturingInput,
+ SeparatorExpected(String),
+ SeparatorExpectedAfterColon(String),
+ ProgramTooLarge,
+ IllegalVariableName(String),
+ IllegalContractName(String),
+ UnknownQuotedValue(String),
+ FailedParsingIntValue(String),
+ FailedParsingUIntValue(String),
+ FailedParsingBuffer(String),
+ FailedParsingHexValue(String, String),
+ FailedParsingPrincipal(String),
+ FailedParsingField(String),
+ FailedParsingRemainder(String),
+ ClosingParenthesisUnexpected,
+ ClosingParenthesisExpected,
+ ClosingTupleLiteralUnexpected,
+ ClosingTupleLiteralExpected,
+ CircularReference(Vec),
+ TupleColonExpected(usize),
+ TupleCommaExpected(usize),
+ TupleItemExpected(usize),
+ NameAlreadyUsed(String),
+ TraitReferenceNotAllowed,
+ ImportTraitBadSignature,
+ DefineTraitBadSignature,
+ ImplTraitBadSignature,
+ TraitReferenceUnknown(String),
+ CommaSeparatorUnexpected,
+ ColonSeparatorUnexpected,
+ InvalidCharactersDetected,
+ InvalidEscaping,
+ CostComputationFailed(String),
+
+ // V2 Errors
+ Lexer(LexerError),
+ ContractNameTooLong(String),
+ ExpectedContractIdentifier,
+ ExpectedTraitIdentifier,
+ IllegalTraitName(String),
+ InvalidPrincipalLiteral,
+ InvalidBuffer,
+ NameTooLong(String),
+ UnexpectedToken(Token),
+ ExpectedClosing(Token),
+ TupleColonExpectedv2,
+ TupleCommaExpectedv2,
+ TupleValueExpected,
+ IllegalClarityName(String),
+ IllegalASCIIString(String),
+ IllegalUtf8String(String),
+ ExpectedWhitespace,
+ // Notes
+ NoteToMatchThis(Token),
+
+ /// Should be an unreachable error
+ UnexpectedParserFailure,
+ /// Should be an unreachable failure which invalidates the transaction
+ InterpreterFailure,
+
+ ExecutionTimeExpired,
+}
+
+#[derive(Debug, PartialEq)]
+pub struct ParseError {
+ pub err: ParseErrors,
+ pub pre_expressions: Option>,
+ pub diagnostic: Diagnostic,
+}
+
+impl ParseError {
+ pub fn new(err: ParseErrors) -> ParseError {
+ let diagnostic = Diagnostic::err(&err);
+ ParseError {
+ err,
+ pre_expressions: None,
+ diagnostic,
+ }
+ }
+
+ pub fn rejectable(&self) -> bool {
+ matches!(self.err, ParseErrors::InterpreterFailure)
+ }
+
+ pub fn has_pre_expression(&self) -> bool {
+ self.pre_expressions.is_some()
+ }
+
+ pub fn set_pre_expression(&mut self, expr: &PreSymbolicExpression) {
+ self.diagnostic.spans = vec![expr.span().clone()];
+ self.pre_expressions.replace(vec![expr.clone()]);
+ }
+
+ pub fn set_pre_expressions(&mut self, exprs: Vec) {
+ self.diagnostic.spans = exprs.iter().map(|e| e.span().clone()).collect();
+ self.pre_expressions.replace(exprs.to_vec());
+ }
+}
+
+impl fmt::Display for ParseError {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "{:?}", self.err)?;
+
+ if let Some(ref e) = self.pre_expressions {
+ write!(f, "\nNear:\n{e:?}")?;
+ }
+
+ Ok(())
+ }
+}
+
+impl error::Error for ParseError {
+ fn source(&self) -> Option<&(dyn error::Error + 'static)> {
+ None
+ }
+}
+
+impl From for ParseError {
+ fn from(err: ParseErrors) -> Self {
+ ParseError::new(err)
+ }
+}
+
+impl From for ParseError {
+ fn from(err: CostErrors) -> Self {
+ match err {
+ CostErrors::CostOverflow => ParseError::new(ParseErrors::CostOverflow),
+ CostErrors::CostBalanceExceeded(a, b) => {
+ ParseError::new(ParseErrors::CostBalanceExceeded(a, b))
+ }
+ CostErrors::MemoryBalanceExceeded(a, b) => {
+ ParseError::new(ParseErrors::MemoryBalanceExceeded(a, b))
+ }
+ CostErrors::CostComputationFailed(s) => {
+ ParseError::new(ParseErrors::CostComputationFailed(s))
+ }
+ CostErrors::CostContractLoadFailure => ParseError::new(
+ ParseErrors::CostComputationFailed("Failed to load cost contract".into()),
+ ),
+ CostErrors::InterpreterFailure | CostErrors::Expect(_) => {
+ ParseError::new(ParseErrors::InterpreterFailure)
+ }
+ CostErrors::ExecutionTimeExpired => ParseError::new(ParseErrors::ExecutionTimeExpired),
+ }
+ }
+}
+
+impl DiagnosableError for ParseErrors {
+ fn message(&self) -> String {
+ match &self {
+ ParseErrors::CostOverflow => "Used up cost budget during the parse".into(),
+ ParseErrors::CostBalanceExceeded(bal, used) => {
+ format!("Used up cost budget during the parse: {bal} balance, {used} used")
+ }
+ ParseErrors::MemoryBalanceExceeded(bal, used) => {
+ format!("Used up memory budget during the parse: {bal} balance, {used} used")
+ }
+ ParseErrors::TooManyExpressions => "Too many expressions".into(),
+ ParseErrors::FailedCapturingInput => "Failed to capture value from input".into(),
+ ParseErrors::SeparatorExpected(found) => {
+ format!("Expected whitespace or a close parens. Found: '{found}'")
+ }
+ ParseErrors::SeparatorExpectedAfterColon(found) => {
+ format!("Whitespace expected after colon (:), Found: '{found}'")
+ }
+ ParseErrors::ProgramTooLarge => "Program too large to parse".into(),
+ ParseErrors::IllegalContractName(contract_name) => {
+ format!("Illegal contract name: '{contract_name}'")
+ }
+ ParseErrors::IllegalVariableName(var_name) => {
+ format!("Illegal variable name: '{var_name}'")
+ }
+ ParseErrors::UnknownQuotedValue(value) => format!("Unknown 'quoted value '{value}'"),
+ ParseErrors::FailedParsingIntValue(value) => {
+ format!("Failed to parse int literal '{value}'")
+ }
+ ParseErrors::FailedParsingUIntValue(value) => {
+ format!("Failed to parse uint literal 'u{value}'")
+ }
+ ParseErrors::FailedParsingHexValue(value, x) => {
+ format!("Invalid hex-string literal {value}: {x}")
+ }
+ ParseErrors::FailedParsingPrincipal(value) => {
+ format!("Invalid principal literal: {value}")
+ }
+ ParseErrors::FailedParsingBuffer(value) => format!("Invalid buffer literal: {value}"),
+ ParseErrors::FailedParsingField(value) => format!("Invalid field literal: {value}"),
+ ParseErrors::FailedParsingRemainder(remainder) => {
+ format!("Failed to lex input remainder: '{remainder}'")
+ }
+ ParseErrors::ClosingParenthesisUnexpected => {
+ "Tried to close list which isn't open.".into()
+ }
+ ParseErrors::ClosingParenthesisExpected => "List expressions (..) left opened.".into(),
+ ParseErrors::ClosingTupleLiteralUnexpected => {
+ "Tried to close tuple literal which isn't open.".into()
+ }
+ ParseErrors::ClosingTupleLiteralExpected => "Tuple literal {{..}} left opened.".into(),
+ ParseErrors::ColonSeparatorUnexpected => "Misplaced colon.".into(),
+ ParseErrors::CommaSeparatorUnexpected => "Misplaced comma.".into(),
+ ParseErrors::TupleColonExpected(i) => {
+ format!("Tuple literal construction expects a colon at index {i}")
+ }
+ ParseErrors::TupleCommaExpected(i) => {
+ format!("Tuple literal construction expects a comma at index {i}")
+ }
+ ParseErrors::TupleItemExpected(i) => {
+ format!("Tuple literal construction expects a key or value at index {i}")
+ }
+ ParseErrors::CircularReference(function_names) => format!(
+ "detected interdependent functions ({})",
+ function_names.join(", ")
+ ),
+ ParseErrors::NameAlreadyUsed(name) => {
+ format!("defining '{name}' conflicts with previous value")
+ }
+ ParseErrors::ImportTraitBadSignature => {
+ "(use-trait ...) expects a trait name and a trait identifier".into()
+ }
+ ParseErrors::DefineTraitBadSignature => {
+ "(define-trait ...) expects a trait name and a trait definition".into()
+ }
+ ParseErrors::ImplTraitBadSignature => {
+ "(impl-trait ...) expects a trait identifier".into()
+ }
+ ParseErrors::TraitReferenceNotAllowed => "trait references can not be stored".into(),
+ ParseErrors::TraitReferenceUnknown(trait_name) => {
+ format!("use of undeclared trait <{trait_name}>")
+ }
+ ParseErrors::ExpressionStackDepthTooDeep => format!(
+ "AST has too deep of an expression nesting. The maximum stack depth is {MAX_CALL_STACK_DEPTH}"
+ ),
+ ParseErrors::VaryExpressionStackDepthTooDeep => format!(
+ "AST has too deep of an expression nesting. The maximum stack depth is {MAX_CALL_STACK_DEPTH}"
+ ),
+ ParseErrors::InvalidCharactersDetected => "invalid characters detected".into(),
+ ParseErrors::InvalidEscaping => "invalid escaping detected in string".into(),
+ ParseErrors::CostComputationFailed(s) => format!("Cost computation failed: {s}"),
+
+ // Parser v2 errors
+ ParseErrors::Lexer(le) => le.message(),
+ ParseErrors::ContractNameTooLong(name) => {
+ format!("contract name '{name}' is too long")
+ }
+ ParseErrors::ExpectedContractIdentifier => "expected contract identifier".into(),
+ ParseErrors::ExpectedTraitIdentifier => "expected trait identifier".into(),
+ ParseErrors::IllegalTraitName(name) => format!("illegal trait name, '{name}'"),
+ ParseErrors::InvalidPrincipalLiteral => "invalid principal literal".into(),
+ ParseErrors::InvalidBuffer => "invalid hex-string literal".into(),
+ ParseErrors::NameTooLong(name) => format!("illegal name (too long), '{name}'"),
+ ParseErrors::UnexpectedToken(token) => format!("unexpected '{token}'"),
+ ParseErrors::ExpectedClosing(token) => format!("expected closing '{token}'"),
+ ParseErrors::TupleColonExpectedv2 => "expected ':' after key in tuple".into(),
+ ParseErrors::TupleCommaExpectedv2 => {
+ "expected ',' separating key-value pairs in tuple".into()
+ }
+ ParseErrors::TupleValueExpected => "expected value expression for tuple".into(),
+ ParseErrors::IllegalClarityName(name) => format!("illegal clarity name, '{name}'"),
+ ParseErrors::IllegalASCIIString(s) => format!("illegal ascii string \"{s}\""),
+ ParseErrors::IllegalUtf8String(s) => format!("illegal UTF8 string \"{s}\""),
+ ParseErrors::ExpectedWhitespace => "expected whitespace before expression".into(),
+ ParseErrors::NoteToMatchThis(token) => format!("to match this '{token}'"),
+ ParseErrors::UnexpectedParserFailure => "unexpected failure while parsing".to_string(),
+ ParseErrors::InterpreterFailure => "unexpected failure while parsing".to_string(),
+ ParseErrors::ExecutionTimeExpired => "max execution time expired".to_string(),
+ }
+ }
+
+ fn suggestion(&self) -> Option {
+ None
+ }
+
+ fn level(&self) -> Level {
+ match self {
+ ParseErrors::NoteToMatchThis(_) => Level::Note,
+ ParseErrors::Lexer(lexer_error) => lexer_error.level(),
+ _ => Level::Error,
+ }
+ }
+}
+
+pub struct PlacedError {
+ pub e: ParseErrors,
+ pub span: Span,
+}
diff --git a/clarity-serialization/src/errors/cost.rs b/clarity-serialization/src/errors/cost.rs
new file mode 100644
index 0000000000..2678d8ade4
--- /dev/null
+++ b/clarity-serialization/src/errors/cost.rs
@@ -0,0 +1,54 @@
+// Copyright (C) 2025 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 std::fmt;
+
+use crate::execution_cost::ExecutionCost;
+
+#[derive(Debug, PartialEq, Eq)]
+pub enum CostErrors {
+ CostComputationFailed(String),
+ CostOverflow,
+ CostBalanceExceeded(ExecutionCost, ExecutionCost),
+ MemoryBalanceExceeded(u64, u64),
+ CostContractLoadFailure,
+ InterpreterFailure,
+ Expect(String),
+ ExecutionTimeExpired,
+}
+
+impl fmt::Display for CostErrors {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match self {
+ CostErrors::CostComputationFailed(s) => write!(f, "Cost computation failed: {s}"),
+ CostErrors::CostOverflow => write!(f, "Cost overflow"),
+ CostErrors::CostBalanceExceeded(total, limit) => {
+ write!(f, "Cost balance exceeded: total {total}, limit {limit}")
+ }
+ CostErrors::MemoryBalanceExceeded(used, limit) => {
+ write!(f, "Memory balance exceeded: used {used}, limit {limit}")
+ }
+ CostErrors::CostContractLoadFailure => write!(f, "Failed to load cost contract"),
+ CostErrors::InterpreterFailure => write!(f, "Interpreter failure"),
+ CostErrors::Expect(s) => write!(f, "Expectation failed: {s}"),
+ CostErrors::ExecutionTimeExpired => write!(f, "Execution time expired"),
+ }
+ }
+}
+
+impl CostErrors {
+ pub fn rejectable(&self) -> bool {
+ matches!(self, CostErrors::InterpreterFailure | CostErrors::Expect(_))
+ }
+}
diff --git a/clarity-serialization/src/errors/lexer.rs b/clarity-serialization/src/errors/lexer.rs
new file mode 100644
index 0000000000..27d6a2c3a2
--- /dev/null
+++ b/clarity-serialization/src/errors/lexer.rs
@@ -0,0 +1,94 @@
+// Copyright (C) 2025 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::diagnostic::{DiagnosableError, Level};
+use crate::representations::Span;
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum LexerError {
+ InvalidCharInt(char),
+ InvalidCharUint(char),
+ InvalidCharBuffer(char),
+ InvalidCharIdent(char),
+ InvalidCharTraitIdent(char),
+ InvalidCharPrincipal(char),
+ InvalidBufferLength(usize),
+ UnknownEscapeChar(char),
+ IllegalCharString(char),
+ IllegalCharUTF8Encoding(char),
+ UnterminatedUTF8Encoding,
+ ExpectedClosing(char),
+ ExpectedSeparator,
+ EmptyUTF8Encoding,
+ InvalidUTF8Encoding,
+ SingleSemiColon,
+ UnknownSymbol(char),
+ NonASCIIChar(char),
+ NoteToMatchThis(char),
+ UnsupportedLineEnding,
+ EditorCRLFMode,
+}
+
+#[derive(Debug)]
+pub struct PlacedError {
+ pub e: LexerError,
+ pub span: Span,
+}
+
+impl DiagnosableError for LexerError {
+ fn message(&self) -> String {
+ use self::LexerError::*;
+ match self {
+ InvalidCharInt(c) => format!("invalid character, '{c}', in int literal"),
+ InvalidCharUint(c) => format!("invalid character, '{c}', in uint literal"),
+ InvalidCharBuffer(c) => format!("invalid character, '{c}', in buffer"),
+ InvalidCharIdent(c) => format!("invalid character, '{c}', in identifier"),
+ InvalidCharTraitIdent(c) => format!("invalid character, '{c}', in trait identifier"),
+ InvalidCharPrincipal(c) => format!("invalid character, '{c}', in principal literal"),
+ IllegalCharString(c) => format!("invalid character, '{c}', in string literal"),
+ IllegalCharUTF8Encoding(c) => format!("invalid character, '{c}', in UTF8 encoding"),
+ InvalidUTF8Encoding => "invalid UTF8 encoding".to_string(),
+ EmptyUTF8Encoding => "empty UTF8 encoding".to_string(),
+ UnterminatedUTF8Encoding => "unterminated UTF8 encoding, missing '}'".to_string(),
+ InvalidBufferLength(size) => format!("invalid buffer length, {size}"),
+ UnknownEscapeChar(c) => format!("unknown escape character, '{c}'"),
+ ExpectedClosing(c) => format!("expected closing '{c}'"),
+ ExpectedSeparator => "expected separator".to_string(),
+ SingleSemiColon => "unexpected single ';' (comments begin with \";;\"".to_string(),
+ UnknownSymbol(c) => format!("unknown symbol, '{c}'"),
+ NonASCIIChar(c) => format!("illegal non-ASCII character, '{c}'"),
+ NoteToMatchThis(c) => format!("to match this '{c}'"),
+ UnsupportedLineEnding => {
+ "unsupported line-ending '\\r', only '\\n' is supported".to_string()
+ }
+ EditorCRLFMode => {
+ "you may need to change your editor from CRLF mode to LF mode".to_string()
+ }
+ }
+ }
+
+ fn suggestion(&self) -> Option {
+ None
+ }
+
+ fn level(&self) -> Level {
+ use self::LexerError::*;
+ match self {
+ NoteToMatchThis(_) => Level::Note,
+ EditorCRLFMode => Level::Note,
+ _ => Level::Error,
+ }
+ }
+}
diff --git a/clarity-serialization/src/errors/mod.rs b/clarity-serialization/src/errors/mod.rs
new file mode 100644
index 0000000000..e65fe47e62
--- /dev/null
+++ b/clarity-serialization/src/errors/mod.rs
@@ -0,0 +1,256 @@
+// Copyright (C) 2025 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 .
+
+pub mod analysis;
+pub mod ast;
+pub mod cost;
+pub mod lexer;
+
+use std::{error, fmt};
+
+pub use analysis::{CheckError, CheckErrors, CheckResult};
+pub use ast::{ParseError, ParseErrors, ParseResult};
+pub use cost::CostErrors;
+pub use lexer::LexerError;
+#[cfg(feature = "rusqlite")]
+use rusqlite::Error as SqliteError;
+use serde_json::Error as SerdeJSONErr;
+use stacks_common::types::chainstate::BlockHeaderHash;
+
+use crate::types::{FunctionIdentifier, Value};
+
+pub type StackTrace = Vec;
+
+#[derive(Debug)]
+pub struct IncomparableError {
+ pub err: T,
+}
+
+#[derive(Debug)]
+pub enum Error {
+ /// UncheckedErrors are errors that *should* be caught by the
+ /// TypeChecker and other check passes. Test executions may
+ /// trigger these errors.
+ Unchecked(CheckErrors),
+ Interpreter(InterpreterError),
+ Runtime(RuntimeErrorType, Option),
+ ShortReturn(ShortReturnType),
+}
+
+/// InterpreterErrors are errors that *should never* occur.
+/// Test executions may trigger these errors.
+#[derive(Debug, PartialEq)]
+pub enum InterpreterError {
+ BadSender(Value),
+ BadSymbolicRepresentation(String),
+ InterpreterError(String),
+ UninitializedPersistedVariable,
+ FailedToConstructAssetTable,
+ FailedToConstructEventBatch,
+ #[cfg(feature = "rusqlite")]
+ SqliteError(IncomparableError),
+ BadFileName,
+ FailedToCreateDataDirectory,
+ MarfFailure(String),
+ FailureConstructingTupleWithType,
+ FailureConstructingListWithType,
+ InsufficientBalance,
+ CostContractLoadFailure,
+ DBError(String),
+ Expect(String),
+}
+
+/// RuntimeErrors are errors that smart contracts are expected
+/// to be able to trigger during execution (e.g., arithmetic errors)
+#[derive(Debug, PartialEq)]
+pub enum RuntimeErrorType {
+ Arithmetic(String),
+ ArithmeticOverflow,
+ ArithmeticUnderflow,
+ SupplyOverflow(u128, u128),
+ SupplyUnderflow(u128, u128),
+ DivisionByZero,
+ // error in parsing types
+ ParseError(String),
+ // error in parsing the AST
+ ASTError(ParseError),
+ MaxStackDepthReached,
+ MaxContextDepthReached,
+ ListDimensionTooHigh,
+ BadTypeConstruction,
+ ValueTooLarge,
+ BadBlockHeight(String),
+ TransferNonPositiveAmount,
+ NoSuchToken,
+ NotImplemented,
+ NoCallerInContext,
+ NoSenderInContext,
+ NonPositiveTokenSupply,
+ JSONParseError(IncomparableError),
+ AttemptToFetchInTransientContext,
+ BadNameValue(&'static str, String),
+ UnknownBlockHeaderHash(BlockHeaderHash),
+ BadBlockHash(Vec),
+ UnwrapFailure,
+ DefunctPoxContract,
+ PoxAlreadyLocked,
+ MetadataAlreadySet,
+}
+
+#[derive(Debug, PartialEq)]
+pub enum ShortReturnType {
+ ExpectedValue(Value),
+ AssertionFailed(Value),
+}
+
+pub type InterpreterResult = Result;
+
+impl PartialEq> for IncomparableError {
+ fn eq(&self, _other: &IncomparableError) -> bool {
+ false
+ }
+}
+
+impl PartialEq for Error {
+ fn eq(&self, other: &Error) -> bool {
+ match (self, other) {
+ (Error::Runtime(x, _), Error::Runtime(y, _)) => x == y,
+ (Error::Unchecked(x), Error::Unchecked(y)) => x == y,
+ (Error::ShortReturn(x), Error::ShortReturn(y)) => x == y,
+ (Error::Interpreter(x), Error::Interpreter(y)) => x == y,
+ _ => false,
+ }
+ }
+}
+
+impl fmt::Display for Error {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match self {
+ Error::Runtime(err, stack) => {
+ write!(f, "{err}")?;
+ if let Some(stack_trace) = stack {
+ writeln!(f, "\n Stack Trace: ")?;
+ for item in stack_trace.iter() {
+ writeln!(f, "{item}")?;
+ }
+ }
+ Ok(())
+ }
+ _ => write!(f, "{self:?}"),
+ }
+ }
+}
+
+impl fmt::Display for RuntimeErrorType {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{self:?}")
+ }
+}
+
+impl error::Error for Error {
+ fn source(&self) -> Option<&(dyn error::Error + 'static)> {
+ None
+ }
+}
+
+impl error::Error for RuntimeErrorType {
+ fn source(&self) -> Option<&(dyn error::Error + 'static)> {
+ None
+ }
+}
+
+impl From for Error {
+ fn from(err: ParseError) -> Self {
+ match &err.err {
+ ParseErrors::InterpreterFailure => Error::from(InterpreterError::Expect(
+ "Unexpected interpreter failure during parsing".into(),
+ )),
+ _ => Error::from(RuntimeErrorType::ASTError(err)),
+ }
+ }
+}
+
+impl From for Error {
+ fn from(err: CostErrors) -> Self {
+ match err {
+ CostErrors::InterpreterFailure => Error::from(InterpreterError::Expect(
+ "Interpreter failure during cost calculation".into(),
+ )),
+ CostErrors::Expect(s) => Error::from(InterpreterError::Expect(format!(
+ "Interpreter failure during cost calculation: {s}"
+ ))),
+ other_err => Error::from(CheckErrors::from(other_err)),
+ }
+ }
+}
+
+impl From for Error {
+ fn from(err: RuntimeErrorType) -> Self {
+ Error::Runtime(err, None)
+ }
+}
+
+impl From for Error {
+ fn from(err: CheckErrors) -> Self {
+ Error::Unchecked(err)
+ }
+}
+
+impl From for Error {
+ fn from(err: ShortReturnType) -> Self {
+ Error::ShortReturn(err)
+ }
+}
+
+impl From for Error {
+ fn from(err: InterpreterError) -> Self {
+ Error::Interpreter(err)
+ }
+}
+
+#[cfg(any(test, feature = "testing"))]
+impl From for () {
+ fn from(_err: Error) -> Self {}
+}
+
+impl From for Value {
+ fn from(val: ShortReturnType) -> Self {
+ match val {
+ ShortReturnType::ExpectedValue(v) => v,
+ ShortReturnType::AssertionFailed(v) => v,
+ }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ #[test]
+ fn equality() {
+ assert_eq!(
+ Error::ShortReturn(ShortReturnType::ExpectedValue(Value::Bool(true))),
+ Error::ShortReturn(ShortReturnType::ExpectedValue(Value::Bool(true)))
+ );
+ assert_eq!(
+ Error::Interpreter(InterpreterError::InterpreterError("".to_string())),
+ Error::Interpreter(InterpreterError::InterpreterError("".to_string()))
+ );
+ assert!(
+ Error::ShortReturn(ShortReturnType::ExpectedValue(Value::Bool(true)))
+ != Error::Interpreter(InterpreterError::InterpreterError("".to_string()))
+ );
+ }
+}
diff --git a/clarity-serialization/src/execution_cost.rs b/clarity-serialization/src/execution_cost.rs
new file mode 100644
index 0000000000..d261eb00c1
--- /dev/null
+++ b/clarity-serialization/src/execution_cost.rs
@@ -0,0 +1,217 @@
+// Copyright (C) 2025 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 std::{cmp, fmt};
+
+#[cfg(feature = "rusqlite")]
+use rusqlite::{
+ ToSql,
+ types::{FromSql, FromSqlResult, ToSqlOutput, ValueRef},
+};
+
+use crate::errors::CostErrors;
+
+#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Hash)]
+pub struct ExecutionCost {
+ pub write_length: u64,
+ pub write_count: u64,
+ pub read_length: u64,
+ pub read_count: u64,
+ pub runtime: u64,
+}
+
+impl fmt::Display for ExecutionCost {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(
+ f,
+ "{{\"runtime\": {}, \"write_len\": {}, \"write_cnt\": {}, \"read_len\": {}, \"read_cnt\": {}}}",
+ self.runtime, self.write_length, self.write_count, self.read_length, self.read_count
+ )
+ }
+}
+
+impl ExecutionCost {
+ pub const ZERO: Self = Self {
+ runtime: 0,
+ write_length: 0,
+ read_count: 0,
+ write_count: 0,
+ read_length: 0,
+ };
+
+ /// Returns the percentage of self consumed in `numerator`'s largest proportion dimension.
+ pub fn proportion_largest_dimension(&self, numerator: &ExecutionCost) -> u64 {
+ // max() should always return because there are > 0 elements
+ #[allow(clippy::expect_used)]
+ *[
+ numerator.runtime / cmp::max(1, self.runtime / 100),
+ numerator.write_length / cmp::max(1, self.write_length / 100),
+ numerator.write_count / cmp::max(1, self.write_count / 100),
+ numerator.read_length / cmp::max(1, self.read_length / 100),
+ numerator.read_count / cmp::max(1, self.read_count / 100),
+ ]
+ .iter()
+ .max()
+ .expect("BUG: should find maximum")
+ }
+
+ /// Returns the dot product of this execution cost with `resolution`/block_limit
+ /// This provides a scalar value representing the cumulative consumption
+ /// of `self` in the provided block_limit.
+ pub fn proportion_dot_product(&self, block_limit: &ExecutionCost, resolution: u64) -> u64 {
+ [
+ // each field here is calculating `r * self / limit`, using f64
+ // use MAX(1, block_limit) to guard against divide by zero
+ // use MIN(1, self/block_limit) to guard against self > block_limit
+ resolution as f64
+ * 1_f64.min(self.runtime as f64 / 1_f64.max(block_limit.runtime as f64)),
+ resolution as f64
+ * 1_f64.min(self.read_count as f64 / 1_f64.max(block_limit.read_count as f64)),
+ resolution as f64
+ * 1_f64.min(self.write_count as f64 / 1_f64.max(block_limit.write_count as f64)),
+ resolution as f64
+ * 1_f64.min(self.read_length as f64 / 1_f64.max(block_limit.read_length as f64)),
+ resolution as f64
+ * 1_f64.min(self.write_length as f64 / 1_f64.max(block_limit.write_length as f64)),
+ ]
+ .iter()
+ .fold(0, |acc, dim| acc.saturating_add(cmp::max(*dim as u64, 1)))
+ }
+
+ pub fn max_value() -> ExecutionCost {
+ Self {
+ runtime: u64::MAX,
+ write_length: u64::MAX,
+ read_count: u64::MAX,
+ write_count: u64::MAX,
+ read_length: u64::MAX,
+ }
+ }
+
+ pub fn runtime(runtime: u64) -> ExecutionCost {
+ Self {
+ runtime,
+ write_length: 0,
+ read_count: 0,
+ write_count: 0,
+ read_length: 0,
+ }
+ }
+
+ pub fn add_runtime(&mut self, runtime: u64) -> Result<(), CostErrors> {
+ self.runtime = self.runtime.cost_overflow_add(runtime)?;
+ Ok(())
+ }
+
+ pub fn add(&mut self, other: &ExecutionCost) -> Result<(), CostErrors> {
+ self.runtime = self.runtime.cost_overflow_add(other.runtime)?;
+ self.read_count = self.read_count.cost_overflow_add(other.read_count)?;
+ self.read_length = self.read_length.cost_overflow_add(other.read_length)?;
+ self.write_length = self.write_length.cost_overflow_add(other.write_length)?;
+ self.write_count = self.write_count.cost_overflow_add(other.write_count)?;
+ Ok(())
+ }
+
+ pub fn sub(&mut self, other: &ExecutionCost) -> Result<(), CostErrors> {
+ self.runtime = self.runtime.cost_overflow_sub(other.runtime)?;
+ self.read_count = self.read_count.cost_overflow_sub(other.read_count)?;
+ self.read_length = self.read_length.cost_overflow_sub(other.read_length)?;
+ self.write_length = self.write_length.cost_overflow_sub(other.write_length)?;
+ self.write_count = self.write_count.cost_overflow_sub(other.write_count)?;
+ Ok(())
+ }
+
+ pub fn multiply(&mut self, times: u64) -> Result<(), CostErrors> {
+ self.runtime = self.runtime.cost_overflow_mul(times)?;
+ self.read_count = self.read_count.cost_overflow_mul(times)?;
+ self.read_length = self.read_length.cost_overflow_mul(times)?;
+ self.write_length = self.write_length.cost_overflow_mul(times)?;
+ self.write_count = self.write_count.cost_overflow_mul(times)?;
+ Ok(())
+ }
+
+ pub fn divide(&mut self, divisor: u64) -> Result<(), CostErrors> {
+ self.runtime = self.runtime.cost_overflow_div(divisor)?;
+ self.read_count = self.read_count.cost_overflow_div(divisor)?;
+ self.read_length = self.read_length.cost_overflow_div(divisor)?;
+ self.write_length = self.write_length.cost_overflow_div(divisor)?;
+ self.write_count = self.write_count.cost_overflow_div(divisor)?;
+ Ok(())
+ }
+
+ /// Returns whether or not this cost exceeds any dimension of the
+ /// other cost.
+ pub fn exceeds(&self, other: &ExecutionCost) -> bool {
+ self.runtime > other.runtime
+ || self.write_length > other.write_length
+ || self.write_count > other.write_count
+ || self.read_count > other.read_count
+ || self.read_length > other.read_length
+ }
+
+ pub fn max_cost(first: ExecutionCost, second: ExecutionCost) -> ExecutionCost {
+ Self {
+ runtime: first.runtime.max(second.runtime),
+ write_length: first.write_length.max(second.write_length),
+ write_count: first.write_count.max(second.write_count),
+ read_count: first.read_count.max(second.read_count),
+ read_length: first.read_length.max(second.read_length),
+ }
+ }
+
+ pub fn is_zero(&self) -> bool {
+ *self == Self::ZERO
+ }
+}
+
+pub trait CostOverflowingMath {
+ fn cost_overflow_mul(self, other: T) -> Result;
+ fn cost_overflow_add(self, other: T) -> Result;
+ fn cost_overflow_sub(self, other: T) -> Result;
+ fn cost_overflow_div(self, other: T) -> Result;
+}
+
+impl CostOverflowingMath for u64 {
+ fn cost_overflow_mul(self, other: u64) -> Result {
+ self.checked_mul(other).ok_or(CostErrors::CostOverflow)
+ }
+ fn cost_overflow_add(self, other: u64) -> Result {
+ self.checked_add(other).ok_or(CostErrors::CostOverflow)
+ }
+ fn cost_overflow_sub(self, other: u64) -> Result {
+ self.checked_sub(other).ok_or(CostErrors::CostOverflow)
+ }
+ fn cost_overflow_div(self, other: u64) -> Result {
+ self.checked_div(other).ok_or(CostErrors::CostOverflow)
+ }
+}
+
+#[cfg(feature = "rusqlite")]
+impl ToSql for ExecutionCost {
+ fn to_sql(&self) -> rusqlite::Result> {
+ let val = serde_json::to_string(self)
+ .map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?;
+ Ok(ToSqlOutput::from(val))
+ }
+}
+
+#[cfg(feature = "rusqlite")]
+impl FromSql for ExecutionCost {
+ fn column_result(value: ValueRef) -> FromSqlResult {
+ let str_val = String::column_result(value)?;
+ let parsed = serde_json::from_str(&str_val)
+ .map_err(|e| rusqlite::types::FromSqlError::Other(Box::new(e)))?;
+ Ok(parsed)
+ }
+}
diff --git a/clarity-serialization/src/lib.rs b/clarity-serialization/src/lib.rs
index 054d8d0a1b..de51f53915 100644
--- a/clarity-serialization/src/lib.rs
+++ b/clarity-serialization/src/lib.rs
@@ -24,14 +24,19 @@ pub use stacks_common::{
impl_byte_array_serde, types as stacks_types, util,
};
+pub mod diagnostic;
pub mod errors;
+pub mod execution_cost;
pub mod representations;
+pub mod token;
pub mod types;
-pub use errors::CodecError;
+pub use errors::Error;
pub use representations::{ClarityName, ContractName};
pub use types::Value;
+pub const MAX_CALL_STACK_DEPTH: usize = 64;
+
#[cfg(test)]
pub mod tests;
diff --git a/clarity-serialization/src/representations.rs b/clarity-serialization/src/representations.rs
index 74bffc2a53..f68d35ee2e 100644
--- a/clarity-serialization/src/representations.rs
+++ b/clarity-serialization/src/representations.rs
@@ -22,7 +22,9 @@ use lazy_static::lazy_static;
use regex::Regex;
use stacks_common::codec::{Error as codec_error, StacksMessageCodec, read_next, write_next};
-use crate::errors::CodecError;
+use crate::Value;
+use crate::errors::RuntimeErrorType;
+use crate::types::TraitIdentifier;
pub const CONTRACT_MIN_NAME_LENGTH: usize = 1;
pub const CONTRACT_MAX_NAME_LENGTH: usize = 40;
@@ -64,8 +66,8 @@ guarded_string!(
"ClarityName",
CLARITY_NAME_REGEX,
MAX_STRING_LEN,
- CodecError,
- CodecError::InvalidClarityName
+ RuntimeErrorType,
+ RuntimeErrorType::BadNameValue
);
guarded_string!(
@@ -73,8 +75,8 @@ guarded_string!(
"ContractName",
CONTRACT_NAME_REGEX,
MAX_STRING_LEN,
- CodecError,
- CodecError::InvalidContractName
+ RuntimeErrorType,
+ RuntimeErrorType::BadNameValue
);
impl StacksMessageCodec for ClarityName {
@@ -160,3 +162,513 @@ impl StacksMessageCodec for ContractName {
Ok(name)
}
}
+
+#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
+pub enum PreSymbolicExpressionType {
+ AtomValue(Value),
+ Atom(ClarityName),
+ List(Vec),
+ Tuple(Vec),
+ SugaredContractIdentifier(ContractName),
+ SugaredFieldIdentifier(ContractName, ClarityName),
+ FieldIdentifier(TraitIdentifier),
+ TraitReference(ClarityName),
+ Comment(String),
+ Placeholder(String),
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
+pub struct PreSymbolicExpression {
+ pub pre_expr: PreSymbolicExpressionType,
+ pub id: u64,
+
+ #[cfg(feature = "developer-mode")]
+ pub span: Span,
+}
+
+pub trait SymbolicExpressionCommon {
+ type S: SymbolicExpressionCommon;
+ fn set_id(&mut self, id: u64);
+ fn match_list_mut(&mut self) -> Option<&mut [Self::S]>;
+}
+
+impl SymbolicExpressionCommon for PreSymbolicExpression {
+ type S = PreSymbolicExpression;
+ fn set_id(&mut self, id: u64) {
+ self.id = id;
+ }
+ fn match_list_mut(&mut self) -> Option<&mut [PreSymbolicExpression]> {
+ if let PreSymbolicExpressionType::List(ref mut list) = self.pre_expr {
+ Some(list)
+ } else {
+ None
+ }
+ }
+}
+
+impl SymbolicExpressionCommon for SymbolicExpression {
+ type S = SymbolicExpression;
+ fn set_id(&mut self, id: u64) {
+ self.id = id;
+ }
+ fn match_list_mut(&mut self) -> Option<&mut [SymbolicExpression]> {
+ if let SymbolicExpressionType::List(ref mut list) = self.expr {
+ Some(list)
+ } else {
+ None
+ }
+ }
+}
+
+impl PreSymbolicExpression {
+ #[cfg(feature = "developer-mode")]
+ fn cons() -> PreSymbolicExpression {
+ PreSymbolicExpression {
+ id: 0,
+ span: Span::zero(),
+ pre_expr: PreSymbolicExpressionType::AtomValue(Value::Bool(false)),
+ }
+ }
+ #[cfg(not(feature = "developer-mode"))]
+ fn cons() -> PreSymbolicExpression {
+ PreSymbolicExpression {
+ id: 0,
+ pre_expr: PreSymbolicExpressionType::AtomValue(Value::Bool(false)),
+ }
+ }
+
+ #[cfg(feature = "developer-mode")]
+ pub fn set_span(&mut self, start_line: u32, start_column: u32, end_line: u32, end_column: u32) {
+ self.span = Span {
+ start_line,
+ start_column,
+ end_line,
+ end_column,
+ }
+ }
+
+ #[cfg(not(feature = "developer-mode"))]
+ pub fn set_span(
+ &mut self,
+ _start_line: u32,
+ _start_column: u32,
+ _end_line: u32,
+ _end_column: u32,
+ ) {
+ }
+
+ #[cfg(feature = "developer-mode")]
+ pub fn copy_span(&mut self, src: &Span) {
+ self.span = src.clone();
+ }
+
+ #[cfg(not(feature = "developer-mode"))]
+ pub fn copy_span(&mut self, _src: &Span) {}
+
+ #[cfg(feature = "developer-mode")]
+ pub fn span(&self) -> &Span {
+ &self.span
+ }
+
+ #[cfg(not(feature = "developer-mode"))]
+ pub fn span(&self) -> &Span {
+ &Span::ZERO
+ }
+
+ pub fn sugared_contract_identifier(val: ContractName) -> PreSymbolicExpression {
+ PreSymbolicExpression {
+ pre_expr: PreSymbolicExpressionType::SugaredContractIdentifier(val),
+ ..PreSymbolicExpression::cons()
+ }
+ }
+
+ pub fn sugared_field_identifier(
+ contract_name: ContractName,
+ name: ClarityName,
+ ) -> PreSymbolicExpression {
+ PreSymbolicExpression {
+ pre_expr: PreSymbolicExpressionType::SugaredFieldIdentifier(contract_name, name),
+ ..PreSymbolicExpression::cons()
+ }
+ }
+
+ pub fn atom_value(val: Value) -> PreSymbolicExpression {
+ PreSymbolicExpression {
+ pre_expr: PreSymbolicExpressionType::AtomValue(val),
+ ..PreSymbolicExpression::cons()
+ }
+ }
+
+ pub fn atom(val: ClarityName) -> PreSymbolicExpression {
+ PreSymbolicExpression {
+ pre_expr: PreSymbolicExpressionType::Atom(val),
+ ..PreSymbolicExpression::cons()
+ }
+ }
+
+ pub fn trait_reference(val: ClarityName) -> PreSymbolicExpression {
+ PreSymbolicExpression {
+ pre_expr: PreSymbolicExpressionType::TraitReference(val),
+ ..PreSymbolicExpression::cons()
+ }
+ }
+
+ pub fn field_identifier(val: TraitIdentifier) -> PreSymbolicExpression {
+ PreSymbolicExpression {
+ pre_expr: PreSymbolicExpressionType::FieldIdentifier(val),
+ ..PreSymbolicExpression::cons()
+ }
+ }
+
+ pub fn list(val: Vec) -> PreSymbolicExpression {
+ PreSymbolicExpression {
+ pre_expr: PreSymbolicExpressionType::List(val),
+ ..PreSymbolicExpression::cons()
+ }
+ }
+
+ pub fn tuple(val: Vec) -> PreSymbolicExpression {
+ PreSymbolicExpression {
+ pre_expr: PreSymbolicExpressionType::Tuple(val),
+ ..PreSymbolicExpression::cons()
+ }
+ }
+
+ pub fn placeholder(s: String) -> PreSymbolicExpression {
+ PreSymbolicExpression {
+ pre_expr: PreSymbolicExpressionType::Placeholder(s),
+ ..PreSymbolicExpression::cons()
+ }
+ }
+
+ pub fn comment(comment: String) -> PreSymbolicExpression {
+ PreSymbolicExpression {
+ pre_expr: PreSymbolicExpressionType::Comment(comment),
+ ..PreSymbolicExpression::cons()
+ }
+ }
+
+ pub fn match_trait_reference(&self) -> Option<&ClarityName> {
+ if let PreSymbolicExpressionType::TraitReference(ref value) = self.pre_expr {
+ Some(value)
+ } else {
+ None
+ }
+ }
+
+ pub fn match_atom_value(&self) -> Option<&Value> {
+ if let PreSymbolicExpressionType::AtomValue(ref value) = self.pre_expr {
+ Some(value)
+ } else {
+ None
+ }
+ }
+
+ pub fn match_atom(&self) -> Option<&ClarityName> {
+ if let PreSymbolicExpressionType::Atom(ref value) = self.pre_expr {
+ Some(value)
+ } else {
+ None
+ }
+ }
+
+ pub fn match_list(&self) -> Option<&[PreSymbolicExpression]> {
+ if let PreSymbolicExpressionType::List(ref list) = self.pre_expr {
+ Some(list)
+ } else {
+ None
+ }
+ }
+
+ pub fn match_field_identifier(&self) -> Option<&TraitIdentifier> {
+ if let PreSymbolicExpressionType::FieldIdentifier(ref value) = self.pre_expr {
+ Some(value)
+ } else {
+ None
+ }
+ }
+
+ pub fn match_placeholder(&self) -> Option<&str> {
+ if let PreSymbolicExpressionType::Placeholder(ref s) = self.pre_expr {
+ Some(s.as_str())
+ } else {
+ None
+ }
+ }
+
+ pub fn match_comment(&self) -> Option<&str> {
+ if let PreSymbolicExpressionType::Comment(ref s) = self.pre_expr {
+ Some(s.as_str())
+ } else {
+ None
+ }
+ }
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
+pub enum SymbolicExpressionType {
+ AtomValue(Value),
+ Atom(ClarityName),
+ List(Vec),
+ LiteralValue(Value),
+ Field(TraitIdentifier),
+ TraitReference(ClarityName, TraitDefinition),
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
+pub enum TraitDefinition {
+ Defined(TraitIdentifier),
+ Imported(TraitIdentifier),
+}
+
+pub fn depth_traverse(expr: &SymbolicExpression, mut visit: F) -> Result