@@ -19,6 +19,7 @@ use std::fmt;
1919use std:: mem:: replace;
2020use std:: time:: { Duration , Instant } ;
2121
22+ use clarity_types:: errors:: { ParseError , ParseErrorKind } ;
2223use clarity_types:: representations:: ClarityName ;
2324use serde:: Serialize ;
2425use serde_json:: json;
@@ -36,7 +37,8 @@ use crate::vm::database::{
3637 NonFungibleTokenMetadata ,
3738} ;
3839use crate :: vm:: errors:: {
39- RuntimeCheckErrorKind , RuntimeError , StackTrace , VmExecutionError , VmInternalError ,
40+ ClarityEvalError , RuntimeCheckErrorKind , RuntimeError , StackTrace , VmExecutionError ,
41+ VmInternalError ,
4042} ;
4143use crate :: vm:: events:: * ;
4244use crate :: vm:: representations:: SymbolicExpression ;
@@ -722,7 +724,7 @@ impl<'a, 'hooks> OwnedEnvironment<'a, 'hooks> {
722724 contract_identifier : QualifiedContractIdentifier ,
723725 contract_content : & str ,
724726 sponsor : Option < PrincipalData > ,
725- ) -> Result < ( ( ) , AssetMap , Vec < StacksTransactionEvent > ) , VmExecutionError > {
727+ ) -> Result < ( ( ) , AssetMap , Vec < StacksTransactionEvent > ) , ClarityEvalError > {
726728 self . execute_in_env (
727729 contract_identifier. issuer . clone ( ) . into ( ) ,
728730 sponsor,
@@ -737,7 +739,7 @@ impl<'a, 'hooks> OwnedEnvironment<'a, 'hooks> {
737739 version : ClarityVersion ,
738740 contract_content : & str ,
739741 sponsor : Option < PrincipalData > ,
740- ) -> Result < ( ( ) , AssetMap , Vec < StacksTransactionEvent > ) , VmExecutionError > {
742+ ) -> Result < ( ( ) , AssetMap , Vec < StacksTransactionEvent > ) , ClarityEvalError > {
741743 self . execute_in_env (
742744 contract_identifier. issuer . clone ( ) . into ( ) ,
743745 sponsor,
@@ -831,7 +833,7 @@ impl<'a, 'hooks> OwnedEnvironment<'a, 'hooks> {
831833 pub fn eval_raw (
832834 & mut self ,
833835 program : & str ,
834- ) -> Result < ( Value , AssetMap , Vec < StacksTransactionEvent > ) , VmExecutionError > {
836+ ) -> Result < ( Value , AssetMap , Vec < StacksTransactionEvent > ) , ClarityEvalError > {
835837 self . execute_in_env (
836838 QualifiedContractIdentifier :: transient ( ) . issuer . into ( ) ,
837839 None ,
@@ -844,7 +846,7 @@ impl<'a, 'hooks> OwnedEnvironment<'a, 'hooks> {
844846 & mut self ,
845847 contract : & QualifiedContractIdentifier ,
846848 program : & str ,
847- ) -> Result < ( Value , AssetMap , Vec < StacksTransactionEvent > ) , VmExecutionError > {
849+ ) -> Result < ( Value , AssetMap , Vec < StacksTransactionEvent > ) , ClarityEvalError > {
848850 self . execute_in_env (
849851 QualifiedContractIdentifier :: transient ( ) . issuer . into ( ) ,
850852 None ,
@@ -1014,31 +1016,8 @@ impl<'a, 'b, 'hooks> Environment<'a, 'b, 'hooks> {
10141016 & mut self ,
10151017 contract_identifier : & QualifiedContractIdentifier ,
10161018 program : & str ,
1017- ) -> Result < Value , VmExecutionError > {
1018- let clarity_version = self . contract_context . clarity_version ;
1019-
1020- let parsed = ast:: build_ast (
1021- contract_identifier,
1022- program,
1023- self ,
1024- clarity_version,
1025- self . global_context . epoch_id ,
1026- ) ?
1027- . expressions ;
1028-
1029- if parsed. is_empty ( ) {
1030- // `TypeParseFailure` is **unreachable** in standard Clarity VM execution.
1031- // - `eval_read_only` parses a raw program string into an AST.
1032- // - Any empty or invalid program would be rejected at publish/deploy time or earlier parsing stages.
1033- // - Therefore, `parsed.is_empty()` cannot occur for programs originating from a valid contract
1034- // or transaction.
1035- // - Only malformed input fed directly to this internal method (e.g., in unit tests or
1036- // artificial VM invocations) can trigger this error.
1037- return Err ( RuntimeError :: TypeParseFailure (
1038- "Expected a program of at least length 1" . to_string ( ) ,
1039- )
1040- . into ( ) ) ;
1041- }
1019+ ) -> Result < Value , ClarityEvalError > {
1020+ let parsed = self . parse_nonempty_program ( contract_identifier, program) ?;
10421021
10431022 self . global_context . begin ( ) ;
10441023
@@ -1062,40 +1041,19 @@ impl<'a, 'b, 'hooks> Environment<'a, 'b, 'hooks> {
10621041 ) ;
10631042 let local_context = LocalContext :: new ( ) ;
10641043 eval ( & parsed[ 0 ] , & mut nested_env, & local_context)
1065- } ;
1044+ }
1045+ . map_err ( ClarityEvalError :: from) ;
10661046
10671047 self . global_context . roll_back ( ) ?;
10681048
10691049 result
10701050 }
10711051
1072- pub fn eval_raw ( & mut self , program : & str ) -> Result < Value , VmExecutionError > {
1073- let contract_id = QualifiedContractIdentifier :: transient ( ) ;
1074- let clarity_version = self . contract_context . clarity_version ;
1075-
1076- let parsed = ast:: build_ast (
1077- & contract_id,
1078- program,
1079- self ,
1080- clarity_version,
1081- self . global_context . epoch_id ,
1082- ) ?
1083- . expressions ;
1084-
1085- if parsed. is_empty ( ) {
1086- // `TypeParseFailure` is **unreachable** in standard Clarity VM execution.
1087- // - `eval_raw` parses a raw program string into an AST.
1088- // - All programs deployed or called via the standard VM go through static parsing and validation first.
1089- // - Any empty or invalid program would be rejected at publish/deploy time or earlier parsing stages.
1090- // - Therefore, `parsed.is_empty()` cannot occur for a program that originates from a valid Clarity contract or transaction.
1091- // Only malformed input directly fed to this internal method (e.g., in unit tests) can trigger this error.
1092- return Err ( RuntimeError :: TypeParseFailure (
1093- "Expected a program of at least length 1" . to_string ( ) ,
1094- )
1095- . into ( ) ) ;
1096- }
1052+ pub fn eval_raw ( & mut self , program : & str ) -> Result < Value , ClarityEvalError > {
1053+ let parsed =
1054+ self . parse_nonempty_program ( & QualifiedContractIdentifier :: transient ( ) , program) ?;
10971055 let local_context = LocalContext :: new ( ) ;
1098- eval ( & parsed[ 0 ] , self , & local_context)
1056+ eval ( & parsed[ 0 ] , self , & local_context) . map_err ( ClarityEvalError :: from )
10991057 }
11001058
11011059 /// Used only for contract-call! cost short-circuiting. Once the short-circuited cost
@@ -1115,6 +1073,41 @@ impl<'a, 'b, 'hooks> Environment<'a, 'b, 'hooks> {
11151073 result
11161074 }
11171075
1076+ /// Parse `program` into a **non-empty** list of `SymbolicExpression`s.
1077+ ///
1078+ /// This is a wrapper around `ast::build_ast(..)` that enforces the invariant
1079+ /// that a parsed program must contain at least one top-level expression.
1080+ ///
1081+ /// # Errors
1082+ /// - Returns `Err` if the program fails to parse/build an AST.
1083+ /// - Returns `Err(UnexpectedParserFailure)` if parsing succeeds but yields *zero* expressions.
1084+ ///
1085+ /// # Notes
1086+ /// The empty-expression case should be unreachable for normal VM execution because
1087+ /// published/deployed contract code and transaction programs are validated earlier.
1088+ /// It exists as a defensive check for malformed input in tests, fuzzing, or internal
1089+ /// callers that bypass normal validation paths.
1090+ fn parse_nonempty_program (
1091+ & mut self ,
1092+ contract_identifier : & QualifiedContractIdentifier ,
1093+ program : & str ,
1094+ ) -> Result < Vec < SymbolicExpression > , ClarityEvalError > {
1095+ let expressions = ast:: build_ast (
1096+ contract_identifier,
1097+ program,
1098+ self ,
1099+ self . contract_context . clarity_version ,
1100+ self . global_context . epoch_id ,
1101+ ) ?
1102+ . expressions ;
1103+
1104+ if expressions. is_empty ( ) {
1105+ return Err ( ParseError :: from ( ParseErrorKind :: UnexpectedParserFailure ) . into ( ) ) ;
1106+ }
1107+
1108+ Ok ( expressions)
1109+ }
1110+
11181111 /// This is the epoch of the block that this transaction is executing within.
11191112 /// Note: in the current plans for 2.1, there is also a contract-specific **Clarity version**
11201113 /// which governs which native functions are available / defined. That is separate from this
@@ -1303,7 +1296,7 @@ impl<'a, 'b, 'hooks> Environment<'a, 'b, 'hooks> {
13031296 & mut self ,
13041297 contract_identifier : QualifiedContractIdentifier ,
13051298 contract_content : & str ,
1306- ) -> Result < ( ) , VmExecutionError > {
1299+ ) -> Result < ( ) , ClarityEvalError > {
13071300 let clarity_version = self . contract_context . clarity_version ;
13081301
13091302 let contract_ast = ast:: build_ast (
@@ -1319,6 +1312,7 @@ impl<'a, 'b, 'hooks> Environment<'a, 'b, 'hooks> {
13191312 & contract_ast,
13201313 contract_content,
13211314 )
1315+ . map_err ( ClarityEvalError :: from)
13221316 }
13231317
13241318 pub fn initialize_contract_from_ast (
@@ -1754,7 +1748,7 @@ impl<'a, 'hooks> GlobalContext<'a, 'hooks> {
17541748 f : F ,
17551749 ) -> std:: result:: Result < A , E >
17561750 where
1757- E : From < VmExecutionError > ,
1751+ E : From < ClarityEvalError > ,
17581752 F : FnOnce ( & mut Environment ) -> std:: result:: Result < A , E > ,
17591753 {
17601754 self . begin ( ) ;
@@ -1773,7 +1767,7 @@ impl<'a, 'hooks> GlobalContext<'a, 'hooks> {
17731767 ) ;
17741768 f ( & mut exec_env)
17751769 } ;
1776- self . roll_back ( ) ?;
1770+ self . roll_back ( ) . map_err ( ClarityEvalError :: from ) ?;
17771771
17781772 match result {
17791773 Ok ( return_value) => Ok ( return_value) ,
@@ -2422,13 +2416,9 @@ mod test {
24222416 // Call eval_read_only with an empty program
24232417 let program = "" ; // empty program triggers parsed.is_empty()
24242418 let err = env. eval_raw ( program) . unwrap_err ( ) ;
2425-
2426- assert ! (
2427- matches!(
2428- err,
2429- VmExecutionError :: Runtime ( RuntimeError :: TypeParseFailure ( msg) , _) if msg. contains( "Expected a program of at least length 1" ) ) ,
2430- "Expected a type parse failure"
2431- ) ;
2419+ let expected_err =
2420+ ClarityEvalError :: from ( ParseError :: new ( ParseErrorKind :: UnexpectedParserFailure ) ) ;
2421+ assert_eq ! ( err, expected_err, "Expected a type parse failure" ) ;
24322422 }
24332423
24342424 #[ test]
@@ -2443,13 +2433,9 @@ mod test {
24432433 // Call eval_read_only with an empty program
24442434 let program = "" ; // empty program triggers parsed.is_empty()
24452435 let err = env. eval_read_only ( & contract_id, program) . unwrap_err ( ) ;
2446-
2447- assert ! (
2448- matches!(
2449- err,
2450- VmExecutionError :: Runtime ( RuntimeError :: TypeParseFailure ( msg) , _) if msg. contains( "Expected a program of at least length 1" ) ) ,
2451- "Expected a type parse failure"
2452- ) ;
2436+ let expected_err =
2437+ ClarityEvalError :: from ( ParseError :: new ( ParseErrorKind :: UnexpectedParserFailure ) ) ;
2438+ assert_eq ! ( err, expected_err, "Expected a type parse failure" ) ;
24532439 }
24542440
24552441 #[ test]
0 commit comments