Skip to content

Commit 7dc8b45

Browse files
authored
Merge pull request #6615 from obycode/fix/to-ascii-typecheck
feat: make `to-ascii?` result type tailored to the input
2 parents bb20f48 + 867665b commit 7dc8b45

File tree

6 files changed

+289
-156
lines changed

6 files changed

+289
-156
lines changed

clarity-types/src/types/signatures.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -870,6 +870,18 @@ impl TypeSignature {
870870
/// Longest ([`MAX_TO_ASCII_RESULT_LEN`]) string allowed for `to-ascii?` call.
871871
pub const TO_ASCII_STRING_ASCII_MAX: TypeSignature =
872872
Self::type_ascii_const(MAX_TO_ASCII_RESULT_LEN);
873+
/// Longest string result possible for `(to-ascii? <int>)` result
874+
/// e.g. "-170141183460469231731687303715884105728"
875+
pub const TO_ASCII_INT_RESULT_MAX: TypeSignature = Self::type_ascii_const(40);
876+
/// Longest string result possible for `(to-ascii? <uint>)` result
877+
/// e.g. "u340282366920938463463374607431768211455"
878+
pub const TO_ASCII_UINT_RESULT_MAX: TypeSignature = Self::type_ascii_const(40);
879+
/// Longest string result possible for `(to-ascii? <bool>)` result
880+
/// e.g. "false"
881+
pub const TO_ASCII_BOOL_RESULT_MAX: TypeSignature = Self::type_ascii_const(5);
882+
/// Longest string result possible for `(to-ascii? <principal>)` result
883+
/// e.g. "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.contract-name-can-be-up-to-128-characters-long-so-41-characters-for-the-address-plus-1-for-the-dot-plus-128-for-the-name-is-170-"
884+
pub const TO_ASCII_PRINCIPAL_RESULT_MAX: TypeSignature = Self::type_ascii_const(170);
873885

874886
/// Longest ([`CONTRACT_MAX_NAME_LENGTH`]) string allowed for `contract-name`.
875887
pub const CONTRACT_NAME_STRING_ASCII_MAX: TypeSignature =
@@ -915,6 +927,14 @@ impl TypeSignature {
915927
Self::type_ascii_const(len)
916928
}
917929

930+
/// Creates a string ASCII type with the specified length.
931+
/// Returns an error if the provided length is invalid.
932+
pub fn new_ascii_type(len: i128) -> Result<Self, CheckErrors> {
933+
Ok(SequenceType(SequenceSubtype::StringType(
934+
StringSubtype::ASCII(BufferLength::try_from_i128(len)?),
935+
)))
936+
}
937+
918938
/// If one of the types is a NoType, return Ok(the other type), otherwise return least_supertype(a, b)
919939
pub fn factor_out_no_type(
920940
epoch: &StacksEpochId,

clarity/src/vm/analysis/type_checker/v2_1/natives/conversions.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use clarity_types::errors::CheckErrors;
2+
use clarity_types::types::{StringSubtype, MAX_TO_ASCII_BUFFER_LEN};
13
use stacks_common::types::StacksEpochId;
24

35
use super::TypeChecker;
@@ -41,3 +43,61 @@ pub fn check_special_from_consensus_buff(
4143
checker.type_check_expects(&args[1], context, &TypeSignature::BUFFER_MAX)?;
4244
TypeSignature::new_option(result_type).map_err(CheckError::from)
4345
}
46+
47+
/// `to-ascii?` admits exactly one argument, a value to convert to a
48+
/// `string-ascii`. It can be any of the following types:
49+
/// - `int`
50+
/// - `uint`
51+
/// - `bool`
52+
/// - `principal`
53+
/// - `(buff 524284)`
54+
/// - `(string-utf8 262144)`
55+
///
56+
/// It returns a `(response (string-ascii 1048571) uint)`.
57+
pub fn check_special_to_ascii(
58+
checker: &mut TypeChecker,
59+
args: &[SymbolicExpression],
60+
context: &TypingContext,
61+
) -> Result<TypeSignature, CheckError> {
62+
check_argument_count(1, args)?;
63+
let input_type = checker.type_check(
64+
args.first()
65+
.ok_or(CheckErrors::CheckerImplementationFailure)?,
66+
context,
67+
)?;
68+
69+
let result_type = match input_type {
70+
TypeSignature::IntType => TypeSignature::TO_ASCII_INT_RESULT_MAX,
71+
TypeSignature::UIntType => TypeSignature::TO_ASCII_UINT_RESULT_MAX,
72+
TypeSignature::BoolType => TypeSignature::TO_ASCII_BOOL_RESULT_MAX,
73+
TypeSignature::PrincipalType | TypeSignature::CallableType(_) => {
74+
TypeSignature::TO_ASCII_PRINCIPAL_RESULT_MAX
75+
}
76+
TypeSignature::SequenceType(SequenceSubtype::BufferType(len))
77+
if u32::from(len.clone()) <= MAX_TO_ASCII_BUFFER_LEN =>
78+
{
79+
// Each byte in the buffer becomes two ASCII characters, plus "0x" prefix
80+
TypeSignature::new_ascii_type((u32::from(len) * 2 + 2).into())?
81+
}
82+
TypeSignature::SequenceType(SequenceSubtype::StringType(StringSubtype::UTF8(len))) => {
83+
// Each UTF-8 character is exactly one ASCII character
84+
TypeSignature::new_ascii_type(u32::from(len).into())?
85+
}
86+
_ => {
87+
let types = vec![
88+
TypeSignature::IntType,
89+
TypeSignature::UIntType,
90+
TypeSignature::BoolType,
91+
TypeSignature::PrincipalType,
92+
TypeSignature::TO_ASCII_BUFFER_MAX,
93+
TypeSignature::STRING_UTF8_MAX,
94+
];
95+
return Err(CheckErrors::UnionTypeError(types, input_type.into()).into());
96+
}
97+
};
98+
Ok(
99+
TypeSignature::new_response(result_type, TypeSignature::UIntType).map_err(|_| {
100+
CheckErrors::Expects("FATAL: Legal Clarity response type marked invalid".into())
101+
})?,
102+
)
103+
}

clarity/src/vm/analysis/type_checker/v2_1/natives/mod.rs

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1194,23 +1194,7 @@ impl TypedNativeFunction {
11941194
)
11951195
.map_err(|_| CheckErrors::Expects("Bad constructor".into()))?,
11961196
}))),
1197-
ToAscii => Simple(SimpleNativeFunction(FunctionType::UnionArgs(
1198-
vec![
1199-
TypeSignature::IntType,
1200-
TypeSignature::UIntType,
1201-
TypeSignature::BoolType,
1202-
TypeSignature::PrincipalType,
1203-
TypeSignature::TO_ASCII_BUFFER_MAX,
1204-
TypeSignature::STRING_UTF8_MAX,
1205-
],
1206-
TypeSignature::new_response(
1207-
TypeSignature::TO_ASCII_STRING_ASCII_MAX,
1208-
TypeSignature::UIntType,
1209-
)
1210-
.map_err(|_| {
1211-
CheckErrors::Expects("FATAL: Legal Clarity response type marked invalid".into())
1212-
})?,
1213-
))),
1197+
ToAscii => Special(SpecialNativeFunction(&conversions::check_special_to_ascii)),
12141198
RestrictAssets => Special(SpecialNativeFunction(
12151199
&post_conditions::check_restrict_assets,
12161200
)),

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

Lines changed: 0 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -3503,142 +3503,3 @@ fn test_contract_hash(#[case] version: ClarityVersion, #[case] epoch: StacksEpoc
35033503
assert_eq!(&actual, expected, "Failed for test case: {description}");
35043504
}
35053505
}
3506-
3507-
/// Pass various types to `to-ascii?`
3508-
#[apply(test_clarity_versions)]
3509-
fn test_to_ascii(#[case] version: ClarityVersion, #[case] epoch: StacksEpochId) {
3510-
let to_ascii_response_type = Some(
3511-
TypeSignature::new_response(
3512-
TypeSignature::TO_ASCII_STRING_ASCII_MAX,
3513-
TypeSignature::UIntType,
3514-
)
3515-
.unwrap(),
3516-
);
3517-
let to_ascii_expected_types = vec![
3518-
TypeSignature::IntType,
3519-
TypeSignature::UIntType,
3520-
TypeSignature::BoolType,
3521-
TypeSignature::PrincipalType,
3522-
TypeSignature::TO_ASCII_BUFFER_MAX,
3523-
TypeSignature::STRING_UTF8_MAX,
3524-
];
3525-
let test_cases = [
3526-
(
3527-
"(to-ascii? 123)",
3528-
"int type",
3529-
Ok(to_ascii_response_type.clone()),
3530-
),
3531-
(
3532-
"(to-ascii? u123)",
3533-
"uint type",
3534-
Ok(to_ascii_response_type.clone()),
3535-
),
3536-
(
3537-
"(to-ascii? true)",
3538-
"bool type",
3539-
Ok(to_ascii_response_type.clone()),
3540-
),
3541-
(
3542-
"(to-ascii? 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM)",
3543-
"standard principal",
3544-
Ok(to_ascii_response_type.clone()),
3545-
),
3546-
(
3547-
"(to-ascii? 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.foo)",
3548-
"contract principal",
3549-
Ok(to_ascii_response_type.clone()),
3550-
),
3551-
(
3552-
"(to-ascii? 0x1234)",
3553-
"buffer type",
3554-
Ok(to_ascii_response_type.clone()),
3555-
),
3556-
(
3557-
&format!("(to-ascii? 0x{})", "ff".repeat(524284)),
3558-
"max len buffer type",
3559-
Ok(to_ascii_response_type.clone()),
3560-
),
3561-
(
3562-
&format!("(to-ascii? 0x{})", "ff".repeat(524285)),
3563-
"oversized buffer type",
3564-
Err(CheckErrors::UnionTypeError(
3565-
to_ascii_expected_types.clone(),
3566-
Box::new(TypeSignature::SequenceType(SequenceSubtype::BufferType(
3567-
BufferLength::try_from(524285u32).unwrap(),
3568-
))),
3569-
)),
3570-
),
3571-
(
3572-
"(to-ascii? u\"I am serious, and don't call me Shirley.\")",
3573-
"utf8 string",
3574-
Ok(to_ascii_response_type),
3575-
),
3576-
(
3577-
"(to-ascii? \"60 percent of the time, it works every time\")",
3578-
"ascii string",
3579-
Err(CheckErrors::UnionTypeError(
3580-
to_ascii_expected_types.clone(),
3581-
Box::new(TypeSignature::SequenceType(SequenceSubtype::StringType(
3582-
StringSubtype::ASCII(BufferLength::try_from(43u32).unwrap()),
3583-
))),
3584-
)),
3585-
),
3586-
(
3587-
"(to-ascii? (list 1 2 3))",
3588-
"list type",
3589-
Err(CheckErrors::UnionTypeError(
3590-
to_ascii_expected_types.clone(),
3591-
Box::new(TypeSignature::SequenceType(SequenceSubtype::ListType(
3592-
ListTypeData::new_list(TypeSignature::IntType, 3).unwrap(),
3593-
))),
3594-
)),
3595-
),
3596-
(
3597-
"(to-ascii? { a: 1, b: u2 })",
3598-
"tuple type",
3599-
Err(CheckErrors::UnionTypeError(
3600-
to_ascii_expected_types.clone(),
3601-
Box::new(TypeSignature::TupleType(
3602-
vec![
3603-
(ClarityName::from("a"), TypeSignature::IntType),
3604-
(ClarityName::from("b"), TypeSignature::UIntType),
3605-
]
3606-
.try_into()
3607-
.unwrap(),
3608-
)),
3609-
)),
3610-
),
3611-
(
3612-
"(to-ascii? (some u789))",
3613-
"optional type",
3614-
Err(CheckErrors::UnionTypeError(
3615-
to_ascii_expected_types.clone(),
3616-
Box::new(TypeSignature::new_option(TypeSignature::UIntType).unwrap()),
3617-
)),
3618-
),
3619-
(
3620-
"(to-ascii? (ok true))",
3621-
"response type",
3622-
Err(CheckErrors::UnionTypeError(
3623-
to_ascii_expected_types.clone(),
3624-
Box::new(
3625-
TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::NoType)
3626-
.unwrap(),
3627-
),
3628-
)),
3629-
),
3630-
];
3631-
3632-
for (source, description, clarity4_expected) in test_cases.iter() {
3633-
let result = mem_run_analysis(source, version, epoch);
3634-
let actual = result.map(|(type_sig, _)| type_sig).map_err(|e| *e.err);
3635-
3636-
let expected = if version >= ClarityVersion::Clarity4 {
3637-
clarity4_expected
3638-
} else {
3639-
&Err(CheckErrors::UnknownFunction("to-ascii?".to_string()))
3640-
};
3641-
3642-
assert_eq!(&actual, expected, "Failed for test case: {description}");
3643-
}
3644-
}

0 commit comments

Comments
 (0)