Skip to content

Commit 9e6280e

Browse files
Merge pull request #6806 from jacinta-stacks/refactor/remove-ast-error
Refactor: remove ASTError from VmExecutionError and seperate parse_ast from vm calls
2 parents 3d27a91 + 04d7e66 commit 9e6280e

File tree

35 files changed

+443
-362
lines changed

35 files changed

+443
-362
lines changed

clarity/src/vm/analysis/type_checker/v2_1/tests/contracts.rs

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1636,9 +1636,7 @@ fn clarity_trait_experiments_use_undefined(
16361636
let err = db
16371637
.execute(|db| load_versioned(db, "no-trait", version, epoch))
16381638
.unwrap_err();
1639-
assert!(err.starts_with(
1640-
"ASTError(ParseError { err: TraitReferenceUnknown(\"trait-to-be-defined-later\")"
1641-
));
1639+
assert!(err.starts_with("TraitReferenceUnknown(\"trait-to-be-defined-later\")"));
16421640
}
16431641

16441642
#[apply(test_clarity_versions)]
@@ -1656,7 +1654,7 @@ fn clarity_trait_experiments_circular(
16561654
load_versioned(db, "circular-trait-2", version, epoch)
16571655
})
16581656
.unwrap_err();
1659-
assert!(err.starts_with("ASTError(ParseError { err: CircularReference([\"circular\"])"));
1657+
assert!(err.starts_with("CircularReference([\"circular\"])"));
16601658
}
16611659

16621660
#[apply(test_clarity_versions)]
@@ -1894,7 +1892,7 @@ fn clarity_trait_experiments_selfret_trait(
18941892
let err = db
18951893
.execute(|db| load_versioned(db, "selfret-trait", version, epoch))
18961894
.unwrap_err();
1897-
assert!(err.starts_with("ASTError(ParseError { err: CircularReference([\"self-return\"])"));
1895+
assert!(err.starts_with("CircularReference([\"self-return\"])"));
18981896
}
18991897

19001898
#[apply(test_clarity_versions)]
@@ -2300,7 +2298,7 @@ fn clarity_trait_experiments_trait_data_1(
23002298
load_versioned(db, "trait-data-1", version, epoch)
23012299
})
23022300
.unwrap_err();
2303-
assert!(err.starts_with("ASTError(ParseError { err: TraitReferenceNotAllowed"));
2301+
assert!(err.starts_with("TraitReferenceNotAllowed"));
23042302
}
23052303

23062304
#[apply(test_clarity_versions)]
@@ -2319,7 +2317,7 @@ fn clarity_trait_experiments_trait_data_2(
23192317
load_versioned(db, "trait-data-2", version, epoch)
23202318
})
23212319
.unwrap_err();
2322-
assert!(err.starts_with("ASTError(ParseError { err: TraitReferenceNotAllowed"));
2320+
assert!(err.starts_with("TraitReferenceNotAllowed"));
23232321
}
23242322

23252323
#[apply(test_clarity_versions)]

clarity/src/vm/ast/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ pub fn parse(
5050
source_code: &str,
5151
version: ClarityVersion,
5252
epoch: StacksEpochId,
53-
) -> Result<Vec<crate::vm::representations::SymbolicExpression>, crate::vm::errors::VmExecutionError>
53+
) -> Result<Vec<crate::vm::representations::SymbolicExpression>, clarity_types::errors::ParseError>
5454
{
5555
let ast = build_ast(contract_identifier, source_code, &mut (), version, epoch)?;
5656
Ok(ast.expressions)

clarity/src/vm/clarity.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ use crate::vm::ast::errors::{ParseError, ParseErrorKind};
2525
use crate::vm::contexts::{AssetMap, Environment, OwnedEnvironment};
2626
use crate::vm::costs::{ExecutionCost, LimitedCostTracker};
2727
use crate::vm::database::ClarityDatabase;
28-
use crate::vm::errors::VmExecutionError;
28+
use crate::vm::errors::{ClarityEvalError, VmExecutionError};
2929
use crate::vm::events::StacksTransactionEvent;
3030
use crate::vm::types::{BuffData, PrincipalData, QualifiedContractIdentifier};
3131
use crate::vm::{ClarityVersion, ContractContext, SymbolicExpression, Value, analysis, ast};
@@ -111,6 +111,15 @@ impl From<StaticCheckError> for ClarityError {
111111
}
112112
}
113113

114+
impl From<ClarityEvalError> for ClarityError {
115+
fn from(e: ClarityEvalError) -> Self {
116+
match e {
117+
ClarityEvalError::Parse(err) => ClarityError::from(err),
118+
ClarityEvalError::Vm(err) => ClarityError::from(err),
119+
}
120+
}
121+
}
122+
114123
/// Converts [`VmExecutionError`] to [`ClarityError`] for transaction execution contexts.
115124
///
116125
/// This conversion is used in:
@@ -195,9 +204,9 @@ pub trait ClarityConnection {
195204
sponsor: Option<PrincipalData>,
196205
cost_track: LimitedCostTracker,
197206
to_do: F,
198-
) -> Result<R, VmExecutionError>
207+
) -> Result<R, ClarityEvalError>
199208
where
200-
F: FnOnce(&mut Environment) -> Result<R, VmExecutionError>,
209+
F: FnOnce(&mut Environment) -> Result<R, ClarityEvalError>,
201210
{
202211
let epoch_id = self.get_epoch();
203212
let clarity_version = ClarityVersion::default_for_epoch(epoch_id);

clarity/src/vm/contexts.rs

Lines changed: 60 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ use std::fmt;
1919
use std::mem::replace;
2020
use std::time::{Duration, Instant};
2121

22+
use clarity_types::errors::{ParseError, ParseErrorKind};
2223
use clarity_types::representations::ClarityName;
2324
use serde::Serialize;
2425
use serde_json::json;
@@ -36,7 +37,8 @@ use crate::vm::database::{
3637
NonFungibleTokenMetadata,
3738
};
3839
use crate::vm::errors::{
39-
RuntimeCheckErrorKind, RuntimeError, StackTrace, VmExecutionError, VmInternalError,
40+
ClarityEvalError, RuntimeCheckErrorKind, RuntimeError, StackTrace, VmExecutionError,
41+
VmInternalError,
4042
};
4143
use crate::vm::events::*;
4244
use 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]

clarity/src/vm/docs/contracts.rs

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ use crate::vm::contexts::GlobalContext;
2323
use crate::vm::costs::LimitedCostTracker;
2424
use crate::vm::database::MemoryBackingStore;
2525
use crate::vm::docs::{get_input_type_string, get_output_type_string, get_signature};
26+
use crate::vm::errors::ClarityEvalError;
2627
use crate::vm::types::{FunctionType, QualifiedContractIdentifier, Value};
2728
use crate::vm::version::ClarityVersion;
2829
use crate::vm::{self, ContractContext};
@@ -81,7 +82,7 @@ fn get_constant_value(var_name: &str, contract_content: &str) -> Value {
8182
.expect("BUG: failed to return constant value")
8283
}
8384

84-
fn doc_execute(program: &str) -> Result<Option<Value>, vm::VmExecutionError> {
85+
fn doc_execute(program: &str) -> Result<Option<Value>, ClarityEvalError> {
8586
let contract_id = QualifiedContractIdentifier::transient();
8687
let mut contract_context = ContractContext::new(contract_id.clone(), ClarityVersion::Clarity2);
8788
let mut marf = MemoryBackingStore::new();
@@ -93,17 +94,17 @@ fn doc_execute(program: &str) -> Result<Option<Value>, vm::VmExecutionError> {
9394
LimitedCostTracker::new_free(),
9495
DOCS_GENERATION_EPOCH,
9596
);
96-
global_context.execute(|g| {
97-
let parsed = build_ast(
98-
&contract_id,
99-
program,
100-
&mut (),
101-
ClarityVersion::latest(),
102-
StacksEpochId::latest(),
103-
)?
104-
.expressions;
105-
vm::eval_all(&parsed, &mut contract_context, g, None)
106-
})
97+
let parsed = build_ast(
98+
&contract_id,
99+
program,
100+
&mut (),
101+
ClarityVersion::latest(),
102+
StacksEpochId::latest(),
103+
)?
104+
.expressions;
105+
global_context
106+
.execute(|g| vm::eval_all(&parsed, &mut contract_context, g, None))
107+
.map_err(ClarityEvalError::from)
107108
}
108109

109110
#[allow(clippy::expect_used)]

0 commit comments

Comments
 (0)