Skip to content

Commit 4e8722b

Browse files
committed
--wip-- [skip ci]
1 parent d877257 commit 4e8722b

File tree

5 files changed

+199
-8
lines changed

5 files changed

+199
-8
lines changed

components/ts-to-clar/src/converter.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,15 @@ fn convert_expression_with_type(
140140

141141
_ => return Err(anyhow::anyhow!("Unsupported expression for Tuple")),
142142
},
143-
_ => return Err(anyhow::anyhow!("Unsupported type for variable")),
143+
ClarityTypeSignature::ResponseType(boxed_types) => match &expr {
144+
_ => return Err(anyhow::anyhow!("Invalid expression for Response type")),
145+
},
146+
_ => {
147+
return Err(anyhow::anyhow!(format!(
148+
"Unsupported type for variable with {:?} type",
149+
r#type
150+
)))
151+
}
144152
})
145153
}
146154

components/ts-to-clar/src/expression_converter.rs

Lines changed: 146 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,12 @@ impl<'a> StatementConverter<'a> {
8888
{
8989
Some(ts_to_clar_type(&boxed_type_annotation.type_annotation).unwrap())
9090
} else {
91-
// type annotation is not always needed
92-
// but this current approach probably isn't robust enough
93-
None
91+
// Try to infer type from initializer expression
92+
if let Some(init_expr) = &variable_declarator.init {
93+
self.infer_type_from_expression(init_expr)
94+
} else {
95+
None
96+
}
9497
};
9598

9699
let binding_name = if let ast::BindingPattern {
@@ -109,6 +112,45 @@ impl<'a> StatementConverter<'a> {
109112
type_annotation
110113
}
111114

115+
fn infer_type_from_expression(&self, expr: &Expression<'a>) -> Option<TypeSignature> {
116+
match expr {
117+
// Handle method call chains like: counts.get(txSender).defaultTo(0)
118+
Expression::CallExpression(call_expr) => {
119+
if let Expression::StaticMemberExpression(member_expr) = &call_expr.callee {
120+
if member_expr.property.name.as_str() == "defaultTo" {
121+
if let Some(root_type) = self.find_root_data_map_type(&member_expr.object) {
122+
return Some(root_type);
123+
}
124+
}
125+
}
126+
None
127+
}
128+
_ => None,
129+
}
130+
}
131+
132+
fn find_root_data_map_type(&self, expr: &Expression<'a>) -> Option<TypeSignature> {
133+
match expr {
134+
Expression::StaticMemberExpression(member_expr) => {
135+
self.find_root_data_map_type(&member_expr.object)
136+
}
137+
Expression::CallExpression(call_expr) => {
138+
self.find_root_data_map_type(&call_expr.callee)
139+
}
140+
Expression::Identifier(ident) => {
141+
let var_name = ident.name.as_str();
142+
self.ir.data_maps.iter().find_map(|data_map| {
143+
if data_map.name == var_name {
144+
Some(data_map.value_type.clone())
145+
} else {
146+
None
147+
}
148+
})
149+
}
150+
_ => None,
151+
}
152+
}
153+
112154
fn get_parameter_type(&self, param_name: &str) -> Option<&TypeSignature> {
113155
self.function
114156
.parameters
@@ -387,6 +429,16 @@ impl<'a> Traverse<'a, ConverterState<'a>> for StatementConverter<'a> {
387429
}
388430
Expression::StaticMemberExpression(member_expr) => {
389431
let Expression::Identifier(ident) = &member_expr.object else {
432+
if member_expr.property.name.as_str() == "defaultTo" {
433+
// For defaultTo, we need special handling to get correct argument order
434+
self.lists_stack
435+
.push(PreSymbolicExpression::list(vec![atom("default-to")]));
436+
// Keep the current context type for proper type inference of the default value
437+
// The argument should have the same type as the optional's inner type
438+
self.current_context_type_stack
439+
.push(self.current_context_type.clone());
440+
return;
441+
}
390442
return;
391443
};
392444
let ident_name = ident.name.as_str();
@@ -410,6 +462,27 @@ impl<'a> Traverse<'a, ConverterState<'a>> for StatementConverter<'a> {
410462
return;
411463
}
412464

465+
// Handle data map access
466+
if let Some(data_map) = self
467+
.ir
468+
.data_maps
469+
.iter()
470+
.find(|data_map| data_map.name == ident_name)
471+
{
472+
self.current_context_type = Some(data_map.value_type.clone());
473+
let atom_name = match member_expr.property.name.as_str() {
474+
"get" => "map-get?",
475+
"insert" => "map-insert",
476+
"set" => "map-set",
477+
"delete" => "map-delete",
478+
_ => return,
479+
};
480+
self.lists_stack
481+
.push(PreSymbolicExpression::list(vec![atom(atom_name)]));
482+
ctx.state.ingest_call_expression = true;
483+
return;
484+
}
485+
413486
// Handle std namespace calls
414487
if self
415488
.ir
@@ -442,9 +515,24 @@ impl<'a> Traverse<'a, ConverterState<'a>> for StatementConverter<'a> {
442515

443516
fn exit_call_expression(
444517
&mut self,
445-
_call_expr: &mut ast::CallExpression<'a>,
518+
call_expr: &mut ast::CallExpression<'a>,
446519
ctx: &mut TraverseCtx<'a>,
447520
) {
521+
if let Expression::StaticMemberExpression(member_expr) = &call_expr.callee {
522+
if member_expr.property.name.as_str() == "defaultTo" {
523+
// For defaultTo, we need to reorder arguments: (default-to default_value optional_expr)
524+
if let Some(current_list) = self.lists_stack.last_mut() {
525+
if let PreSymbolicExpressionType::List(list) = &mut current_list.pre_expr {
526+
if list.len() == 3 {
527+
list.swap(1, 2);
528+
}
529+
}
530+
}
531+
self.ingest_last_stack_item();
532+
return;
533+
}
534+
}
535+
448536
if ctx.state.ingest_call_expression {
449537
// Don't ingest immediately if we're inside an object property
450538
// Let the object property handler take care of it
@@ -1336,4 +1424,58 @@ mod test {
13361424
let expected_clar_src = "{ list: (list (list true false)) }";
13371425
assert_body_eq(ts_src, expected_clar_src);
13381426
}
1427+
1428+
#[test]
1429+
fn test_data_map_get() {
1430+
let ts_src = indoc! {
1431+
r#"const counts = new DataMap<Principal, Uint>();
1432+
function getMyCount() {
1433+
const count = counts.get(txSender);
1434+
return count;
1435+
}"#
1436+
};
1437+
let expected_clar_src = "(let ((count (map-get? counts tx-sender))) count)";
1438+
assert_body_eq(ts_src, expected_clar_src);
1439+
}
1440+
1441+
#[test]
1442+
fn test_optional_default_to() {
1443+
let ts_src = indoc! {
1444+
r#"const counts = new DataMap<Principal, Uint>();
1445+
function getMyCount() {
1446+
return counts.get(txSender).defaultTo(0);
1447+
}"#
1448+
};
1449+
let expected_clar_src = "(default-to u0 (map-get? counts tx-sender))";
1450+
assert_body_eq(ts_src, expected_clar_src);
1451+
}
1452+
1453+
#[test]
1454+
fn test_optional_default_to_type_inference() {
1455+
let ts_src = indoc! {
1456+
r#"const counts = new DataMap<Principal, Uint>();
1457+
function getMyCountPlus1() {
1458+
return counts.get(txSender).defaultTo(0) + 1;
1459+
}"#
1460+
};
1461+
let expected_clar_src = "(+ (default-to u0 (map-get? counts tx-sender)) u1)";
1462+
assert_body_eq(ts_src, expected_clar_src);
1463+
}
1464+
1465+
#[test]
1466+
fn test_variable_type_inference() {
1467+
let ts_src = indoc! {
1468+
r#"const counts = new DataMap<Principal, Uint>();
1469+
function getMyCountPlus1() {
1470+
const currentCount = counts.get(txSender).defaultTo(0);
1471+
return currentCount + 1;
1472+
}"#
1473+
};
1474+
let expected_clar_src = indoc! {
1475+
r#"(let ((current-count (default-to u0 (map-get? counts tx-sender))))
1476+
(+ current-count u1)
1477+
)"#
1478+
};
1479+
assert_body_eq(ts_src, expected_clar_src);
1480+
}
13391481
}

components/ts-to-clar/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ mod test {
3535
fn test_transpile_toplevel_data() {
3636
let src = indoc! {
3737
"const OWNER_ROLE = new Constant<Uint>(1);
38+
const ERR_FORBIDDEN = new Constant<ClError<never, Uint>>(err(4001));
3839
const count = new DataVar<Uint>(0);
3940
const msgs = new DataMap<Uint, StringAscii<16>>();
4041
"};
@@ -44,6 +45,7 @@ mod test {
4445
clarity_code,
4546
indoc! {r#"
4647
(define-const OWNER_ROLE u1)
48+
(define-const ERR_FORBIDDEN (err u4001))
4749
(define-data-var count uint u0)
4850
(define-map msgs
4951
uint

components/ts-to-clar/src/parser.rs

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,7 @@ mod test {
347347
use clarity::vm::types::TypeSignature::{self, *};
348348
use clarity::vm::ClarityName;
349349
use indoc::{formatdoc, indoc};
350-
use oxc_allocator::{Allocator, Box, FromIn};
350+
use oxc_allocator::{Allocator, FromIn};
351351
use oxc_ast::ast::{
352352
BinaryOperator, Expression, NumberBase, ObjectPropertyKind, PropertyKey, PropertyKind,
353353
Statement,
@@ -378,6 +378,18 @@ mod test {
378378
)
379379
}
380380

381+
fn expr_error_uint<'a>(allocator: &'a Allocator, value: f64) -> Expression<'a> {
382+
let func = expr_identifier(allocator, "err");
383+
let arg = expr_number(allocator, value);
384+
AstBuilder::new(allocator).expression_call(
385+
Span::empty(0),
386+
func,
387+
None::<oxc_allocator::Box<'a, oxc_ast::ast::TSTypeParameterInstantiation<'a>>>,
388+
oxc_allocator::Vec::from_array_in([arg.into()], allocator),
389+
false,
390+
)
391+
}
392+
381393
fn expr_string<'a>(allocator: &'a Allocator, value: &'a str) -> Expression<'a> {
382394
AstBuilder::new(allocator).expression_string_literal(
383395
Span::empty(0),
@@ -398,7 +410,7 @@ mod test {
398410
) -> Expression<'a> {
399411
let expr =
400412
AstBuilder::new(allocator).binary_expression(Span::empty(0), left, operator, right);
401-
Expression::BinaryExpression(Box::new_in(expr, allocator))
413+
Expression::BinaryExpression(oxc_allocator::Box::new_in(expr, allocator))
402414
}
403415

404416
fn simple_object_property<'a>(
@@ -418,7 +430,6 @@ mod test {
418430
)
419431
}
420432

421-
#[track_caller]
422433
fn assert_expr_eq(actual: &Expression, expected: &Expression) {
423434
use Expression::*;
424435
match (&actual, &expected) {
@@ -466,6 +477,17 @@ mod test {
466477
}
467478
}
468479
}
480+
(CallExpression(actual_call), CallExpression(expected_call)) => {
481+
assert_expr_eq(&actual_call.callee, &expected_call.callee);
482+
assert_eq!(actual_call.arguments.len(), expected_call.arguments.len());
483+
for (actual_arg, expected_arg) in actual_call
484+
.arguments
485+
.iter()
486+
.zip(expected_call.arguments.iter())
487+
{
488+
assert_expr_eq(actual_arg.to_expression(), expected_arg.to_expression());
489+
}
490+
}
469491
_ => panic!("Expected matching expression types"),
470492
}
471493
}
@@ -512,6 +534,19 @@ mod test {
512534
assert_constant_eq(&constants[2], &expected);
513535
}
514536

537+
#[test]
538+
fn test_constant_err_ir() {
539+
let allocator = Allocator::default();
540+
let src = "const ERR_FORBIDDEN = new Constant<ClError<never, Uint>>(err(4003));";
541+
let constants = get_tmp_ir(&allocator, src).constants;
542+
let expected = IRConstant {
543+
name: "ERR_FORBIDDEN".to_string(),
544+
r#type: ResponseType(Box::new((NoType, UIntType))),
545+
expr: expr_error_uint(&allocator, 4003.0),
546+
};
547+
assert_constant_eq(&constants[0], &expected);
548+
}
549+
515550
#[test]
516551
fn test_data_var_ir() {
517552
let allocator = Allocator::default();

components/ts-to-clar/src/types.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ fn extract_type(
4343
"Int" => Ok(TypeSignature::IntType),
4444
"Bool" => Ok(TypeSignature::BoolType),
4545
"Principal" => Ok(TypeSignature::PrincipalType),
46+
"ClError" => Ok(TypeSignature::ResponseType(Box::new((
47+
TypeSignature::NoType,
48+
TypeSignature::UIntType,
49+
)))),
4650
"StringAscii" => extract_numeric_type_param(type_params).map(get_ascii_type),
4751
"StringUtf8" => extract_numeric_type_param(type_params).map(get_utf8_type),
4852
_ => Err(anyhow!("Unknown type: {}", type_ident)),

0 commit comments

Comments
 (0)