diff --git a/.github/workflows/cargo-hack-check.yml b/.github/workflows/cargo-hack-check.yml
new file mode 100644
index 0000000000..99e9819690
--- /dev/null
+++ b/.github/workflows/cargo-hack-check.yml
@@ -0,0 +1,118 @@
+name: Cargo Hack Check
+
+on:
+ workflow_call:
+
+env:
+ RUST_BACKTRACE: full
+
+concurrency:
+ group: cargo-hack-check-${{ github.head_ref || github.ref || github.run_id }}
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
+
+jobs:
+ # Setup job to prepare common dependencies
+ setup:
+ name: Setup
+ runs-on: ubuntu-latest
+ outputs:
+ rust-toolchain: ${{ steps.toolchain.outputs.rust-toolchain }}
+ steps:
+ - name: Checkout
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ persist-credentials: false
+
+ - name: Get Rust toolchain
+ id: toolchain
+ run: echo "rust-toolchain=$(cat ./rust-toolchain)" >> $GITHUB_OUTPUT
+
+ # Native targets (Windows/Linux)
+ native-targets:
+ name: All Crates (Windows/Linux)
+ runs-on: ubuntu-latest
+ needs: setup
+ steps:
+ - name: Checkout
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ persist-credentials: false
+
+ - name: Setup Rust with Cache
+ uses: actions-rust-lang/setup-rust-toolchain@11df97af8e8102fd60b60a77dfbf58d40cd843b8 # v1.10.1
+ with:
+ toolchain: ${{ needs.setup.outputs.rust-toolchain }}
+ target: x86_64-pc-windows-gnu,x86_64-unknown-linux-gnu
+ cache: true
+ cache-key: cargo-hack-native-${{ needs.setup.outputs.rust-toolchain }}-${{ hashFiles('**/Cargo.lock') }}
+
+ - name: Install cargo-hack
+ uses: taiki-e/install-action@2383334cf567d78771fc7d89b6b3802ef1412cf6 # v2.56.8
+ with:
+ tool: cargo-hack
+
+ - name: Install Windows cross-compilation tools
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y gcc-mingw-w64-x86-64
+
+ - name: Run cargo hack check
+ run: |
+ cargo hack check \
+ --all \
+ --each-feature \
+ --no-dev-deps \
+ --exclude-features=wasm-deterministic,wasm-web \
+ --target x86_64-pc-windows-gnu \
+ --target x86_64-unknown-linux-gnu
+
+ # WASM targets - separate cache since dependencies differ
+ wasm-targets:
+ name: ${{ matrix.name }}
+ runs-on: ubuntu-latest
+ needs: setup
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - name: "Clarity & Stacks-Common WASM Web"
+ command: |
+ cargo hack check \
+ -p clarity \
+ -p stacks-common \
+ --each-feature \
+ --no-dev-deps \
+ --exclude-features=default,rusqlite,testing,wasm-deterministic \
+ --features=wasm-web
+
+ - name: "Clarity & Stacks-Common WASM Deterministic"
+ command: |
+ cargo hack check \
+ -p clarity \
+ -p stacks-common \
+ --each-feature \
+ --no-dev-deps \
+ --include-features=wasm-deterministic,slog_json \
+ --features=wasm-deterministic
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ persist-credentials: false
+
+ - name: Setup Rust with Cache
+ uses: actions-rust-lang/setup-rust-toolchain@11df97af8e8102fd60b60a77dfbf58d40cd843b8 # v1.10.1
+ with:
+ toolchain: ${{ needs.setup.outputs.rust-toolchain }}
+ target: wasm32-unknown-unknown
+ cache: true
+ cache-key: cargo-hack-wasm-${{ matrix.name }}-${{ needs.setup.outputs.rust-toolchain }}-${{ hashFiles('**/Cargo.lock') }}
+
+ - name: Install cargo-hack
+ uses: taiki-e/install-action@2383334cf567d78771fc7d89b6b3802ef1412cf6 # v2.56.8
+ with:
+ tool: cargo-hack
+
+ - name: Run cargo hack check
+ run: ${{ matrix.command }}
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 1ae2b64189..d5ad1d8e06 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -93,7 +93,7 @@ jobs:
needs:
- rustfmt
- check-release
- secrets: inherit
+ secrets: inherit
uses: ./.github/workflows/github-release.yml
with:
node_tag: ${{ needs.check-release.outputs.node_tag }}
@@ -145,7 +145,28 @@ jobs:
- check-release
uses: ./.github/workflows/stacks-core-tests.yml
- ## Checks to run on built binaries
+ ## Validate constants dumped by stacks-inspect
+ ##
+ ## Runs when:
+ ## - it is a node or signer-only release run
+ ## or any of:
+ ## - this workflow is called manually
+ ## - PR is opened
+ ## - PR added to merge queue
+ constants-check:
+ if: |
+ needs.check-release.outputs.is_node_release == 'true' ||
+ needs.check-release.outputs.is_signer_release == 'true' ||
+ github.event_name == 'workflow_dispatch' ||
+ github.event_name == 'pull_request' ||
+ github.event_name == 'merge_group'
+ name: Constants Check
+ needs:
+ - rustfmt
+ - check-release
+ uses: ./.github/workflows/constants-check.yml
+
+ ## Cargo check for Linux/Windows/Wasm32 targets with all features
##
## Runs when:
## - it is a node or signer-only release run
@@ -153,18 +174,18 @@ jobs:
## - this workflow is called manually
## - PR is opened
## - PR added to merge queue
- stacks-core-build-tests:
+ cargo-hack-check:
if: |
needs.check-release.outputs.is_node_release == 'true' ||
needs.check-release.outputs.is_signer_release == 'true' ||
github.event_name == 'workflow_dispatch' ||
github.event_name == 'pull_request' ||
github.event_name == 'merge_group'
- name: Stacks Core Build Tests
+ name: Cargo Hack Check
needs:
- rustfmt
- check-release
- uses: ./.github/workflows/core-build-tests.yml
+ uses: ./.github/workflows/cargo-hack-check.yml
## Checks to run on built binaries
##
diff --git a/.github/workflows/core-build-tests.yml b/.github/workflows/constants-check.yml
similarity index 62%
rename from .github/workflows/core-build-tests.yml
rename to .github/workflows/constants-check.yml
index b6ce3f487a..5c3b563acd 100644
--- a/.github/workflows/core-build-tests.yml
+++ b/.github/workflows/constants-check.yml
@@ -1,10 +1,9 @@
-name: Core build tests
+name: Constants Check
-# Only run when:
-# - PRs are (re)opened against develop branch
+# Validates that the constants dumped by stacks-inspect match expected values
on:
workflow_call:
-
+
jobs:
check-consts:
name: Check the constants from stacks-inspect
@@ -26,27 +25,6 @@ jobs:
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
- ## run cargo check steps
- - name: Cargo Check
- id: cargo_check
- run: |
- cargo check
-
- - name: Cargo Check (monitoring_prom)
- id: cargo_check_prom
- run: |
- cargo check --features monitoring_prom
-
- - name: Cargo Check (clarity)
- id: cargo_check_clarity
- run: |
- cargo check -p clarity --no-default-features
-
- - name: Cargo Check (stacks-common)
- id: cargo_check_stacks-common
- run: |
- cargo check -p stacks-common --no-default-features
-
- name: Dump constants JSON
id: consts-dump
run: |
diff --git a/Cargo.lock b/Cargo.lock
index 5326152b43..c0441023c6 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -425,9 +425,9 @@ checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
[[package]]
name = "base64ct"
-version = "1.6.0"
+version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
+checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba"
[[package]]
name = "bit-set"
@@ -542,16 +542,16 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
-version = "0.4.34"
+version = "0.4.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b"
+checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"wasm-bindgen",
- "windows-targets 0.52.0",
+ "windows-link",
]
[[package]]
@@ -610,7 +610,6 @@ dependencies = [
"lazy_static",
"mutants",
"rand 0.8.5",
- "rand_chacha 0.3.1",
"regex",
"rstest",
"rstest_reuse",
@@ -799,9 +798,9 @@ checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5"
[[package]]
name = "der"
-version = "0.7.8"
+version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c"
+checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
dependencies = [
"const-oid",
"zeroize",
@@ -1206,8 +1205,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5"
dependencies = [
"cfg-if 1.0.0",
+ "js-sys",
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
+ "wasm-bindgen",
]
[[package]]
@@ -3169,13 +3170,13 @@ dependencies = [
"chrono",
"curve25519-dalek",
"ed25519-dalek",
+ "getrandom 0.2.12",
"hashbrown 0.15.2",
"lazy_static",
"libsecp256k1",
"nix",
"proptest",
"rand 0.8.5",
- "rand_core 0.6.4",
"ripemd",
"rusqlite",
"secp256k1",
@@ -4178,6 +4179,12 @@ dependencies = [
"windows-targets 0.52.0",
]
+[[package]]
+name = "windows-link"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
+
[[package]]
name = "windows-sys"
version = "0.48.0"
@@ -4352,6 +4359,6 @@ dependencies = [
[[package]]
name = "zeroize"
-version = "1.7.0"
+version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d"
+checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
diff --git a/Cargo.toml b/Cargo.toml
index 74f59ccb84..cabb5abb99 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -14,24 +14,29 @@ members = [
# Dependencies we want to keep the same between workspace members
[workspace.dependencies]
-ed25519-dalek = { version = "2.1.1", features = ["serde", "rand_core"] }
+ed25519-dalek = { version = "2.1.1", default-features = false }
hashbrown = { version = "0.15.2", features = ["serde"] }
+lazy_static = "1.4.0"
rand_core = "0.6.4"
rand = "0.8"
rand_chacha = "0.3.1"
-tikv-jemallocator = "0.5.4"
+serde = "1"
+serde_derive = "1"
+serde_json = { version = "1.0", features = ["arbitrary_precision", "unbounded_depth"] }
+slog = { version = "2.5.2", features = [ "max_level_trace" ] }
rusqlite = { version = "0.31.0", features = ["blob", "serde_json", "i128_blob", "bundled", "trace"] }
+tikv-jemallocator = "0.5.4"
thiserror = "1.0.65"
toml = "0.5.6"
# Use a bit more than default optimization for
-# dev builds to speed up test execution
+# dev builds to speed up test execution
[profile.dev]
opt-level = 1
# Use release-level optimization for dependencies
# This slows down "first" builds on development environments,
-# but won't impact subsequent builds.
+# but won't impact subsequent builds.
[profile.dev.package."*"]
opt-level = 3
diff --git a/clarity/Cargo.toml b/clarity/Cargo.toml
index 37b3f0ed6f..8b714fd86b 100644
--- a/clarity/Cargo.toml
+++ b/clarity/Cargo.toml
@@ -18,38 +18,46 @@ name = "clarity"
path = "./src/libclarity.rs"
[dependencies]
-rand = { workspace = true }
-rand_chacha = { workspace = true }
-serde = "1"
-serde_derive = "1"
-serde_stacker = "0.1"
-regex = "1"
-lazy_static = "1.4.0"
-integer-sqrt = "0.1.3"
-slog = { version = "2.5.2", features = [ "max_level_trace" ] }
-stacks_common = { package = "stacks-common", path = "../stacks-common", default-features = false }
-rstest = "0.17.0"
-rstest_reuse = "0.5.0"
hashbrown = { workspace = true }
-rusqlite = { workspace = true, optional = true }
+lazy_static = { workspace = true }
+regex = { version = "1", default-features = false }
+serde = { workspace = true }
+serde_derive = { workspace = true }
+serde_json = { workspace = true }
+slog = { workspace = true }
+stacks_common = { package = "stacks-common", path = "../stacks-common", default-features = false }
-[dependencies.serde_json]
-version = "1.0"
-features = ["arbitrary_precision", "unbounded_depth"]
+# Optional dependencies
+rand = { workspace = true, optional = true }
+serde_stacker = { version = "0.1", default-features = false, optional = true }
+integer-sqrt = { version = "0.1.3", default-features = false, optional = true }
+rusqlite = { workspace = true, optional = true }
+rstest = { version = "0.17.0", default-features = false, optional = true }
+rstest_reuse = { version = "0.5.0", default-features = false, optional = true }
[dev-dependencies]
assert-json-diff = "1.0.0"
mutants = "0.0.3"
-# a nightly rustc regression (35dbef235 2021-03-02) prevents criterion from compiling
-# but it isn't necessary for tests: only benchmarks. therefore, commenting out for now.
-# criterion = "0.3"
[features]
-default = ["rusqlite"]
-developer-mode = ["stacks_common/developer-mode"]
+# The default feature set provides the full Clarity virtual machine with its SQLite-based
+# database backend.
+# To use `clarity` as a lightweight serialization/deserialization library,
+# depend on it with `default-features = false`.
+default = ["vm", "rusqlite"]
+# Enables the complete Clarity Virtual Machine. This includes the parser, analyzer,
+# cost-checking system, and execution engine. It transitively enables all necessary
+# dependencies for running smart contracts. This feature is required for any on-chain
+# contract execution or local contract testing.
+vm = ["dep:rand", "dep:serde_stacker", "dep:integer-sqrt"]
+developer-mode = ["vm", "stacks_common/developer-mode"]
+devtools = ["vm"]
+testing = ["vm", "dep:rstest", "dep:rstest_reuse", "rusqlite"]
+rusqlite = ["vm", "stacks_common/rusqlite", "dep:rusqlite"]
slog_json = ["stacks_common/slog_json"]
-rusqlite = ["stacks_common/rusqlite", "dep:rusqlite"]
-testing = []
-devtools = []
-rollback_value_check = []
-disable-costs = []
+rollback_value_check = ["vm"]
+disable-costs = ["vm"]
+
+# Wasm-specific features for easier configuration
+wasm-web = ["stacks_common/wasm-web"]
+wasm-deterministic = ["stacks_common/wasm-deterministic"]
diff --git a/clarity/src/libclarity.rs b/clarity/src/libclarity.rs
index 7ce2a4f903..4b3bbf269c 100644
--- a/clarity/src/libclarity.rs
+++ b/clarity/src/libclarity.rs
@@ -20,10 +20,6 @@
#![allow(non_upper_case_globals)]
#![cfg_attr(test, allow(unused_variables, unused_assignments))]
-#[allow(unused_imports)]
-#[macro_use(o, slog_log, slog_trace, slog_debug, slog_info, slog_warn, slog_error)]
-extern crate slog;
-
#[macro_use]
extern crate serde_derive;
@@ -49,6 +45,7 @@ pub use stacks_common::{
/// The Clarity virtual machine
pub mod vm;
+#[cfg(feature = "vm")]
pub mod boot_util {
use stacks_common::types::chainstate::StacksAddress;
diff --git a/clarity/src/vm/analysis/arithmetic_checker/tests.rs b/clarity/src/vm/analysis/arithmetic_checker/tests.rs
index 0e7d520cb3..6453402f9b 100644
--- a/clarity/src/vm/analysis/arithmetic_checker/tests.rs
+++ b/clarity/src/vm/analysis/arithmetic_checker/tests.rs
@@ -215,13 +215,13 @@ fn test_functions_clarity1() {
Err(FunctionNotPermitted(NativeFunctions::SetVar))),
("(define-private (foo (a principal)) (ft-get-balance tokaroos a))",
Err(FunctionNotPermitted(NativeFunctions::GetTokenBalance))),
- ("(define-private (foo (a principal))
+ ("(define-private (foo (a principal))
(ft-transfer? stackaroo u50 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR 'SPAXYA5XS51713FDTQ8H94EJ4V579CXMTRNBZKSF))",
Err(FunctionNotPermitted(NativeFunctions::TransferToken))),
- ("(define-private (foo (a principal))
+ ("(define-private (foo (a principal))
(ft-mint? stackaroo u100 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR))",
Err(FunctionNotPermitted(NativeFunctions::MintToken))),
- ("(define-private (foo (a principal))
+ ("(define-private (foo (a principal))
(nft-mint? stackaroo \"Roo\" 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR))",
Err(FunctionNotPermitted(NativeFunctions::MintAsset))),
("(nft-transfer? stackaroo \"Roo\" 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR 'SPAXYA5XS51713FDTQ8H94EJ4V579CXMTRNBZKSF)",
@@ -293,7 +293,7 @@ fn test_functions_clarity1() {
Ok(())),
("(buff-to-uint-be 0x0001)",
Ok(())),
- ("(is-standard 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6)",
+ ("(is-standard 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6)",
Ok(())),
("(principal-destruct? 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6)",
Ok(())),
@@ -358,7 +358,7 @@ fn test_functions_clarity2() {
Err(FunctionNotPermitted(NativeFunctions::IsStandard))),
("(principal-destruct? 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6)",
Err(FunctionNotPermitted(NativeFunctions::PrincipalDestruct))),
- ("(principal-construct? 0x22 0xfa6bf38ed557fe417333710d6033e9419391a320)",
+ ("(principal-construct? 0x22 0xfa6bf38ed557fe417333710d6033e9419391a320)",
Err(FunctionNotPermitted(NativeFunctions::PrincipalConstruct))),
("(string-to-int? \"-1\")",
Err(FunctionNotPermitted(NativeFunctions::StringToInt))),
diff --git a/clarity/src/vm/analysis/engine.rs b/clarity/src/vm/analysis/engine.rs
new file mode 100644
index 0000000000..4355c8ac9e
--- /dev/null
+++ b/clarity/src/vm/analysis/engine.rs
@@ -0,0 +1,172 @@
+// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation
+// Copyright (C) 2020 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 stacks_common::types::StacksEpochId;
+
+pub use crate::vm::analysis::analysis_db::AnalysisDatabase;
+use crate::vm::analysis::arithmetic_checker::ArithmeticOnlyChecker;
+use crate::vm::analysis::contract_interface_builder::build_contract_interface;
+pub use crate::vm::analysis::errors::{CheckError, CheckErrors, CheckResult};
+use crate::vm::analysis::read_only_checker::ReadOnlyChecker;
+use crate::vm::analysis::trait_checker::TraitChecker;
+use crate::vm::analysis::type_checker::v2_05::TypeChecker as TypeChecker2_05;
+use crate::vm::analysis::type_checker::v2_1::TypeChecker as TypeChecker2_1;
+pub use crate::vm::analysis::types::{AnalysisPass, ContractAnalysis};
+#[cfg(feature = "rusqlite")]
+use crate::vm::ast::{build_ast_with_rules, ASTRules};
+use crate::vm::costs::LimitedCostTracker;
+#[cfg(feature = "rusqlite")]
+use crate::vm::database::MemoryBackingStore;
+use crate::vm::database::STORE_CONTRACT_SRC_INTERFACE;
+use crate::vm::representations::SymbolicExpression;
+use crate::vm::types::QualifiedContractIdentifier;
+#[cfg(feature = "rusqlite")]
+use crate::vm::types::TypeSignature;
+use crate::vm::ClarityVersion;
+
+/// Used by CLI tools like the docs generator. Not used in production
+#[cfg(feature = "rusqlite")]
+pub fn mem_type_check(
+ snippet: &str,
+ version: ClarityVersion,
+ epoch: StacksEpochId,
+) -> CheckResult<(Option, ContractAnalysis)> {
+ let contract_identifier: QualifiedContractIdentifier = QualifiedContractIdentifier::transient();
+ let contract: Vec = build_ast_with_rules(
+ &contract_identifier,
+ snippet,
+ &mut (),
+ version,
+ epoch,
+ ASTRules::PrecheckSize,
+ )
+ .map_err(|_| CheckErrors::Expects("Failed to build AST".into()))?
+ .expressions;
+
+ let mut marf: MemoryBackingStore = MemoryBackingStore::new();
+ let mut analysis_db = marf.as_analysis_db();
+ let cost_tracker = LimitedCostTracker::new_free();
+ match run_analysis(
+ &QualifiedContractIdentifier::transient(),
+ &contract,
+ &mut analysis_db,
+ false,
+ cost_tracker,
+ epoch,
+ version,
+ true,
+ ) {
+ Ok(x) => {
+ // return the first type result of the type checker
+ let first_type = x
+ .type_map
+ .as_ref()
+ .ok_or_else(|| CheckErrors::Expects("Should be non-empty".into()))?
+ .get_type_expected(
+ x.expressions
+ .last()
+ .ok_or_else(|| CheckErrors::Expects("Should be non-empty".into()))?,
+ )
+ .cloned();
+ Ok((first_type, x))
+ }
+ Err((e, _)) => Err(e),
+ }
+}
+
+// Legacy function
+// The analysis is not just checking type.
+#[cfg(test)]
+pub fn type_check(
+ contract_identifier: &QualifiedContractIdentifier,
+ expressions: &mut [SymbolicExpression],
+ analysis_db: &mut AnalysisDatabase,
+ insert_contract: bool,
+ epoch: &StacksEpochId,
+ version: &ClarityVersion,
+) -> CheckResult {
+ run_analysis(
+ contract_identifier,
+ expressions,
+ analysis_db,
+ insert_contract,
+ // for the type check tests, the cost tracker's epoch doesn't
+ // matter: the costs in those tests are all free anyways.
+ LimitedCostTracker::new_free(),
+ *epoch,
+ *version,
+ true,
+ )
+ .map_err(|(e, _cost_tracker)| e)
+}
+
+#[allow(clippy::too_many_arguments)]
+pub fn run_analysis(
+ contract_identifier: &QualifiedContractIdentifier,
+ expressions: &[SymbolicExpression],
+ analysis_db: &mut AnalysisDatabase,
+ save_contract: bool,
+ cost_tracker: LimitedCostTracker,
+ epoch: StacksEpochId,
+ version: ClarityVersion,
+ build_type_map: bool,
+) -> Result {
+ let mut contract_analysis = ContractAnalysis::new(
+ contract_identifier.clone(),
+ expressions.to_vec(),
+ cost_tracker,
+ epoch,
+ version,
+ );
+ let result = analysis_db.execute(|db| {
+ ReadOnlyChecker::run_pass(&epoch, &mut contract_analysis, db)?;
+ match epoch {
+ StacksEpochId::Epoch20 | StacksEpochId::Epoch2_05 => {
+ TypeChecker2_05::run_pass(&epoch, &mut contract_analysis, db, build_type_map)
+ }
+ StacksEpochId::Epoch21
+ | StacksEpochId::Epoch22
+ | StacksEpochId::Epoch23
+ | StacksEpochId::Epoch24
+ | StacksEpochId::Epoch25
+ | StacksEpochId::Epoch30
+ | StacksEpochId::Epoch31 => {
+ TypeChecker2_1::run_pass(&epoch, &mut contract_analysis, db, build_type_map)
+ }
+ StacksEpochId::Epoch10 => {
+ return Err(CheckErrors::Expects(
+ "Epoch 1.0 is not a valid epoch for analysis".into(),
+ )
+ .into())
+ }
+ }?;
+ TraitChecker::run_pass(&epoch, &mut contract_analysis, db)?;
+ ArithmeticOnlyChecker::check_contract_cost_eligible(&mut contract_analysis);
+
+ if STORE_CONTRACT_SRC_INTERFACE {
+ let interface = build_contract_interface(&contract_analysis)?;
+ contract_analysis.contract_interface = Some(interface);
+ }
+ if save_contract {
+ db.insert_contract(contract_identifier, &contract_analysis)?;
+ }
+ Ok(())
+ });
+ match result {
+ Ok(_) => Ok(contract_analysis),
+ Err(e) => Err((e, contract_analysis.take_contract_cost_tracker())),
+ }
+}
diff --git a/clarity/src/vm/analysis/mod.rs b/clarity/src/vm/analysis/mod.rs
index 19183f5f67..450f927e3c 100644
--- a/clarity/src/vm/analysis/mod.rs
+++ b/clarity/src/vm/analysis/mod.rs
@@ -14,171 +14,27 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
+pub mod errors;
+
+#[cfg(feature = "vm")]
pub mod analysis_db;
+#[cfg(feature = "vm")]
pub mod arithmetic_checker;
+#[cfg(feature = "vm")]
pub mod contract_interface_builder;
-pub mod errors;
+#[cfg(feature = "vm")]
pub mod read_only_checker;
+#[cfg(feature = "vm")]
pub mod trait_checker;
+#[cfg(feature = "vm")]
pub mod type_checker;
+#[cfg(feature = "vm")]
pub mod types;
-use stacks_common::types::StacksEpochId;
-
-pub use self::analysis_db::AnalysisDatabase;
-use self::arithmetic_checker::ArithmeticOnlyChecker;
-use self::contract_interface_builder::build_contract_interface;
-pub use self::errors::{CheckError, CheckErrors, CheckResult};
-use self::read_only_checker::ReadOnlyChecker;
-use self::trait_checker::TraitChecker;
-use self::type_checker::v2_05::TypeChecker as TypeChecker2_05;
-use self::type_checker::v2_1::TypeChecker as TypeChecker2_1;
-pub use self::types::{AnalysisPass, ContractAnalysis};
-#[cfg(feature = "rusqlite")]
-use crate::vm::ast::{build_ast_with_rules, ASTRules};
-use crate::vm::costs::LimitedCostTracker;
-#[cfg(feature = "rusqlite")]
-use crate::vm::database::MemoryBackingStore;
-use crate::vm::database::STORE_CONTRACT_SRC_INTERFACE;
-use crate::vm::representations::SymbolicExpression;
-use crate::vm::types::QualifiedContractIdentifier;
-#[cfg(feature = "rusqlite")]
-use crate::vm::types::TypeSignature;
-use crate::vm::ClarityVersion;
-
-/// Used by CLI tools like the docs generator. Not used in production
-#[cfg(feature = "rusqlite")]
-pub fn mem_type_check(
- snippet: &str,
- version: ClarityVersion,
- epoch: StacksEpochId,
-) -> CheckResult<(Option, ContractAnalysis)> {
- let contract_identifier = QualifiedContractIdentifier::transient();
- let contract = build_ast_with_rules(
- &contract_identifier,
- snippet,
- &mut (),
- version,
- epoch,
- ASTRules::PrecheckSize,
- )
- .map_err(|_| CheckErrors::Expects("Failed to build AST".into()))?
- .expressions;
-
- let mut marf = MemoryBackingStore::new();
- let mut analysis_db = marf.as_analysis_db();
- let cost_tracker = LimitedCostTracker::new_free();
- match run_analysis(
- &QualifiedContractIdentifier::transient(),
- &contract,
- &mut analysis_db,
- false,
- cost_tracker,
- epoch,
- version,
- true,
- ) {
- Ok(x) => {
- // return the first type result of the type checker
- let first_type = x
- .type_map
- .as_ref()
- .ok_or_else(|| CheckErrors::Expects("Should be non-empty".into()))?
- .get_type_expected(
- x.expressions
- .last()
- .ok_or_else(|| CheckErrors::Expects("Should be non-empty".into()))?,
- )
- .cloned();
- Ok((first_type, x))
- }
- Err((e, _)) => Err(e),
- }
-}
-
-// Legacy function
-// The analysis is not just checking type.
-#[cfg(test)]
-pub fn type_check(
- contract_identifier: &QualifiedContractIdentifier,
- expressions: &mut [SymbolicExpression],
- analysis_db: &mut AnalysisDatabase,
- insert_contract: bool,
- epoch: &StacksEpochId,
- version: &ClarityVersion,
-) -> CheckResult {
- run_analysis(
- contract_identifier,
- expressions,
- analysis_db,
- insert_contract,
- // for the type check tests, the cost tracker's epoch doesn't
- // matter: the costs in those tests are all free anyways.
- LimitedCostTracker::new_free(),
- *epoch,
- *version,
- true,
- )
- .map_err(|(e, _cost_tracker)| e)
-}
-
-#[allow(clippy::too_many_arguments)]
-pub fn run_analysis(
- contract_identifier: &QualifiedContractIdentifier,
- expressions: &[SymbolicExpression],
- analysis_db: &mut AnalysisDatabase,
- save_contract: bool,
- cost_tracker: LimitedCostTracker,
- epoch: StacksEpochId,
- version: ClarityVersion,
- build_type_map: bool,
-) -> Result {
- let mut contract_analysis = ContractAnalysis::new(
- contract_identifier.clone(),
- expressions.to_vec(),
- cost_tracker,
- epoch,
- version,
- );
- let result = analysis_db.execute(|db| {
- ReadOnlyChecker::run_pass(&epoch, &mut contract_analysis, db)?;
- match epoch {
- StacksEpochId::Epoch20 | StacksEpochId::Epoch2_05 => {
- TypeChecker2_05::run_pass(&epoch, &mut contract_analysis, db, build_type_map)
- }
- StacksEpochId::Epoch21
- | StacksEpochId::Epoch22
- | StacksEpochId::Epoch23
- | StacksEpochId::Epoch24
- | StacksEpochId::Epoch25
- | StacksEpochId::Epoch30
- | StacksEpochId::Epoch31 => {
- TypeChecker2_1::run_pass(&epoch, &mut contract_analysis, db, build_type_map)
- }
- StacksEpochId::Epoch10 => {
- return Err(CheckErrors::Expects(
- "Epoch 1.0 is not a valid epoch for analysis".into(),
- )
- .into())
- }
- }?;
- TraitChecker::run_pass(&epoch, &mut contract_analysis, db)?;
- ArithmeticOnlyChecker::check_contract_cost_eligible(&mut contract_analysis);
-
- if STORE_CONTRACT_SRC_INTERFACE {
- let interface = build_contract_interface(&contract_analysis)?;
- contract_analysis.contract_interface = Some(interface);
- }
- if save_contract {
- db.insert_contract(contract_identifier, &contract_analysis)?;
- }
- Ok(())
- });
- match result {
- Ok(_) => Ok(contract_analysis),
- Err(e) => Err((e, contract_analysis.take_contract_cost_tracker())),
- }
-}
+#[cfg(feature = "vm")]
+pub mod engine;
+#[cfg(feature = "vm")]
+pub use engine::*;
#[cfg(test)]
mod tests;
diff --git a/clarity/src/vm/analysis/type_checker/v2_1/tests/contracts.rs b/clarity/src/vm/analysis/type_checker/v2_1/tests/contracts.rs
index 838be9e6bb..6e6f57df8f 100644
--- a/clarity/src/vm/analysis/type_checker/v2_1/tests/contracts.rs
+++ b/clarity/src/vm/analysis/type_checker/v2_1/tests/contracts.rs
@@ -111,8 +111,8 @@ const SIMPLE_TOKENS: &str = "(define-map tokens { account: principal } { balance
const SIMPLE_NAMES: &str = "(define-constant burn-address 'SP000000000000000000002Q6VF78)
(define-private (price-function (name uint))
(if (< name u100000) u1000 u100))
-
- (define-map name-map
+
+ (define-map name-map
{ name: uint } { owner: principal })
(define-map preorder-map
{ name-hash: (buff 20) }
@@ -121,7 +121,7 @@ const SIMPLE_NAMES: &str = "(define-constant burn-address 'SP0000000000000000000
(define-private (check-balance)
(contract-call? .tokens my-get-token-balance tx-sender))
- (define-public (preorder
+ (define-public (preorder
(name-hash (buff 20))
(name-price uint))
(let ((xfer-result (contract-call? .tokens token-transfer
@@ -145,13 +145,13 @@ const SIMPLE_NAMES: &str = "(define-constant burn-address 'SP0000000000000000000
;; preorder entry must exist!
(unwrap! (map-get? preorder-map
(tuple (name-hash (hash160 (xor name salt))))) (err 2)))
- (name-entry
+ (name-entry
(map-get? name-map (tuple (name name)))))
(if (and
;; name shouldn't *already* exist
(is-none name-entry)
;; preorder must have paid enough
- (<= (price-function name)
+ (<= (price-function name)
(get paid preorder-entry))
;; preorder must have been the current principal
(is-eq tx-sender
@@ -280,7 +280,7 @@ fn test_names_tokens_contracts_interface() {
{ "name": "tn1", "type": "bool" },
{ "name": "tn2", "type": "int128" },
{ "name": "tn3", "type": { "buffer": { "length": 1 } }}
- ] } }
+ ] } }
},
{ "name": "f11",
"access": "private",
@@ -413,7 +413,7 @@ fn test_names_tokens_contracts_interface() {
"name": "n2",
"type": "bool"
}
- ]
+ ]
}
}]
}
@@ -1478,10 +1478,10 @@ fn test_trait_to_subtrait_and_back() {
))
(define-private (foo-0 (impl-contract ))
(foo-1 impl-contract))
-
+
(define-private (foo-1 (impl-contract ))
(foo-2 impl-contract))
-
+
(define-private (foo-2 (impl-contract ))
true)";
diff --git a/clarity/src/vm/ast/ast_builder.rs b/clarity/src/vm/ast/ast_builder.rs
new file mode 100644
index 0000000000..dc56482382
--- /dev/null
+++ b/clarity/src/vm/ast/ast_builder.rs
@@ -0,0 +1,616 @@
+// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation
+// Copyright (C) 2020 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 stacks_common::types::StacksEpochId;
+
+use crate::vm::ast::definition_sorter::DefinitionSorter;
+use crate::vm::ast::errors::ParseResult;
+use crate::vm::ast::expression_identifier::ExpressionIdentifier;
+use crate::vm::ast::parser::v1::{
+ parse as parse_v1, parse_no_stack_limit as parse_v1_no_stack_limit,
+};
+use crate::vm::ast::parser::v2::parse as parse_v2;
+use crate::vm::ast::stack_depth_checker::{StackDepthChecker, VaryStackDepthChecker};
+use crate::vm::ast::sugar_expander::SugarExpander;
+use crate::vm::ast::traits_resolver::TraitsResolver;
+use crate::vm::ast::types::BuildASTPass;
+pub use crate::vm::ast::types::ContractAST;
+use crate::vm::costs::cost_functions::ClarityCostFunction;
+use crate::vm::costs::{runtime_cost, CostTracker};
+use crate::vm::diagnostic::{Diagnostic, Level};
+use crate::vm::representations::PreSymbolicExpression;
+use crate::vm::types::QualifiedContractIdentifier;
+use crate::vm::ClarityVersion;
+
+/// Legacy function
+#[cfg(any(test, feature = "testing"))]
+pub fn parse(
+ contract_identifier: &QualifiedContractIdentifier,
+ source_code: &str,
+ version: ClarityVersion,
+ epoch: StacksEpochId,
+) -> Result, crate::vm::errors::Error> {
+ let ast = build_ast(contract_identifier, source_code, &mut (), version, epoch)?;
+ Ok(ast.expressions)
+}
+
+// AST parser rulesets to apply.
+define_u8_enum!(ASTRules {
+ Typical = 0,
+ PrecheckSize = 1
+});
+
+/// Parse a program based on which epoch is active
+fn parse_in_epoch(
+ source_code: &str,
+ epoch_id: StacksEpochId,
+ ast_rules: ASTRules,
+) -> ParseResult> {
+ if epoch_id >= StacksEpochId::Epoch21 {
+ parse_v2(source_code)
+ } else if ast_rules == ASTRules::Typical {
+ parse_v1_no_stack_limit(source_code)
+ } else {
+ parse_v1(source_code)
+ }
+}
+
+/// This is the part of the AST parser that runs without respect to cost analysis, specifically
+/// pertaining to verifying that the AST is reasonably-sized.
+/// Used mainly to filter transactions that might be too costly, as an optimization heuristic.
+pub fn ast_check_size(
+ contract_identifier: &QualifiedContractIdentifier,
+ source_code: &str,
+ clarity_version: ClarityVersion,
+ epoch_id: StacksEpochId,
+) -> ParseResult {
+ let pre_expressions = parse_in_epoch(source_code, epoch_id, ASTRules::PrecheckSize)?;
+ let mut contract_ast = ContractAST::new(contract_identifier.clone(), pre_expressions);
+ StackDepthChecker::run_pass(&mut contract_ast, clarity_version)?;
+ VaryStackDepthChecker::run_pass(&mut contract_ast, clarity_version)?;
+ Ok(contract_ast)
+}
+
+/// Build an AST according to a ruleset
+pub fn build_ast_with_rules(
+ contract_identifier: &QualifiedContractIdentifier,
+ source_code: &str,
+ cost_track: &mut T,
+ clarity_version: ClarityVersion,
+ epoch: StacksEpochId,
+ ruleset: ASTRules,
+) -> ParseResult {
+ match ruleset {
+ // After epoch 2.1, prechecking the size is required
+ ASTRules::Typical if epoch < StacksEpochId::Epoch21 => build_ast_typical(
+ contract_identifier,
+ source_code,
+ cost_track,
+ clarity_version,
+ epoch,
+ ),
+ _ => build_ast_precheck_size(
+ contract_identifier,
+ source_code,
+ cost_track,
+ clarity_version,
+ epoch,
+ ),
+ }
+}
+
+/// Build an AST with the typical rules
+fn build_ast_typical(
+ contract_identifier: &QualifiedContractIdentifier,
+ source_code: &str,
+ cost_track: &mut T,
+ clarity_version: ClarityVersion,
+ epoch: StacksEpochId,
+) -> ParseResult {
+ let (contract, _, _) = inner_build_ast(
+ contract_identifier,
+ source_code,
+ cost_track,
+ clarity_version,
+ epoch,
+ ASTRules::Typical,
+ true,
+ )?;
+ Ok(contract)
+}
+
+/// Used by developer tools only. Continues on through errors by inserting
+/// placeholders into the AST. Collects as many diagnostics as possible.
+/// Always returns a ContractAST, a vector of diagnostics, and a boolean
+/// that indicates if the build was successful.
+#[allow(clippy::unwrap_used)]
+pub fn build_ast_with_diagnostics(
+ contract_identifier: &QualifiedContractIdentifier,
+ source_code: &str,
+ cost_track: &mut T,
+ clarity_version: ClarityVersion,
+ epoch: StacksEpochId,
+) -> (ContractAST, Vec, bool) {
+ inner_build_ast(
+ contract_identifier,
+ source_code,
+ cost_track,
+ clarity_version,
+ epoch,
+ ASTRules::PrecheckSize,
+ false,
+ )
+ .unwrap()
+}
+
+fn inner_build_ast(
+ contract_identifier: &QualifiedContractIdentifier,
+ source_code: &str,
+ cost_track: &mut T,
+ clarity_version: ClarityVersion,
+ epoch: StacksEpochId,
+ ast_rules: ASTRules,
+ error_early: bool,
+) -> ParseResult<(ContractAST, Vec, bool)> {
+ let cost_err = match runtime_cost(
+ ClarityCostFunction::AstParse,
+ cost_track,
+ source_code.len() as u64,
+ ) {
+ Err(e) if error_early => return Err(e.into()),
+ Err(e) => Some(e),
+ _ => None,
+ };
+
+ let (pre_expressions, mut diagnostics, mut success) = if epoch >= StacksEpochId::Epoch21 {
+ if error_early {
+ let exprs = crate::vm::ast::parser::v2::parse(source_code)?;
+ (exprs, Vec::new(), true)
+ } else {
+ crate::vm::ast::parser::v2::parse_collect_diagnostics(source_code)
+ }
+ } else {
+ let parse_result = match ast_rules {
+ ASTRules::Typical => parse_v1_no_stack_limit(source_code),
+ ASTRules::PrecheckSize => parse_v1(source_code),
+ };
+ match parse_result {
+ Ok(pre_expressions) => (pre_expressions, vec![], true),
+ Err(error) if error_early => return Err(error),
+ Err(error) => (vec![], vec![error.diagnostic], false),
+ }
+ };
+
+ if let Some(e) = cost_err {
+ diagnostics.insert(
+ 0,
+ Diagnostic {
+ level: Level::Error,
+ message: format!("runtime_cost error: {:?}", e),
+ spans: vec![],
+ suggestion: None,
+ },
+ );
+ }
+
+ let mut contract_ast = ContractAST::new(contract_identifier.clone(), pre_expressions);
+ match StackDepthChecker::run_pass(&mut contract_ast, clarity_version) {
+ Err(e) if error_early => return Err(e),
+ Err(e) => {
+ diagnostics.push(e.diagnostic);
+ success = false;
+ }
+ _ => (),
+ }
+
+ if ast_rules != ASTRules::Typical {
+ // run extra stack-depth pass for tuples
+ match VaryStackDepthChecker::run_pass(&mut contract_ast, clarity_version) {
+ Err(e) if error_early => return Err(e),
+ Err(e) => {
+ diagnostics.push(e.diagnostic);
+ success = false;
+ }
+ _ => (),
+ }
+ }
+
+ match ExpressionIdentifier::run_pre_expression_pass(&mut contract_ast, clarity_version) {
+ Err(e) if error_early => return Err(e),
+ Err(e) => {
+ diagnostics.push(e.diagnostic);
+ success = false;
+ }
+ _ => (),
+ }
+ match DefinitionSorter::run_pass(&mut contract_ast, cost_track, clarity_version) {
+ Err(e) if error_early => return Err(e),
+ Err(e) => {
+ diagnostics.push(e.diagnostic);
+ success = false;
+ }
+ _ => (),
+ }
+ match TraitsResolver::run_pass(&mut contract_ast, clarity_version) {
+ Err(e) if error_early => return Err(e),
+ Err(e) => {
+ diagnostics.push(e.diagnostic);
+ success = false;
+ }
+ _ => (),
+ }
+ match SugarExpander::run_pass(&mut contract_ast, clarity_version) {
+ Err(e) if error_early => return Err(e),
+ Err(e) => {
+ diagnostics.push(e.diagnostic);
+ success = false;
+ }
+ _ => (),
+ }
+ match ExpressionIdentifier::run_expression_pass(&mut contract_ast, clarity_version) {
+ Err(e) if error_early => return Err(e),
+ Err(e) => {
+ diagnostics.push(e.diagnostic);
+ success = false;
+ }
+ _ => (),
+ }
+ Ok((contract_ast, diagnostics, success))
+}
+
+/// Built an AST, but pre-check the size of the AST before doing more work
+fn build_ast_precheck_size(
+ contract_identifier: &QualifiedContractIdentifier,
+ source_code: &str,
+ cost_track: &mut T,
+ clarity_version: ClarityVersion,
+ epoch: StacksEpochId,
+) -> ParseResult {
+ let (contract, _, _) = inner_build_ast(
+ contract_identifier,
+ source_code,
+ cost_track,
+ clarity_version,
+ epoch,
+ ASTRules::PrecheckSize,
+ true,
+ )?;
+ Ok(contract)
+}
+
+/// Test compatibility
+#[cfg(any(test, feature = "testing"))]
+pub fn build_ast(
+ contract_identifier: &QualifiedContractIdentifier,
+ source_code: &str,
+ cost_track: &mut T,
+ clarity_version: ClarityVersion,
+ epoch_id: StacksEpochId,
+) -> ParseResult {
+ build_ast_typical(
+ contract_identifier,
+ source_code,
+ cost_track,
+ clarity_version,
+ epoch_id,
+ )
+}
+
+#[cfg(test)]
+mod test {
+ use hashbrown::HashMap;
+ use stacks_common::types::StacksEpochId;
+
+ use crate::vm::ast::errors::ParseErrors;
+ use crate::vm::ast::stack_depth_checker::AST_CALL_STACK_DEPTH_BUFFER;
+ use crate::vm::ast::{build_ast, build_ast_with_rules, ASTRules};
+ use crate::vm::costs::{LimitedCostTracker, *};
+ use crate::vm::representations::depth_traverse;
+ use crate::vm::types::QualifiedContractIdentifier;
+ use crate::vm::{ClarityCostFunction, ClarityName, ClarityVersion, MAX_CALL_STACK_DEPTH};
+
+ #[derive(PartialEq, Debug)]
+ struct UnitTestTracker {
+ invoked_functions: Vec<(ClarityCostFunction, Vec)>,
+ invocation_count: u64,
+ cost_addition_count: u64,
+ }
+ impl UnitTestTracker {
+ pub fn new() -> Self {
+ UnitTestTracker {
+ invoked_functions: vec![],
+ invocation_count: 0,
+ cost_addition_count: 0,
+ }
+ }
+ }
+ impl CostTracker for UnitTestTracker {
+ fn compute_cost(
+ &mut self,
+ cost_f: ClarityCostFunction,
+ input: &[u64],
+ ) -> std::result::Result {
+ self.invoked_functions.push((cost_f, input.to_vec()));
+ self.invocation_count += 1;
+ Ok(ExecutionCost::ZERO)
+ }
+ fn add_cost(&mut self, _cost: ExecutionCost) -> std::result::Result<(), CostErrors> {
+ self.cost_addition_count += 1;
+ Ok(())
+ }
+ fn add_memory(&mut self, _memory: u64) -> std::result::Result<(), CostErrors> {
+ Ok(())
+ }
+ fn drop_memory(&mut self, _memory: u64) -> std::result::Result<(), CostErrors> {
+ Ok(())
+ }
+ fn reset_memory(&mut self) {}
+ fn short_circuit_contract_call(
+ &mut self,
+ _contract: &QualifiedContractIdentifier,
+ _function: &ClarityName,
+ _input: &[u64],
+ ) -> Result {
+ Ok(false)
+ }
+ }
+
+ #[test]
+ fn test_cost_tracking_deep_contracts_2_05() {
+ let clarity_version = ClarityVersion::Clarity1;
+ let stack_limit =
+ (AST_CALL_STACK_DEPTH_BUFFER + (MAX_CALL_STACK_DEPTH as u64) + 1) as usize;
+ let exceeds_stack_depth_tuple = format!(
+ "{}u1 {}",
+ "{ a : ".repeat(stack_limit + 1),
+ "} ".repeat(stack_limit + 1)
+ );
+
+ // for deep lists, a test like this works:
+ // it can assert a limit, that you can also verify
+ // by disabling `VaryStackDepthChecker` and arbitrarily bumping up the parser lexer limits
+ // and see that it produces the same result
+ let exceeds_stack_depth_list = format!(
+ "{}u1 {}",
+ "(list ".repeat(stack_limit + 1),
+ ")".repeat(stack_limit + 1)
+ );
+
+ // with old rules, this is just ExpressionStackDepthTooDeep
+ let mut cost_track = UnitTestTracker::new();
+ let err = build_ast_with_rules(
+ &QualifiedContractIdentifier::transient(),
+ &exceeds_stack_depth_list,
+ &mut cost_track,
+ clarity_version,
+ StacksEpochId::Epoch2_05,
+ ASTRules::Typical,
+ )
+ .expect_err("Contract should error in parsing");
+
+ let expected_err = ParseErrors::ExpressionStackDepthTooDeep;
+ let expected_list_cost_state = UnitTestTracker {
+ invoked_functions: vec![(ClarityCostFunction::AstParse, vec![500])],
+ invocation_count: 1,
+ cost_addition_count: 1,
+ };
+
+ assert_eq!(&expected_err, &err.err);
+ assert_eq!(expected_list_cost_state, cost_track);
+
+ // with new rules, this is now VaryExpressionStackDepthTooDeep
+ let mut cost_track = UnitTestTracker::new();
+ let err = build_ast_with_rules(
+ &QualifiedContractIdentifier::transient(),
+ &exceeds_stack_depth_list,
+ &mut cost_track,
+ clarity_version,
+ StacksEpochId::Epoch2_05,
+ ASTRules::PrecheckSize,
+ )
+ .expect_err("Contract should error in parsing");
+
+ let expected_err = ParseErrors::VaryExpressionStackDepthTooDeep;
+ let expected_list_cost_state = UnitTestTracker {
+ invoked_functions: vec![(ClarityCostFunction::AstParse, vec![500])],
+ invocation_count: 1,
+ cost_addition_count: 1,
+ };
+
+ assert_eq!(&expected_err, &err.err);
+ assert_eq!(expected_list_cost_state, cost_track);
+
+ // you cannot do the same for tuples!
+ // in ASTRules::Typical, this passes
+ let mut cost_track = UnitTestTracker::new();
+ let _ = build_ast_with_rules(
+ &QualifiedContractIdentifier::transient(),
+ &exceeds_stack_depth_tuple,
+ &mut cost_track,
+ clarity_version,
+ StacksEpochId::Epoch2_05,
+ ASTRules::Typical,
+ )
+ .expect("Contract should parse with ASTRules::Typical");
+
+ // this actually won't even error without
+ // the VaryStackDepthChecker changes.
+ let mut cost_track = UnitTestTracker::new();
+ let err = build_ast_with_rules(
+ &QualifiedContractIdentifier::transient(),
+ &exceeds_stack_depth_tuple,
+ &mut cost_track,
+ clarity_version,
+ StacksEpochId::Epoch2_05,
+ ASTRules::PrecheckSize,
+ )
+ .expect_err("Contract should error in parsing with ASTRules::PrecheckSize");
+
+ let expected_err = ParseErrors::VaryExpressionStackDepthTooDeep;
+ let expected_list_cost_state = UnitTestTracker {
+ invoked_functions: vec![(ClarityCostFunction::AstParse, vec![571])],
+ invocation_count: 1,
+ cost_addition_count: 1,
+ };
+
+ assert_eq!(&expected_err, &err.err);
+ assert_eq!(expected_list_cost_state, cost_track);
+ }
+
+ #[test]
+ fn test_cost_tracking_deep_contracts_2_1() {
+ for clarity_version in &[ClarityVersion::Clarity1, ClarityVersion::Clarity2] {
+ let stack_limit =
+ (AST_CALL_STACK_DEPTH_BUFFER + (MAX_CALL_STACK_DEPTH as u64) + 1) as usize;
+ let exceeds_stack_depth_tuple = format!(
+ "{}u1 {}",
+ "{ a : ".repeat(stack_limit + 1),
+ "} ".repeat(stack_limit + 1)
+ );
+
+ // for deep lists, a test like this works:
+ // it can assert a limit, that you can also verify
+ // by disabling `VaryStackDepthChecker` and arbitrarily bumping up the parser lexer limits
+ // and see that it produces the same result
+ let exceeds_stack_depth_list = format!(
+ "{}u1 {}",
+ "(list ".repeat(stack_limit + 1),
+ ")".repeat(stack_limit + 1)
+ );
+
+ // with old rules, this is just ExpressionStackDepthTooDeep
+ let mut cost_track = UnitTestTracker::new();
+ let err = build_ast_with_rules(
+ &QualifiedContractIdentifier::transient(),
+ &exceeds_stack_depth_list,
+ &mut cost_track,
+ *clarity_version,
+ StacksEpochId::Epoch21,
+ ASTRules::Typical,
+ )
+ .expect_err("Contract should error in parsing");
+
+ let expected_err = ParseErrors::ExpressionStackDepthTooDeep;
+ let expected_list_cost_state = UnitTestTracker {
+ invoked_functions: vec![(ClarityCostFunction::AstParse, vec![500])],
+ invocation_count: 1,
+ cost_addition_count: 1,
+ };
+
+ assert_eq!(&expected_err, &err.err);
+ assert_eq!(expected_list_cost_state, cost_track);
+
+ // in 2.1, this is still ExpressionStackDepthTooDeep
+ let mut cost_track = UnitTestTracker::new();
+ let err = build_ast_with_rules(
+ &QualifiedContractIdentifier::transient(),
+ &exceeds_stack_depth_list,
+ &mut cost_track,
+ *clarity_version,
+ StacksEpochId::Epoch21,
+ ASTRules::PrecheckSize,
+ )
+ .expect_err("Contract should error in parsing");
+
+ let expected_err = ParseErrors::ExpressionStackDepthTooDeep;
+ let expected_list_cost_state = UnitTestTracker {
+ invoked_functions: vec![(ClarityCostFunction::AstParse, vec![500])],
+ invocation_count: 1,
+ cost_addition_count: 1,
+ };
+
+ assert_eq!(&expected_err, &err.err);
+ assert_eq!(expected_list_cost_state, cost_track);
+
+ // in 2.1, ASTRules::Typical is ignored -- this still fails to parse
+ let mut cost_track = UnitTestTracker::new();
+ let _ = build_ast_with_rules(
+ &QualifiedContractIdentifier::transient(),
+ &exceeds_stack_depth_tuple,
+ &mut cost_track,
+ *clarity_version,
+ StacksEpochId::Epoch21,
+ ASTRules::Typical,
+ )
+ .expect_err("Contract should error in parsing");
+
+ let expected_err = ParseErrors::ExpressionStackDepthTooDeep;
+ let expected_list_cost_state = UnitTestTracker {
+ invoked_functions: vec![(ClarityCostFunction::AstParse, vec![571])],
+ invocation_count: 1,
+ cost_addition_count: 1,
+ };
+
+ assert_eq!(&expected_err, &err.err);
+ assert_eq!(expected_list_cost_state, cost_track);
+
+ // in 2.1, ASTRules::PrecheckSize is still ignored -- this still fails to parse
+ let mut cost_track = UnitTestTracker::new();
+ let err = build_ast_with_rules(
+ &QualifiedContractIdentifier::transient(),
+ &exceeds_stack_depth_tuple,
+ &mut cost_track,
+ *clarity_version,
+ StacksEpochId::Epoch21,
+ ASTRules::PrecheckSize,
+ )
+ .expect_err("Contract should error in parsing");
+
+ let expected_err = ParseErrors::ExpressionStackDepthTooDeep;
+ let expected_list_cost_state = UnitTestTracker {
+ invoked_functions: vec![(ClarityCostFunction::AstParse, vec![571])],
+ invocation_count: 1,
+ cost_addition_count: 1,
+ };
+
+ assert_eq!(&expected_err, &err.err);
+ assert_eq!(expected_list_cost_state, cost_track);
+ }
+ }
+
+ #[test]
+ fn test_expression_identification_tuples() {
+ for version in &[ClarityVersion::Clarity1, ClarityVersion::Clarity2] {
+ for epoch in &[StacksEpochId::Epoch2_05, StacksEpochId::Epoch21] {
+ let progn = "{ a: (+ 1 2 3),
+ b: 1,
+ c: 3 }";
+
+ let mut cost_track = LimitedCostTracker::new_free();
+ let ast = build_ast(
+ &QualifiedContractIdentifier::transient(),
+ progn,
+ &mut cost_track,
+ *version,
+ *epoch,
+ )
+ .unwrap()
+ .expressions;
+
+ let mut visited = HashMap::new();
+
+ for expr in ast.iter() {
+ depth_traverse::<_, _, ()>(expr, |x| {
+ assert!(!visited.contains_key(&x.id));
+ visited.insert(x.id, true);
+ Ok(())
+ })
+ .unwrap();
+ }
+ }
+ }
+ }
+}
diff --git a/clarity/src/vm/ast/mod.rs b/clarity/src/vm/ast/mod.rs
index 263fc86526..c02c709bd0 100644
--- a/clarity/src/vm/ast/mod.rs
+++ b/clarity/src/vm/ast/mod.rs
@@ -14,610 +14,22 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
+#[cfg(feature = "vm")]
pub mod definition_sorter;
+pub mod errors;
+#[cfg(feature = "vm")]
pub mod expression_identifier;
pub mod parser;
-pub mod traits_resolver;
-
-pub mod errors;
+#[cfg(feature = "vm")]
pub mod stack_depth_checker;
+#[cfg(feature = "vm")]
pub mod sugar_expander;
+#[cfg(feature = "vm")]
+pub mod traits_resolver;
+#[cfg(feature = "vm")]
pub mod types;
-use stacks_common::types::StacksEpochId;
-
-use self::definition_sorter::DefinitionSorter;
-use self::errors::ParseResult;
-use self::expression_identifier::ExpressionIdentifier;
-use self::parser::v1::{parse as parse_v1, parse_no_stack_limit as parse_v1_no_stack_limit};
-use self::parser::v2::parse as parse_v2;
-use self::stack_depth_checker::{StackDepthChecker, VaryStackDepthChecker};
-use self::sugar_expander::SugarExpander;
-use self::traits_resolver::TraitsResolver;
-use self::types::BuildASTPass;
-pub use self::types::ContractAST;
-use crate::vm::costs::cost_functions::ClarityCostFunction;
-use crate::vm::costs::{runtime_cost, CostTracker};
-use crate::vm::diagnostic::{Diagnostic, Level};
-use crate::vm::representations::PreSymbolicExpression;
-use crate::vm::types::QualifiedContractIdentifier;
-use crate::vm::ClarityVersion;
-
-/// Legacy function
-#[cfg(any(test, feature = "testing"))]
-pub fn parse(
- contract_identifier: &QualifiedContractIdentifier,
- source_code: &str,
- version: ClarityVersion,
- epoch: StacksEpochId,
-) -> Result, crate::vm::errors::Error> {
- let ast = build_ast(contract_identifier, source_code, &mut (), version, epoch)?;
- Ok(ast.expressions)
-}
-
-// AST parser rulesets to apply.
-define_u8_enum!(ASTRules {
- Typical = 0,
- PrecheckSize = 1
-});
-
-/// Parse a program based on which epoch is active
-fn parse_in_epoch(
- source_code: &str,
- epoch_id: StacksEpochId,
- ast_rules: ASTRules,
-) -> ParseResult> {
- if epoch_id >= StacksEpochId::Epoch21 {
- parse_v2(source_code)
- } else if ast_rules == ASTRules::Typical {
- parse_v1_no_stack_limit(source_code)
- } else {
- parse_v1(source_code)
- }
-}
-
-/// This is the part of the AST parser that runs without respect to cost analysis, specifically
-/// pertaining to verifying that the AST is reasonably-sized.
-/// Used mainly to filter transactions that might be too costly, as an optimization heuristic.
-pub fn ast_check_size(
- contract_identifier: &QualifiedContractIdentifier,
- source_code: &str,
- clarity_version: ClarityVersion,
- epoch_id: StacksEpochId,
-) -> ParseResult {
- let pre_expressions = parse_in_epoch(source_code, epoch_id, ASTRules::PrecheckSize)?;
- let mut contract_ast = ContractAST::new(contract_identifier.clone(), pre_expressions);
- StackDepthChecker::run_pass(&mut contract_ast, clarity_version)?;
- VaryStackDepthChecker::run_pass(&mut contract_ast, clarity_version)?;
- Ok(contract_ast)
-}
-
-/// Build an AST according to a ruleset
-pub fn build_ast_with_rules(
- contract_identifier: &QualifiedContractIdentifier,
- source_code: &str,
- cost_track: &mut T,
- clarity_version: ClarityVersion,
- epoch: StacksEpochId,
- ruleset: ASTRules,
-) -> ParseResult {
- match ruleset {
- // After epoch 2.1, prechecking the size is required
- ASTRules::Typical if epoch < StacksEpochId::Epoch21 => build_ast_typical(
- contract_identifier,
- source_code,
- cost_track,
- clarity_version,
- epoch,
- ),
- _ => build_ast_precheck_size(
- contract_identifier,
- source_code,
- cost_track,
- clarity_version,
- epoch,
- ),
- }
-}
-
-/// Build an AST with the typical rules
-fn build_ast_typical(
- contract_identifier: &QualifiedContractIdentifier,
- source_code: &str,
- cost_track: &mut T,
- clarity_version: ClarityVersion,
- epoch: StacksEpochId,
-) -> ParseResult {
- let (contract, _, _) = inner_build_ast(
- contract_identifier,
- source_code,
- cost_track,
- clarity_version,
- epoch,
- ASTRules::Typical,
- true,
- )?;
- Ok(contract)
-}
-
-/// Used by developer tools only. Continues on through errors by inserting
-/// placeholders into the AST. Collects as many diagnostics as possible.
-/// Always returns a ContractAST, a vector of diagnostics, and a boolean
-/// that indicates if the build was successful.
-#[allow(clippy::unwrap_used)]
-pub fn build_ast_with_diagnostics(
- contract_identifier: &QualifiedContractIdentifier,
- source_code: &str,
- cost_track: &mut T,
- clarity_version: ClarityVersion,
- epoch: StacksEpochId,
-) -> (ContractAST, Vec, bool) {
- inner_build_ast(
- contract_identifier,
- source_code,
- cost_track,
- clarity_version,
- epoch,
- ASTRules::PrecheckSize,
- false,
- )
- .unwrap()
-}
-
-fn inner_build_ast(
- contract_identifier: &QualifiedContractIdentifier,
- source_code: &str,
- cost_track: &mut T,
- clarity_version: ClarityVersion,
- epoch: StacksEpochId,
- ast_rules: ASTRules,
- error_early: bool,
-) -> ParseResult<(ContractAST, Vec, bool)> {
- let cost_err = match runtime_cost(
- ClarityCostFunction::AstParse,
- cost_track,
- source_code.len() as u64,
- ) {
- Err(e) if error_early => return Err(e.into()),
- Err(e) => Some(e),
- _ => None,
- };
-
- let (pre_expressions, mut diagnostics, mut success) = if epoch >= StacksEpochId::Epoch21 {
- if error_early {
- let exprs = parser::v2::parse(source_code)?;
- (exprs, Vec::new(), true)
- } else {
- parser::v2::parse_collect_diagnostics(source_code)
- }
- } else {
- let parse_result = match ast_rules {
- ASTRules::Typical => parse_v1_no_stack_limit(source_code),
- ASTRules::PrecheckSize => parse_v1(source_code),
- };
- match parse_result {
- Ok(pre_expressions) => (pre_expressions, vec![], true),
- Err(error) if error_early => return Err(error),
- Err(error) => (vec![], vec![error.diagnostic], false),
- }
- };
-
- if let Some(e) = cost_err {
- diagnostics.insert(
- 0,
- Diagnostic {
- level: Level::Error,
- message: format!("runtime_cost error: {:?}", e),
- spans: vec![],
- suggestion: None,
- },
- );
- }
-
- let mut contract_ast = ContractAST::new(contract_identifier.clone(), pre_expressions);
- match StackDepthChecker::run_pass(&mut contract_ast, clarity_version) {
- Err(e) if error_early => return Err(e),
- Err(e) => {
- diagnostics.push(e.diagnostic);
- success = false;
- }
- _ => (),
- }
-
- if ast_rules != ASTRules::Typical {
- // run extra stack-depth pass for tuples
- match VaryStackDepthChecker::run_pass(&mut contract_ast, clarity_version) {
- Err(e) if error_early => return Err(e),
- Err(e) => {
- diagnostics.push(e.diagnostic);
- success = false;
- }
- _ => (),
- }
- }
-
- match ExpressionIdentifier::run_pre_expression_pass(&mut contract_ast, clarity_version) {
- Err(e) if error_early => return Err(e),
- Err(e) => {
- diagnostics.push(e.diagnostic);
- success = false;
- }
- _ => (),
- }
- match DefinitionSorter::run_pass(&mut contract_ast, cost_track, clarity_version) {
- Err(e) if error_early => return Err(e),
- Err(e) => {
- diagnostics.push(e.diagnostic);
- success = false;
- }
- _ => (),
- }
- match TraitsResolver::run_pass(&mut contract_ast, clarity_version) {
- Err(e) if error_early => return Err(e),
- Err(e) => {
- diagnostics.push(e.diagnostic);
- success = false;
- }
- _ => (),
- }
- match SugarExpander::run_pass(&mut contract_ast, clarity_version) {
- Err(e) if error_early => return Err(e),
- Err(e) => {
- diagnostics.push(e.diagnostic);
- success = false;
- }
- _ => (),
- }
- match ExpressionIdentifier::run_expression_pass(&mut contract_ast, clarity_version) {
- Err(e) if error_early => return Err(e),
- Err(e) => {
- diagnostics.push(e.diagnostic);
- success = false;
- }
- _ => (),
- }
- Ok((contract_ast, diagnostics, success))
-}
-
-/// Built an AST, but pre-check the size of the AST before doing more work
-fn build_ast_precheck_size(
- contract_identifier: &QualifiedContractIdentifier,
- source_code: &str,
- cost_track: &mut T,
- clarity_version: ClarityVersion,
- epoch: StacksEpochId,
-) -> ParseResult {
- let (contract, _, _) = inner_build_ast(
- contract_identifier,
- source_code,
- cost_track,
- clarity_version,
- epoch,
- ASTRules::PrecheckSize,
- true,
- )?;
- Ok(contract)
-}
-
-/// Test compatibility
-#[cfg(any(test, feature = "testing"))]
-pub fn build_ast(
- contract_identifier: &QualifiedContractIdentifier,
- source_code: &str,
- cost_track: &mut T,
- clarity_version: ClarityVersion,
- epoch_id: StacksEpochId,
-) -> ParseResult {
- build_ast_typical(
- contract_identifier,
- source_code,
- cost_track,
- clarity_version,
- epoch_id,
- )
-}
-
-#[cfg(test)]
-mod test {
- use hashbrown::HashMap;
- use stacks_common::types::StacksEpochId;
-
- use crate::vm::ast::errors::ParseErrors;
- use crate::vm::ast::stack_depth_checker::AST_CALL_STACK_DEPTH_BUFFER;
- use crate::vm::ast::{build_ast, build_ast_with_rules, ASTRules};
- use crate::vm::costs::{LimitedCostTracker, *};
- use crate::vm::representations::depth_traverse;
- use crate::vm::types::QualifiedContractIdentifier;
- use crate::vm::{ClarityCostFunction, ClarityName, ClarityVersion, MAX_CALL_STACK_DEPTH};
-
- #[derive(PartialEq, Debug)]
- struct UnitTestTracker {
- invoked_functions: Vec<(ClarityCostFunction, Vec)>,
- invocation_count: u64,
- cost_addition_count: u64,
- }
- impl UnitTestTracker {
- pub fn new() -> Self {
- UnitTestTracker {
- invoked_functions: vec![],
- invocation_count: 0,
- cost_addition_count: 0,
- }
- }
- }
- impl CostTracker for UnitTestTracker {
- fn compute_cost(
- &mut self,
- cost_f: ClarityCostFunction,
- input: &[u64],
- ) -> std::result::Result {
- self.invoked_functions.push((cost_f, input.to_vec()));
- self.invocation_count += 1;
- Ok(ExecutionCost::ZERO)
- }
- fn add_cost(&mut self, _cost: ExecutionCost) -> std::result::Result<(), CostErrors> {
- self.cost_addition_count += 1;
- Ok(())
- }
- fn add_memory(&mut self, _memory: u64) -> std::result::Result<(), CostErrors> {
- Ok(())
- }
- fn drop_memory(&mut self, _memory: u64) -> std::result::Result<(), CostErrors> {
- Ok(())
- }
- fn reset_memory(&mut self) {}
- fn short_circuit_contract_call(
- &mut self,
- _contract: &QualifiedContractIdentifier,
- _function: &ClarityName,
- _input: &[u64],
- ) -> Result {
- Ok(false)
- }
- }
-
- #[test]
- fn test_cost_tracking_deep_contracts_2_05() {
- let clarity_version = ClarityVersion::Clarity1;
- let stack_limit =
- (AST_CALL_STACK_DEPTH_BUFFER + (MAX_CALL_STACK_DEPTH as u64) + 1) as usize;
- let exceeds_stack_depth_tuple = format!(
- "{}u1 {}",
- "{ a : ".repeat(stack_limit + 1),
- "} ".repeat(stack_limit + 1)
- );
-
- // for deep lists, a test like this works:
- // it can assert a limit, that you can also verify
- // by disabling `VaryStackDepthChecker` and arbitrarily bumping up the parser lexer limits
- // and see that it produces the same result
- let exceeds_stack_depth_list = format!(
- "{}u1 {}",
- "(list ".repeat(stack_limit + 1),
- ")".repeat(stack_limit + 1)
- );
-
- // with old rules, this is just ExpressionStackDepthTooDeep
- let mut cost_track = UnitTestTracker::new();
- let err = build_ast_with_rules(
- &QualifiedContractIdentifier::transient(),
- &exceeds_stack_depth_list,
- &mut cost_track,
- clarity_version,
- StacksEpochId::Epoch2_05,
- ASTRules::Typical,
- )
- .expect_err("Contract should error in parsing");
-
- let expected_err = ParseErrors::ExpressionStackDepthTooDeep;
- let expected_list_cost_state = UnitTestTracker {
- invoked_functions: vec![(ClarityCostFunction::AstParse, vec![500])],
- invocation_count: 1,
- cost_addition_count: 1,
- };
-
- assert_eq!(&expected_err, &err.err);
- assert_eq!(expected_list_cost_state, cost_track);
-
- // with new rules, this is now VaryExpressionStackDepthTooDeep
- let mut cost_track = UnitTestTracker::new();
- let err = build_ast_with_rules(
- &QualifiedContractIdentifier::transient(),
- &exceeds_stack_depth_list,
- &mut cost_track,
- clarity_version,
- StacksEpochId::Epoch2_05,
- ASTRules::PrecheckSize,
- )
- .expect_err("Contract should error in parsing");
-
- let expected_err = ParseErrors::VaryExpressionStackDepthTooDeep;
- let expected_list_cost_state = UnitTestTracker {
- invoked_functions: vec![(ClarityCostFunction::AstParse, vec![500])],
- invocation_count: 1,
- cost_addition_count: 1,
- };
-
- assert_eq!(&expected_err, &err.err);
- assert_eq!(expected_list_cost_state, cost_track);
-
- // you cannot do the same for tuples!
- // in ASTRules::Typical, this passes
- let mut cost_track = UnitTestTracker::new();
- let _ = build_ast_with_rules(
- &QualifiedContractIdentifier::transient(),
- &exceeds_stack_depth_tuple,
- &mut cost_track,
- clarity_version,
- StacksEpochId::Epoch2_05,
- ASTRules::Typical,
- )
- .expect("Contract should parse with ASTRules::Typical");
-
- // this actually won't even error without
- // the VaryStackDepthChecker changes.
- let mut cost_track = UnitTestTracker::new();
- let err = build_ast_with_rules(
- &QualifiedContractIdentifier::transient(),
- &exceeds_stack_depth_tuple,
- &mut cost_track,
- clarity_version,
- StacksEpochId::Epoch2_05,
- ASTRules::PrecheckSize,
- )
- .expect_err("Contract should error in parsing with ASTRules::PrecheckSize");
-
- let expected_err = ParseErrors::VaryExpressionStackDepthTooDeep;
- let expected_list_cost_state = UnitTestTracker {
- invoked_functions: vec![(ClarityCostFunction::AstParse, vec![571])],
- invocation_count: 1,
- cost_addition_count: 1,
- };
-
- assert_eq!(&expected_err, &err.err);
- assert_eq!(expected_list_cost_state, cost_track);
- }
-
- #[test]
- fn test_cost_tracking_deep_contracts_2_1() {
- for clarity_version in &[ClarityVersion::Clarity1, ClarityVersion::Clarity2] {
- let stack_limit =
- (AST_CALL_STACK_DEPTH_BUFFER + (MAX_CALL_STACK_DEPTH as u64) + 1) as usize;
- let exceeds_stack_depth_tuple = format!(
- "{}u1 {}",
- "{ a : ".repeat(stack_limit + 1),
- "} ".repeat(stack_limit + 1)
- );
-
- // for deep lists, a test like this works:
- // it can assert a limit, that you can also verify
- // by disabling `VaryStackDepthChecker` and arbitrarily bumping up the parser lexer limits
- // and see that it produces the same result
- let exceeds_stack_depth_list = format!(
- "{}u1 {}",
- "(list ".repeat(stack_limit + 1),
- ")".repeat(stack_limit + 1)
- );
-
- // with old rules, this is just ExpressionStackDepthTooDeep
- let mut cost_track = UnitTestTracker::new();
- let err = build_ast_with_rules(
- &QualifiedContractIdentifier::transient(),
- &exceeds_stack_depth_list,
- &mut cost_track,
- *clarity_version,
- StacksEpochId::Epoch21,
- ASTRules::Typical,
- )
- .expect_err("Contract should error in parsing");
-
- let expected_err = ParseErrors::ExpressionStackDepthTooDeep;
- let expected_list_cost_state = UnitTestTracker {
- invoked_functions: vec![(ClarityCostFunction::AstParse, vec![500])],
- invocation_count: 1,
- cost_addition_count: 1,
- };
-
- assert_eq!(&expected_err, &err.err);
- assert_eq!(expected_list_cost_state, cost_track);
-
- // in 2.1, this is still ExpressionStackDepthTooDeep
- let mut cost_track = UnitTestTracker::new();
- let err = build_ast_with_rules(
- &QualifiedContractIdentifier::transient(),
- &exceeds_stack_depth_list,
- &mut cost_track,
- *clarity_version,
- StacksEpochId::Epoch21,
- ASTRules::PrecheckSize,
- )
- .expect_err("Contract should error in parsing");
-
- let expected_err = ParseErrors::ExpressionStackDepthTooDeep;
- let expected_list_cost_state = UnitTestTracker {
- invoked_functions: vec![(ClarityCostFunction::AstParse, vec![500])],
- invocation_count: 1,
- cost_addition_count: 1,
- };
-
- assert_eq!(&expected_err, &err.err);
- assert_eq!(expected_list_cost_state, cost_track);
-
- // in 2.1, ASTRules::Typical is ignored -- this still fails to parse
- let mut cost_track = UnitTestTracker::new();
- let _ = build_ast_with_rules(
- &QualifiedContractIdentifier::transient(),
- &exceeds_stack_depth_tuple,
- &mut cost_track,
- *clarity_version,
- StacksEpochId::Epoch21,
- ASTRules::Typical,
- )
- .expect_err("Contract should error in parsing");
-
- let expected_err = ParseErrors::ExpressionStackDepthTooDeep;
- let expected_list_cost_state = UnitTestTracker {
- invoked_functions: vec![(ClarityCostFunction::AstParse, vec![571])],
- invocation_count: 1,
- cost_addition_count: 1,
- };
-
- assert_eq!(&expected_err, &err.err);
- assert_eq!(expected_list_cost_state, cost_track);
-
- // in 2.1, ASTRules::PrecheckSize is still ignored -- this still fails to parse
- let mut cost_track = UnitTestTracker::new();
- let err = build_ast_with_rules(
- &QualifiedContractIdentifier::transient(),
- &exceeds_stack_depth_tuple,
- &mut cost_track,
- *clarity_version,
- StacksEpochId::Epoch21,
- ASTRules::PrecheckSize,
- )
- .expect_err("Contract should error in parsing");
-
- let expected_err = ParseErrors::ExpressionStackDepthTooDeep;
- let expected_list_cost_state = UnitTestTracker {
- invoked_functions: vec![(ClarityCostFunction::AstParse, vec![571])],
- invocation_count: 1,
- cost_addition_count: 1,
- };
-
- assert_eq!(&expected_err, &err.err);
- assert_eq!(expected_list_cost_state, cost_track);
- }
- }
-
- #[test]
- fn test_expression_identification_tuples() {
- for version in &[ClarityVersion::Clarity1, ClarityVersion::Clarity2] {
- for epoch in &[StacksEpochId::Epoch2_05, StacksEpochId::Epoch21] {
- let progn = "{ a: (+ 1 2 3),
- b: 1,
- c: 3 }";
-
- let mut cost_track = LimitedCostTracker::new_free();
- let ast = build_ast(
- &QualifiedContractIdentifier::transient(),
- progn,
- &mut cost_track,
- *version,
- *epoch,
- )
- .unwrap()
- .expressions;
-
- let mut visited = HashMap::new();
- for expr in ast.iter() {
- depth_traverse::<_, _, ()>(expr, |x| {
- assert!(!visited.contains_key(&x.id));
- visited.insert(x.id, true);
- Ok(())
- })
- .unwrap();
- }
- }
- }
- }
-}
+#[cfg(feature = "vm")]
+pub mod ast_builder;
+#[cfg(feature = "vm")]
+pub use ast_builder::*;
diff --git a/clarity/src/vm/ast/parser/mod.rs b/clarity/src/vm/ast/parser/mod.rs
index ced9f3aafc..0e54c950cc 100644
--- a/clarity/src/vm/ast/parser/mod.rs
+++ b/clarity/src/vm/ast/parser/mod.rs
@@ -14,5 +14,6 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
+#[cfg(feature = "vm")]
pub mod v1;
pub mod v2;
diff --git a/clarity/src/vm/ast/parser/v2/lexer/mod.rs b/clarity/src/vm/ast/parser/v2/lexer/mod.rs
index bbd6136916..6775bee523 100644
--- a/clarity/src/vm/ast/parser/v2/lexer/mod.rs
+++ b/clarity/src/vm/ast/parser/v2/lexer/mod.rs
@@ -1425,7 +1425,7 @@ mod tests {
+-*/ < <= >
>=.: ;; comment
"hello" u"world" 0x0123456789abcdeffedcba9876543210
-
+
foo-bar_
"#,
diff --git a/clarity/src/vm/ast/parser/v2/mod.rs b/clarity/src/vm/ast/parser/v2/mod.rs
index dd5a900364..046a151535 100644
--- a/clarity/src/vm/ast/parser/v2/mod.rs
+++ b/clarity/src/vm/ast/parser/v2/mod.rs
@@ -1,3613 +1,6 @@
pub mod lexer;
-use stacks_common::util::hash::hex_bytes;
-
-use self::lexer::token::{PlacedToken, Token};
-use self::lexer::Lexer;
-use crate::vm::ast::errors::{ParseError, ParseErrors, ParseResult, PlacedError};
-use crate::vm::ast::stack_depth_checker::AST_CALL_STACK_DEPTH_BUFFER;
-use crate::vm::diagnostic::{DiagnosableError, Diagnostic, Level};
-use crate::vm::representations::{ClarityName, ContractName, PreSymbolicExpression, Span};
-use crate::vm::types::{
- CharType, PrincipalData, QualifiedContractIdentifier, SequenceData, TraitIdentifier, UTF8Data,
- Value,
-};
-use crate::vm::MAX_CALL_STACK_DEPTH;
-
-pub struct Parser<'a> {
- lexer: Lexer<'a>,
- tokens: Vec,
- next_token: usize,
- diagnostics: Vec,
- success: bool,
- // `fail_fast` mode indicates that the parser should not report warnings
- // and should exit on the first error. This is useful for parsing in the
- // context of a stacks-node, while normal mode is useful for developers.
- fail_fast: bool,
- nesting_depth: u64,
-}
-
-pub const MAX_STRING_LEN: usize = 128;
-pub const MAX_CONTRACT_NAME_LEN: usize = 40;
-pub const MAX_NESTING_DEPTH: u64 = AST_CALL_STACK_DEPTH_BUFFER + (MAX_CALL_STACK_DEPTH as u64) + 1;
-
-enum OpenTupleStatus {
- /// The next thing to parse is a key
- ParseKey,
- /// The next thing to parse is a value
- ParseValue,
-}
-
-enum SetupTupleResult {
- OpenTuple(OpenTuple),
- Closed(PreSymbolicExpression),
-}
-
-struct OpenTuple {
- nodes: Vec,
- span: Span,
- /// Is the next node is expected to be a key or value? All of the preparatory work is done _before_ the parse loop tries to digest the next
- /// node (i.e., whitespace ingestion and checking for commas)
- expects: OpenTupleStatus,
- /// This is the last peeked token before trying to parse a key or value node, used for
- /// diagnostic reporting
- diagnostic_token: PlacedToken,
-}
-
-enum ParserStackElement {
- OpenList {
- nodes: Vec,
- span: Span,
- whitespace: bool,
- },
- OpenTuple(OpenTuple),
-}
-
-impl<'a> Parser<'a> {
- pub fn new(input: &'a str, fail_fast: bool) -> Result {
- let lexer = match Lexer::new(input, fail_fast) {
- Ok(lexer) => lexer,
- Err(e) => return Err(ParseErrors::Lexer(e)),
- };
- let mut p = Self {
- lexer,
- tokens: vec![],
- next_token: 0,
- diagnostics: vec![],
- success: true,
- fail_fast,
- nesting_depth: 0,
- };
-
- loop {
- let token = match p.lexer.read_token() {
- Ok(token) => token,
- Err(e) => {
- assert!(
- fail_fast,
- "Parser::read_token should not return an error when not in fail_fast mode"
- );
- p.success = false;
- return Err(ParseErrors::Lexer(e));
- }
- };
- if token.token == Token::Eof {
- p.tokens.push(token);
- break;
- }
- p.tokens.push(token);
- }
- p.diagnostics = p
- .lexer
- .diagnostics
- .iter()
- .map(|lex_error| PlacedError {
- e: ParseErrors::Lexer(lex_error.e.clone()),
- span: lex_error.span.clone(),
- })
- .collect();
- p.success = p.lexer.success;
- Ok(p)
- }
-
- fn add_diagnostic(&mut self, e: ParseErrors, span: Span) -> ParseResult<()> {
- if self.fail_fast {
- return Err(ParseError::new(e));
- } else {
- if e.level() == Level::Error {
- self.success = false;
- }
- self.diagnostics.push(PlacedError { e, span });
- }
- Ok(())
- }
-
- fn next_token(&mut self) -> Option {
- if self.next_token >= self.tokens.len() {
- return None;
- }
- let token = self.tokens[self.next_token].clone();
- self.next_token += 1;
- Some(token)
- }
-
- fn peek_next_token(&mut self) -> PlacedToken {
- if self.next_token >= self.tokens.len() {
- PlacedToken {
- span: Span {
- start_line: 1,
- start_column: 1,
- end_line: 1,
- end_column: 1,
- },
- token: Token::Eof,
- }
- } else {
- self.tokens[self.next_token].clone()
- }
- }
-
- /// Get a reference to the last processed token. If there is no last token,
- /// raises an UnexpectedParserFailure.
- fn peek_last_token(&self) -> ParseResult<&PlacedToken> {
- if self.next_token == 0 {
- return Err(ParseError::new(ParseErrors::UnexpectedParserFailure));
- }
- self.tokens
- .get(self.next_token - 1)
- .ok_or_else(|| ParseError::new(ParseErrors::UnexpectedParserFailure))
- }
-
- fn skip_to_end(&mut self) {
- self.next_token = self.tokens.len();
- }
-
- fn ignore_whitespace(&mut self) -> bool {
- let mut found = false;
- loop {
- if self.next_token >= self.tokens.len() {
- return found;
- }
- let token = &self.tokens[self.next_token];
- match &token.token {
- Token::Whitespace => {
- self.next_token += 1;
- found = true;
- }
- _ => return found,
- }
- }
- }
-
- fn ignore_whitespace_and_comments(&mut self) -> Vec {
- let mut comments = Vec::new();
- loop {
- if self.next_token >= self.tokens.len() {
- return comments;
- }
- let token = &self.tokens[self.next_token];
- match &token.token {
- Token::Whitespace => {
- self.next_token += 1;
- }
- Token::Comment(comment) => {
- let mut comment = PreSymbolicExpression::comment(comment.to_string());
- comment.copy_span(&token.span);
- comments.push(comment);
- self.next_token += 1;
- }
- _ => return comments,
- }
- }
- }
-
- // TODO: add tests from mutation testing results #4829
- #[cfg_attr(test, mutants::skip)]
- /// Process a new child node for an AST expression that is open and waiting for children nodes. For example,
- /// a list or tuple expression that is waiting for child expressions.
- ///
- /// Returns Some(node) if the open node is finished and should be popped from the stack.
- /// Returns None if the open node is not finished and should remain on the parser stack.
- fn handle_open_node(
- &mut self,
- open_node: &mut ParserStackElement,
- node_opt: Option,
- ) -> ParseResult