From 240ae94ff5cef4a119ab3ea6a9ae10fc475e8e03 Mon Sep 17 00:00:00 2001 From: sofia-bobbiesi Date: Wed, 2 Jul 2025 13:21:49 -0300 Subject: [PATCH 1/9] feat: implement tuple case parsing for variant cases --- crates/tx3-lang/src/parsing.rs | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/crates/tx3-lang/src/parsing.rs b/crates/tx3-lang/src/parsing.rs index 26407e4b..dc397eaf 100644 --- a/crates/tx3-lang/src/parsing.rs +++ b/crates/tx3-lang/src/parsing.rs @@ -1312,6 +1312,33 @@ impl VariantCase { }) } + fn tuple_case_parse(pair: pest::iterators::Pair) -> Result { + let span: Span = pair.as_span().into(); + let mut inner = pair.into_inner(); + + let identifier = Identifier::parse(inner.next().unwrap())?; + + let types = inner.map(Type::parse).collect::, _>>()?; + + // REVIEW + // Convert types to record fields with auto-generated names (_0, _1, _2, ...) + let fields = types + .into_iter() + .enumerate() + .map(|(index, r#type)| RecordField { + name: Identifier::new(format!("_{}", index)), + r#type, + span: span.clone(), + }) + .collect(); + + Ok(Self { + name: identifier, + fields, + span, + }) + } + fn unit_case_parse(pair: pest::iterators::Pair) -> Result { let span = pair.as_span().into(); let mut inner = pair.into_inner(); @@ -1332,7 +1359,7 @@ impl AstNode for VariantCase { fn parse(pair: Pair) -> Result { let case = match pair.as_rule() { Rule::variant_case_struct => Self::struct_case_parse(pair), - Rule::variant_case_tuple => todo!("parse variant case tuple"), + Rule::variant_case_tuple => Self::tuple_case_parse(pair), Rule::variant_case_unit => Self::unit_case_parse(pair), x => unreachable!("Unexpected rule in datum_variant: {:?}", x), }?; From df478ee56d377b19c6658eadc829c5e8efcc4e06 Mon Sep 17 00:00:00 2001 From: Benjamin Martinez Picech Date: Wed, 2 Jul 2025 15:51:46 -0300 Subject: [PATCH 2/9] tuples new type parsing to Expression --- crates/tx3-lang/src/analyzing.rs | 13 + crates/tx3-lang/src/ast.rs | 24 ++ crates/tx3-lang/src/ir.rs | 1 + crates/tx3-lang/src/lowering.rs | 15 ++ crates/tx3-lang/src/parsing.rs | 70 +++--- crates/tx3-lang/src/tx3.pest | 7 + examples/tuple.ast | 408 +++++++++++++++++++++++++++++++ examples/tuple.transfer.tir | 172 +++++++++++++ examples/tuple.tx3 | 29 +++ 9 files changed, 711 insertions(+), 28 deletions(-) create mode 100644 examples/tuple.ast create mode 100644 examples/tuple.transfer.tir create mode 100644 examples/tuple.tx3 diff --git a/crates/tx3-lang/src/analyzing.rs b/crates/tx3-lang/src/analyzing.rs index 836357fc..f3c20a76 100644 --- a/crates/tx3-lang/src/analyzing.rs +++ b/crates/tx3-lang/src/analyzing.rs @@ -540,11 +540,22 @@ impl Analyzable for ListConstructor { } } +impl Analyzable for TupleConstructor { + fn analyze(&mut self, parent: Option>) -> AnalyzeReport { + self.fst.analyze(parent.clone()) + self.snd.analyze(parent.clone()) + } + + fn is_resolved(&self) -> bool { + self.fst.is_resolved() && self.snd.is_resolved() + } +} + impl Analyzable for DataExpr { fn analyze(&mut self, parent: Option>) -> AnalyzeReport { match self { DataExpr::StructConstructor(x) => x.analyze(parent), DataExpr::ListConstructor(x) => x.analyze(parent), + DataExpr::TupleConstructor(x) => x.analyze(parent), DataExpr::Identifier(x) => x.analyze(parent), DataExpr::AddOp(x) => x.analyze(parent), DataExpr::SubOp(x) => x.analyze(parent), @@ -676,6 +687,7 @@ impl Analyzable for Type { match self { Type::Custom(x) => x.analyze(parent), Type::List(x) => x.analyze(parent), + Type::Tuple(fst, snd) => fst.analyze(parent.clone()) + snd.analyze(parent), _ => AnalyzeReport::default(), } } @@ -684,6 +696,7 @@ impl Analyzable for Type { match self { Type::Custom(x) => x.is_resolved(), Type::List(x) => x.is_resolved(), + Type::Tuple(fst, snd) => fst.is_resolved() && snd.is_resolved(), _ => true, } } diff --git a/crates/tx3-lang/src/ast.rs b/crates/tx3-lang/src/ast.rs index 45ab15f9..4b0a97ff 100644 --- a/crates/tx3-lang/src/ast.rs +++ b/crates/tx3-lang/src/ast.rs @@ -572,6 +572,26 @@ impl AnyAssetConstructor { } } +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct TupleConstructor { + pub fst: Box, + pub snd: Box, + pub span: Span, +} + +impl TupleConstructor { + pub fn target_type(&self) -> Option { + Some(Type::Tuple( + Box::new(self.fst.target_type()?), + Box::new(self.snd.target_type()?), + )) + } + + pub fn is_resolved(&self) -> bool { + self.fst.target_type().is_some() && self.snd.target_type().is_some() + } +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct RecordConstructorField { pub name: Identifier, @@ -701,6 +721,7 @@ pub enum DataExpr { HexString(HexStringLiteral), StructConstructor(StructConstructor), ListConstructor(ListConstructor), + TupleConstructor(TupleConstructor), StaticAssetConstructor(StaticAssetConstructor), AnyAssetConstructor(AnyAssetConstructor), Identifier(Identifier), @@ -730,6 +751,7 @@ impl DataExpr { DataExpr::HexString(_) => Some(Type::Bytes), DataExpr::StructConstructor(x) => x.target_type(), DataExpr::ListConstructor(x) => x.target_type(), + DataExpr::TupleConstructor(x) => x.target_type(), DataExpr::AddOp(x) => x.target_type(), DataExpr::SubOp(x) => x.target_type(), DataExpr::NegateOp(x) => x.target_type(), @@ -770,6 +792,7 @@ pub enum Type { AnyAsset, List(Box), Custom(Identifier), + Tuple(Box, Box), } impl std::fmt::Display for Type { @@ -786,6 +809,7 @@ impl std::fmt::Display for Type { Type::Utxo => write!(f, "Utxo"), Type::List(inner) => write!(f, "List<{}>", inner), Type::Custom(id) => write!(f, "{}", id.value), + Type::Tuple(fst, snd) => write!(f, "({} {})", fst, snd), } } } diff --git a/crates/tx3-lang/src/ir.rs b/crates/tx3-lang/src/ir.rs index 36a8707d..8b525ff3 100644 --- a/crates/tx3-lang/src/ir.rs +++ b/crates/tx3-lang/src/ir.rs @@ -150,6 +150,7 @@ pub enum Type { AnyAsset, List, Custom(String), + Tuple, } #[derive(Encode, Decode, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] diff --git a/crates/tx3-lang/src/lowering.rs b/crates/tx3-lang/src/lowering.rs index d716381b..1f7caa7b 100644 --- a/crates/tx3-lang/src/lowering.rs +++ b/crates/tx3-lang/src/lowering.rs @@ -323,6 +323,7 @@ impl IntoLower for ast::Type { ast::Type::AnyAsset => Ok(ir::Type::AnyAsset), ast::Type::List(_) => Ok(ir::Type::List), ast::Type::Custom(x) => Ok(ir::Type::Custom(x.value.clone())), + ast::Type::Tuple(_, _) => Ok(ir::Type::Tuple), } } } @@ -400,6 +401,17 @@ impl IntoLower for ast::ListConstructor { } } +impl IntoLower for ast::TupleConstructor { + type Output = ir::Expression; + + fn into_lower(&self, ctx: &Context) -> Result { + let fst = self.fst.into_lower(ctx)?; + let snd = self.snd.into_lower(ctx)?; + + Ok(ir::Expression::Tuple(Box::new((fst, snd)))) + } +} + impl IntoLower for ast::DataExpr { type Output = ir::Expression; @@ -412,6 +424,7 @@ impl IntoLower for ast::DataExpr { ast::DataExpr::HexString(x) => ir::Expression::Bytes(hex::decode(&x.value)?), ast::DataExpr::StructConstructor(x) => ir::Expression::Struct(x.into_lower(ctx)?), ast::DataExpr::ListConstructor(x) => ir::Expression::List(x.into_lower(ctx)?), + ast::DataExpr::TupleConstructor(x) => x.into_lower(ctx)?, ast::DataExpr::StaticAssetConstructor(x) => x.into_lower(ctx)?, ast::DataExpr::AnyAssetConstructor(x) => x.into_lower(ctx)?, ast::DataExpr::Unit => ir::Expression::Struct(ir::StructExpr::unit()), @@ -867,4 +880,6 @@ mod tests { test_lowering!(env_vars); test_lowering!(local_vars); + + test_lowering!(tuple); } diff --git a/crates/tx3-lang/src/parsing.rs b/crates/tx3-lang/src/parsing.rs index dc397eaf..e66afff9 100644 --- a/crates/tx3-lang/src/parsing.rs +++ b/crates/tx3-lang/src/parsing.rs @@ -1064,6 +1064,28 @@ impl AstNode for ListConstructor { } } +impl AstNode for TupleConstructor { + const RULE: Rule = Rule::tuple_constructor; + + fn parse(pair: Pair) -> Result { + let span = pair.as_span().into(); + let mut inner = pair.into_inner(); + + let fst = DataExpr::parse(inner.next().unwrap())?; + let snd = DataExpr::parse(inner.next().unwrap())?; + + Ok(TupleConstructor { + fst: Box::new(fst), + snd: Box::new(snd), + span, + }) + } + + fn span(&self) -> &Span { + &self.span + } +} + impl DataExpr { fn number_parse(pair: Pair) -> Result { Ok(DataExpr::Number(pair.as_str().parse().unwrap())) @@ -1162,6 +1184,9 @@ impl AstNode for DataExpr { Rule::hex_string => Ok(DataExpr::HexString(HexStringLiteral::parse(x)?)), Rule::struct_constructor => DataExpr::struct_constructor_parse(x), Rule::list_constructor => DataExpr::list_constructor_parse(x), + Rule::tuple_constructor => { + Ok(DataExpr::TupleConstructor(TupleConstructor::parse(x)?)) + } Rule::unit => Ok(DataExpr::Unit), Rule::identifier => DataExpr::identifier_parse(x), Rule::utxo_ref => DataExpr::utxo_ref_parse(x), @@ -1196,6 +1221,7 @@ impl AstNode for DataExpr { DataExpr::HexString(x) => x.span(), DataExpr::StructConstructor(x) => x.span(), DataExpr::ListConstructor(x) => x.span(), + DataExpr::TupleConstructor(x) => x.span(), DataExpr::StaticAssetConstructor(x) => x.span(), DataExpr::AnyAssetConstructor(x) => x.span(), DataExpr::Identifier(x) => x.span(), @@ -1228,6 +1254,12 @@ impl AstNode for Type { let inner = inner.into_inner().next().unwrap(); Ok(Type::List(Box::new(Type::parse(inner)?))) } + Rule::tuple_type => { + let mut inner = inner.into_inner(); + let fst = Type::parse(inner.next().unwrap())?; + let snd = Type::parse(inner.next().unwrap())?; + Ok(Type::Tuple(Box::new(fst), Box::new(snd))) + } Rule::custom_type => Ok(Type::Custom(Identifier::new(inner.as_str().to_owned()))), x => unreachable!("Unexpected rule in type: {:?}", x), } @@ -1312,33 +1344,6 @@ impl VariantCase { }) } - fn tuple_case_parse(pair: pest::iterators::Pair) -> Result { - let span: Span = pair.as_span().into(); - let mut inner = pair.into_inner(); - - let identifier = Identifier::parse(inner.next().unwrap())?; - - let types = inner.map(Type::parse).collect::, _>>()?; - - // REVIEW - // Convert types to record fields with auto-generated names (_0, _1, _2, ...) - let fields = types - .into_iter() - .enumerate() - .map(|(index, r#type)| RecordField { - name: Identifier::new(format!("_{}", index)), - r#type, - span: span.clone(), - }) - .collect(); - - Ok(Self { - name: identifier, - fields, - span, - }) - } - fn unit_case_parse(pair: pest::iterators::Pair) -> Result { let span = pair.as_span().into(); let mut inner = pair.into_inner(); @@ -1359,7 +1364,7 @@ impl AstNode for VariantCase { fn parse(pair: Pair) -> Result { let case = match pair.as_rule() { Rule::variant_case_struct => Self::struct_case_parse(pair), - Rule::variant_case_tuple => Self::tuple_case_parse(pair), + Rule::variant_case_tuple => todo!("parse variant case tuple"), Rule::variant_case_unit => Self::unit_case_parse(pair), x => unreachable!("Unexpected rule in datum_variant: {:?}", x), }?; @@ -1496,6 +1501,13 @@ mod tests { input_to_ast_check!(Type, "list", "List", Type::List(Box::new(Type::Int))); + input_to_ast_check!( + Type, + "tuple", + "Tuple", + Type::Tuple(Box::new(Type::Int), Box::new(Type::Bytes)) + ); + input_to_ast_check!( Type, "identifier", @@ -2400,4 +2412,6 @@ mod tests { test_parsing!(env_vars); test_parsing!(local_vars); + + test_parsing!(tuple); } diff --git a/crates/tx3-lang/src/tx3.pest b/crates/tx3-lang/src/tx3.pest index dcb83336..0da6066d 100644 --- a/crates/tx3-lang/src/tx3.pest +++ b/crates/tx3-lang/src/tx3.pest @@ -24,10 +24,12 @@ primitive_type = { custom_type = { identifier } list_type = { "List<" ~ type ~ ">" } +tuple_type = { "Tuple<" ~ type ~ "," ~ type ~ ">"} type = { primitive_type | list_type | + tuple_type | custom_type } @@ -141,6 +143,7 @@ data_expr = { data_prefix* ~ data_primary ~ data_postfix* ~ (data_infix ~ data_p string | struct_constructor | list_constructor | + tuple_constructor | any_asset_constructor | static_asset_constructor | identifier | @@ -180,6 +183,10 @@ list_constructor = { "[" ~ (data_expr ~ ",")* ~ data_expr? ~ "]" } +tuple_constructor = { + "(" ~ data_expr ~ "," ~ data_expr ~ ")" +} + address_expr = { identifier | hex_string | diff --git a/examples/tuple.ast b/examples/tuple.ast new file mode 100644 index 00000000..ba81e32c --- /dev/null +++ b/examples/tuple.ast @@ -0,0 +1,408 @@ +{ + "env": null, + "txs": [ + { + "name": { + "value": "transfer", + "span": { + "dummy": false, + "start": 73, + "end": 81 + } + }, + "parameters": { + "parameters": [ + { + "name": { + "value": "quantity", + "span": { + "dummy": false, + "start": 87, + "end": 95 + } + }, + "type": "Int" + } + ], + "span": { + "dummy": false, + "start": 81, + "end": 102 + } + }, + "locals": null, + "references": [], + "inputs": [ + { + "name": "source", + "is_many": false, + "fields": [ + { + "From": { + "Identifier": { + "value": "Sender", + "span": { + "dummy": false, + "start": 138, + "end": 144 + } + } + } + }, + { + "MinAmount": { + "StaticAssetConstructor": { + "type": { + "value": "Ada", + "span": { + "dummy": false, + "start": 166, + "end": 169 + } + }, + "amount": { + "Identifier": { + "value": "quantity", + "span": { + "dummy": false, + "start": 170, + "end": 178 + } + } + }, + "span": { + "dummy": false, + "start": 166, + "end": 179 + } + } + } + } + ], + "span": { + "dummy": false, + "start": 109, + "end": 186 + } + } + ], + "outputs": [ + { + "name": null, + "fields": [ + { + "To": { + "Identifier": { + "value": "Receiver", + "span": { + "dummy": false, + "start": 217, + "end": 225 + } + } + } + }, + { + "Amount": { + "StaticAssetConstructor": { + "type": { + "value": "Ada", + "span": { + "dummy": false, + "start": 243, + "end": 246 + } + }, + "amount": { + "Identifier": { + "value": "quantity", + "span": { + "dummy": false, + "start": 247, + "end": 255 + } + } + }, + "span": { + "dummy": false, + "start": 243, + "end": 256 + } + } + } + } + ], + "span": { + "dummy": false, + "start": 196, + "end": 263 + } + }, + { + "name": null, + "fields": [ + { + "To": { + "Identifier": { + "value": "Sender", + "span": { + "dummy": false, + "start": 290, + "end": 296 + } + } + } + }, + { + "Amount": { + "SubOp": { + "lhs": { + "SubOp": { + "lhs": { + "Identifier": { + "value": "source", + "span": { + "dummy": false, + "start": 314, + "end": 320 + } + } + }, + "rhs": { + "StaticAssetConstructor": { + "type": { + "value": "Ada", + "span": { + "dummy": false, + "start": 323, + "end": 326 + } + }, + "amount": { + "Identifier": { + "value": "quantity", + "span": { + "dummy": false, + "start": 327, + "end": 335 + } + } + }, + "span": { + "dummy": false, + "start": 323, + "end": 336 + } + } + }, + "span": { + "dummy": false, + "start": 321, + "end": 322 + } + } + }, + "rhs": { + "Identifier": { + "value": "fees", + "span": { + "dummy": false, + "start": 339, + "end": 343 + } + } + }, + "span": { + "dummy": false, + "start": 337, + "end": 338 + } + } + } + }, + { + "Datum": { + "StructConstructor": { + "type": { + "value": "Datum", + "span": { + "dummy": false, + "start": 360, + "end": 365 + } + }, + "case": { + "name": { + "value": "Default", + "span": { + "dummy": true, + "start": 0, + "end": 0 + } + }, + "fields": [ + { + "name": { + "value": "A", + "span": { + "dummy": false, + "start": 378, + "end": 379 + } + }, + "value": { + "TupleConstructor": { + "fst": { + "Number": 1 + }, + "snd": { + "Number": 2 + }, + "span": { + "dummy": false, + "start": 381, + "end": 386 + } + } + }, + "span": { + "dummy": false, + "start": 378, + "end": 386 + } + } + ], + "spread": null, + "span": { + "dummy": false, + "start": 366, + "end": 397 + } + }, + "span": { + "dummy": false, + "start": 360, + "end": 397 + } + } + } + } + ], + "span": { + "dummy": false, + "start": 269, + "end": 404 + } + } + ], + "validity": null, + "burn": null, + "mints": [], + "signers": null, + "adhoc": [], + "span": { + "dummy": false, + "start": 70, + "end": 406 + }, + "collateral": [], + "metadata": null + } + ], + "types": [ + { + "name": { + "value": "Datum", + "span": { + "dummy": false, + "start": 5, + "end": 10 + } + }, + "cases": [ + { + "name": { + "value": "Default", + "span": { + "dummy": true, + "start": 0, + "end": 0 + } + }, + "fields": [ + { + "name": { + "value": "A", + "span": { + "dummy": false, + "start": 15, + "end": 16 + } + }, + "type": { + "Tuple": [ + "Int", + "Int" + ] + }, + "span": { + "dummy": false, + "start": 15, + "end": 33 + } + } + ], + "span": { + "dummy": false, + "start": 0, + "end": 36 + } + } + ], + "span": { + "dummy": false, + "start": 0, + "end": 36 + } + } + ], + "assets": [], + "parties": [ + { + "name": { + "value": "Sender", + "span": { + "dummy": false, + "start": 44, + "end": 50 + } + }, + "span": { + "dummy": false, + "start": 38, + "end": 51 + } + }, + { + "name": { + "value": "Receiver", + "span": { + "dummy": false, + "start": 59, + "end": 67 + } + }, + "span": { + "dummy": false, + "start": 53, + "end": 68 + } + } + ], + "policies": [], + "span": { + "dummy": false, + "start": 0, + "end": 406 + } +} \ No newline at end of file diff --git a/examples/tuple.transfer.tir b/examples/tuple.transfer.tir new file mode 100644 index 00000000..fe3598ad --- /dev/null +++ b/examples/tuple.transfer.tir @@ -0,0 +1,172 @@ +{ + "fees": { + "EvalParam": "ExpectFees" + }, + "references": [], + "inputs": [ + { + "name": "source", + "query": { + "address": { + "EvalParam": { + "ExpectValue": [ + "sender", + "Address" + ] + } + }, + "min_amount": { + "Assets": [ + { + "policy": "None", + "asset_name": "None", + "amount": { + "EvalParam": { + "ExpectValue": [ + "quantity", + "Int" + ] + } + } + } + ] + }, + "ref": "None" + }, + "refs": [], + "redeemer": null, + "policy": null + } + ], + "outputs": [ + { + "address": { + "EvalParam": { + "ExpectValue": [ + "receiver", + "Address" + ] + } + }, + "datum": "None", + "amount": { + "Assets": [ + { + "policy": "None", + "asset_name": "None", + "amount": { + "EvalParam": { + "ExpectValue": [ + "quantity", + "Int" + ] + } + } + } + ] + } + }, + { + "address": { + "EvalParam": { + "ExpectValue": [ + "sender", + "Address" + ] + } + }, + "datum": { + "Struct": { + "constructor": 0, + "fields": [ + { + "Tuple": [ + { + "Number": 1 + }, + { + "Number": 2 + } + ] + } + ] + } + }, + "amount": { + "EvalBuiltIn": { + "Sub": [ + { + "EvalBuiltIn": { + "Sub": [ + { + "EvalCoerce": { + "IntoAssets": { + "EvalParam": { + "ExpectInput": [ + "source", + { + "address": { + "EvalParam": { + "ExpectValue": [ + "sender", + "Address" + ] + } + }, + "min_amount": { + "Assets": [ + { + "policy": "None", + "asset_name": "None", + "amount": { + "EvalParam": { + "ExpectValue": [ + "quantity", + "Int" + ] + } + } + } + ] + }, + "ref": "None" + } + ] + } + } + } + }, + { + "Assets": [ + { + "policy": "None", + "asset_name": "None", + "amount": { + "EvalParam": { + "ExpectValue": [ + "quantity", + "Int" + ] + } + } + } + ] + } + ] + } + }, + { + "EvalParam": "ExpectFees" + } + ] + } + } + } + ], + "validity": null, + "mints": [], + "adhoc": [], + "collateral": [], + "signers": null, + "metadata": [] +} \ No newline at end of file diff --git a/examples/tuple.tx3 b/examples/tuple.tx3 new file mode 100644 index 00000000..79bf6718 --- /dev/null +++ b/examples/tuple.tx3 @@ -0,0 +1,29 @@ +type Datum { + A: Tuple, +} + +party Sender; + +party Receiver; + +tx transfer( + quantity: Int +) { + input source { + from: Sender, + min_amount: Ada(quantity), + } + + output { + to: Receiver, + amount: Ada(quantity), + } + + output { + to: Sender, + amount: source - Ada(quantity) - fees, + datum: Datum { + A: (1,2), + }, + } +} \ No newline at end of file From 4756a2c296348361837b6466bd44de135260bda0 Mon Sep 17 00:00:00 2001 From: Benjamin Martinez Picech Date: Wed, 2 Jul 2025 17:02:40 -0300 Subject: [PATCH 3/9] adding particular case for list of tuples --- crates/tx3-cardano/src/compile/mod.rs | 1 + crates/tx3-cardano/src/compile/plutus_data.rs | 39 +++++++++++++++++-- crates/tx3-lang/src/applying.rs | 2 +- 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/crates/tx3-cardano/src/compile/mod.rs b/crates/tx3-cardano/src/compile/mod.rs index 27b2f6ae..57865333 100644 --- a/crates/tx3-cardano/src/compile/mod.rs +++ b/crates/tx3-cardano/src/compile/mod.rs @@ -79,6 +79,7 @@ fn compile_data_expr(ir: &ir::Expression) -> Result Ok(x.as_str().as_data()), ir::Expression::Struct(x) => compile_struct(x), ir::Expression::Address(x) => Ok(x.as_data()), + ir::Expression::Tuple(x) => Ok(x.try_as_data()?), _ => Err(Error::CoerceError( format!("{:?}", ir), "DataExpr".to_string(), diff --git a/crates/tx3-cardano/src/compile/plutus_data.rs b/crates/tx3-cardano/src/compile/plutus_data.rs index e6ce3731..98c24b82 100644 --- a/crates/tx3-cardano/src/compile/plutus_data.rs +++ b/crates/tx3-cardano/src/compile/plutus_data.rs @@ -1,4 +1,5 @@ pub use pallas::codec::utils::Int; +use pallas::codec::utils::KeyValuePairs; pub use pallas::ledger::primitives::{BigInt, BoundedBytes, Constr, MaybeIndefArray, PlutusData}; use tx3_lang::ir; @@ -90,12 +91,41 @@ impl IntoData for i128 { impl TryIntoData for Vec { fn try_as_data(&self) -> Result { - let items = self + let all_tuples = self .iter() - .map(TryIntoData::try_as_data) - .collect::, _>>()?; + .all(|expr| matches!(expr, ir::Expression::Tuple(_))); + + if all_tuples && !self.is_empty() { + let key_value_pairs = self + .iter() + .map(|expr| -> Result<(PlutusData, PlutusData), super::Error> { + if let ir::Expression::Tuple(tuple) = expr { + Ok((tuple.0.try_as_data()?, tuple.1.try_as_data()?)) + } else { + unreachable!("Already checked all are tuples") + } + }) + .collect::, _>>()?; + + Ok(PlutusData::Map(KeyValuePairs::Def(key_value_pairs))) + } else { + let items = self + .iter() + .map(TryIntoData::try_as_data) + .collect::, _>>()?; + + Ok(PlutusData::Array(MaybeIndefArray::Def(items))) + } + } +} - Ok(PlutusData::Array(MaybeIndefArray::Def(items))) +impl TryIntoData for (ir::Expression, ir::Expression) { + fn try_as_data(&self) -> Result { + let (fst, snd) = self; + Ok(PlutusData::Map(KeyValuePairs::Def(vec![( + fst.try_as_data()?, + snd.try_as_data()?, + )]))) } } @@ -135,6 +165,7 @@ impl TryIntoData for ir::Expression { ir::Expression::Address(x) => Ok(x.as_data()), ir::Expression::Hash(x) => Ok(x.as_data()), ir::Expression::List(x) => x.try_as_data(), + ir::Expression::Tuple(x) => x.try_as_data(), x => Err(super::Error::CoerceError( format!("{:?}", x), "PlutusData".to_string(), diff --git a/crates/tx3-lang/src/applying.rs b/crates/tx3-lang/src/applying.rs index 12804d55..9f1904c3 100644 --- a/crates/tx3-lang/src/applying.rs +++ b/crates/tx3-lang/src/applying.rs @@ -521,7 +521,7 @@ impl Composite for ir::Coerce { Self::NoOp(x) => Ok(Self::NoOp(x)), Self::IntoAssets(x) => Ok(Self::NoOp(x.into_assets()?)), Self::IntoDatum(x) => Ok(Self::NoOp(x.into_datum()?)), - Self::IntoScript(x) => todo!(), + Self::IntoScript(_x) => todo!(), } } } From f44ab0b7f3936297d01d75822a7b866597e3b731 Mon Sep 17 00:00:00 2001 From: sofia-bobbiesi Date: Wed, 2 Jul 2025 13:21:49 -0300 Subject: [PATCH 4/9] feat: implement tuple case parsing for variant cases --- crates/tx3-lang/src/parsing.rs | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/crates/tx3-lang/src/parsing.rs b/crates/tx3-lang/src/parsing.rs index e4264c69..4c388a24 100644 --- a/crates/tx3-lang/src/parsing.rs +++ b/crates/tx3-lang/src/parsing.rs @@ -1312,6 +1312,33 @@ impl VariantCase { }) } + fn tuple_case_parse(pair: pest::iterators::Pair) -> Result { + let span: Span = pair.as_span().into(); + let mut inner = pair.into_inner(); + + let identifier = Identifier::parse(inner.next().unwrap())?; + + let types = inner.map(Type::parse).collect::, _>>()?; + + // REVIEW + // Convert types to record fields with auto-generated names (_0, _1, _2, ...) + let fields = types + .into_iter() + .enumerate() + .map(|(index, r#type)| RecordField { + name: Identifier::new(format!("_{}", index)), + r#type, + span: span.clone(), + }) + .collect(); + + Ok(Self { + name: identifier, + fields, + span, + }) + } + fn unit_case_parse(pair: pest::iterators::Pair) -> Result { let span = pair.as_span().into(); let mut inner = pair.into_inner(); @@ -1332,7 +1359,7 @@ impl AstNode for VariantCase { fn parse(pair: Pair) -> Result { let case = match pair.as_rule() { Rule::variant_case_struct => Self::struct_case_parse(pair), - Rule::variant_case_tuple => todo!("parse variant case tuple"), + Rule::variant_case_tuple => Self::tuple_case_parse(pair), Rule::variant_case_unit => Self::unit_case_parse(pair), x => unreachable!("Unexpected rule in datum_variant: {:?}", x), }?; From b05aba482c1f9ec12f2854d90d37bfee7382dc74 Mon Sep 17 00:00:00 2001 From: Benjamin Martinez Picech Date: Wed, 2 Jul 2025 15:51:46 -0300 Subject: [PATCH 5/9] tuples new type parsing to Expression --- crates/tx3-lang/src/analyzing.rs | 13 + crates/tx3-lang/src/ast.rs | 24 ++ crates/tx3-lang/src/ir.rs | 1 + crates/tx3-lang/src/lowering.rs | 14 ++ crates/tx3-lang/src/parsing.rs | 70 +++--- crates/tx3-lang/src/tx3.pest | 7 + examples/tuple.ast | 408 +++++++++++++++++++++++++++++++ examples/tuple.transfer.tir | 172 +++++++++++++ examples/tuple.tx3 | 29 +++ 9 files changed, 710 insertions(+), 28 deletions(-) create mode 100644 examples/tuple.ast create mode 100644 examples/tuple.transfer.tir create mode 100644 examples/tuple.tx3 diff --git a/crates/tx3-lang/src/analyzing.rs b/crates/tx3-lang/src/analyzing.rs index 836357fc..f3c20a76 100644 --- a/crates/tx3-lang/src/analyzing.rs +++ b/crates/tx3-lang/src/analyzing.rs @@ -540,11 +540,22 @@ impl Analyzable for ListConstructor { } } +impl Analyzable for TupleConstructor { + fn analyze(&mut self, parent: Option>) -> AnalyzeReport { + self.fst.analyze(parent.clone()) + self.snd.analyze(parent.clone()) + } + + fn is_resolved(&self) -> bool { + self.fst.is_resolved() && self.snd.is_resolved() + } +} + impl Analyzable for DataExpr { fn analyze(&mut self, parent: Option>) -> AnalyzeReport { match self { DataExpr::StructConstructor(x) => x.analyze(parent), DataExpr::ListConstructor(x) => x.analyze(parent), + DataExpr::TupleConstructor(x) => x.analyze(parent), DataExpr::Identifier(x) => x.analyze(parent), DataExpr::AddOp(x) => x.analyze(parent), DataExpr::SubOp(x) => x.analyze(parent), @@ -676,6 +687,7 @@ impl Analyzable for Type { match self { Type::Custom(x) => x.analyze(parent), Type::List(x) => x.analyze(parent), + Type::Tuple(fst, snd) => fst.analyze(parent.clone()) + snd.analyze(parent), _ => AnalyzeReport::default(), } } @@ -684,6 +696,7 @@ impl Analyzable for Type { match self { Type::Custom(x) => x.is_resolved(), Type::List(x) => x.is_resolved(), + Type::Tuple(fst, snd) => fst.is_resolved() && snd.is_resolved(), _ => true, } } diff --git a/crates/tx3-lang/src/ast.rs b/crates/tx3-lang/src/ast.rs index 45ab15f9..4b0a97ff 100644 --- a/crates/tx3-lang/src/ast.rs +++ b/crates/tx3-lang/src/ast.rs @@ -572,6 +572,26 @@ impl AnyAssetConstructor { } } +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct TupleConstructor { + pub fst: Box, + pub snd: Box, + pub span: Span, +} + +impl TupleConstructor { + pub fn target_type(&self) -> Option { + Some(Type::Tuple( + Box::new(self.fst.target_type()?), + Box::new(self.snd.target_type()?), + )) + } + + pub fn is_resolved(&self) -> bool { + self.fst.target_type().is_some() && self.snd.target_type().is_some() + } +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct RecordConstructorField { pub name: Identifier, @@ -701,6 +721,7 @@ pub enum DataExpr { HexString(HexStringLiteral), StructConstructor(StructConstructor), ListConstructor(ListConstructor), + TupleConstructor(TupleConstructor), StaticAssetConstructor(StaticAssetConstructor), AnyAssetConstructor(AnyAssetConstructor), Identifier(Identifier), @@ -730,6 +751,7 @@ impl DataExpr { DataExpr::HexString(_) => Some(Type::Bytes), DataExpr::StructConstructor(x) => x.target_type(), DataExpr::ListConstructor(x) => x.target_type(), + DataExpr::TupleConstructor(x) => x.target_type(), DataExpr::AddOp(x) => x.target_type(), DataExpr::SubOp(x) => x.target_type(), DataExpr::NegateOp(x) => x.target_type(), @@ -770,6 +792,7 @@ pub enum Type { AnyAsset, List(Box), Custom(Identifier), + Tuple(Box, Box), } impl std::fmt::Display for Type { @@ -786,6 +809,7 @@ impl std::fmt::Display for Type { Type::Utxo => write!(f, "Utxo"), Type::List(inner) => write!(f, "List<{}>", inner), Type::Custom(id) => write!(f, "{}", id.value), + Type::Tuple(fst, snd) => write!(f, "({} {})", fst, snd), } } } diff --git a/crates/tx3-lang/src/ir.rs b/crates/tx3-lang/src/ir.rs index 6eed247d..ab525ad7 100644 --- a/crates/tx3-lang/src/ir.rs +++ b/crates/tx3-lang/src/ir.rs @@ -150,6 +150,7 @@ pub enum Type { AnyAsset, List, Custom(String), + Tuple, } #[derive(Encode, Decode, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] diff --git a/crates/tx3-lang/src/lowering.rs b/crates/tx3-lang/src/lowering.rs index 0a5edba0..a2ca7dd1 100644 --- a/crates/tx3-lang/src/lowering.rs +++ b/crates/tx3-lang/src/lowering.rs @@ -323,6 +323,7 @@ impl IntoLower for ast::Type { ast::Type::AnyAsset => Ok(ir::Type::AnyAsset), ast::Type::List(_) => Ok(ir::Type::List), ast::Type::Custom(x) => Ok(ir::Type::Custom(x.value.clone())), + ast::Type::Tuple(_, _) => Ok(ir::Type::Tuple), } } } @@ -400,6 +401,17 @@ impl IntoLower for ast::ListConstructor { } } +impl IntoLower for ast::TupleConstructor { + type Output = ir::Expression; + + fn into_lower(&self, ctx: &Context) -> Result { + let fst = self.fst.into_lower(ctx)?; + let snd = self.snd.into_lower(ctx)?; + + Ok(ir::Expression::Tuple(Box::new((fst, snd)))) + } +} + impl IntoLower for ast::DataExpr { type Output = ir::Expression; @@ -412,6 +424,7 @@ impl IntoLower for ast::DataExpr { ast::DataExpr::HexString(x) => ir::Expression::Bytes(hex::decode(&x.value)?), ast::DataExpr::StructConstructor(x) => ir::Expression::Struct(x.into_lower(ctx)?), ast::DataExpr::ListConstructor(x) => ir::Expression::List(x.into_lower(ctx)?), + ast::DataExpr::TupleConstructor(x) => x.into_lower(ctx)?, ast::DataExpr::StaticAssetConstructor(x) => x.into_lower(ctx)?, ast::DataExpr::AnyAssetConstructor(x) => x.into_lower(ctx)?, ast::DataExpr::Unit => ir::Expression::Struct(ir::StructExpr::unit()), @@ -869,4 +882,5 @@ mod tests { test_lowering!(local_vars); test_lowering!(cardano_witness); + test_lowering!(tuple); } diff --git a/crates/tx3-lang/src/parsing.rs b/crates/tx3-lang/src/parsing.rs index 4c388a24..bc1c2a4c 100644 --- a/crates/tx3-lang/src/parsing.rs +++ b/crates/tx3-lang/src/parsing.rs @@ -1064,6 +1064,28 @@ impl AstNode for ListConstructor { } } +impl AstNode for TupleConstructor { + const RULE: Rule = Rule::tuple_constructor; + + fn parse(pair: Pair) -> Result { + let span = pair.as_span().into(); + let mut inner = pair.into_inner(); + + let fst = DataExpr::parse(inner.next().unwrap())?; + let snd = DataExpr::parse(inner.next().unwrap())?; + + Ok(TupleConstructor { + fst: Box::new(fst), + snd: Box::new(snd), + span, + }) + } + + fn span(&self) -> &Span { + &self.span + } +} + impl DataExpr { fn number_parse(pair: Pair) -> Result { Ok(DataExpr::Number(pair.as_str().parse().unwrap())) @@ -1162,6 +1184,9 @@ impl AstNode for DataExpr { Rule::hex_string => Ok(DataExpr::HexString(HexStringLiteral::parse(x)?)), Rule::struct_constructor => DataExpr::struct_constructor_parse(x), Rule::list_constructor => DataExpr::list_constructor_parse(x), + Rule::tuple_constructor => { + Ok(DataExpr::TupleConstructor(TupleConstructor::parse(x)?)) + } Rule::unit => Ok(DataExpr::Unit), Rule::identifier => DataExpr::identifier_parse(x), Rule::utxo_ref => DataExpr::utxo_ref_parse(x), @@ -1196,6 +1221,7 @@ impl AstNode for DataExpr { DataExpr::HexString(x) => x.span(), DataExpr::StructConstructor(x) => x.span(), DataExpr::ListConstructor(x) => x.span(), + DataExpr::TupleConstructor(x) => x.span(), DataExpr::StaticAssetConstructor(x) => x.span(), DataExpr::AnyAssetConstructor(x) => x.span(), DataExpr::Identifier(x) => x.span(), @@ -1228,6 +1254,12 @@ impl AstNode for Type { let inner = inner.into_inner().next().unwrap(); Ok(Type::List(Box::new(Type::parse(inner)?))) } + Rule::tuple_type => { + let mut inner = inner.into_inner(); + let fst = Type::parse(inner.next().unwrap())?; + let snd = Type::parse(inner.next().unwrap())?; + Ok(Type::Tuple(Box::new(fst), Box::new(snd))) + } Rule::custom_type => Ok(Type::Custom(Identifier::new(inner.as_str().to_owned()))), x => unreachable!("Unexpected rule in type: {:?}", x), } @@ -1312,33 +1344,6 @@ impl VariantCase { }) } - fn tuple_case_parse(pair: pest::iterators::Pair) -> Result { - let span: Span = pair.as_span().into(); - let mut inner = pair.into_inner(); - - let identifier = Identifier::parse(inner.next().unwrap())?; - - let types = inner.map(Type::parse).collect::, _>>()?; - - // REVIEW - // Convert types to record fields with auto-generated names (_0, _1, _2, ...) - let fields = types - .into_iter() - .enumerate() - .map(|(index, r#type)| RecordField { - name: Identifier::new(format!("_{}", index)), - r#type, - span: span.clone(), - }) - .collect(); - - Ok(Self { - name: identifier, - fields, - span, - }) - } - fn unit_case_parse(pair: pest::iterators::Pair) -> Result { let span = pair.as_span().into(); let mut inner = pair.into_inner(); @@ -1359,7 +1364,7 @@ impl AstNode for VariantCase { fn parse(pair: Pair) -> Result { let case = match pair.as_rule() { Rule::variant_case_struct => Self::struct_case_parse(pair), - Rule::variant_case_tuple => Self::tuple_case_parse(pair), + Rule::variant_case_tuple => todo!("parse variant case tuple"), Rule::variant_case_unit => Self::unit_case_parse(pair), x => unreachable!("Unexpected rule in datum_variant: {:?}", x), }?; @@ -1496,6 +1501,13 @@ mod tests { input_to_ast_check!(Type, "list", "List", Type::List(Box::new(Type::Int))); + input_to_ast_check!( + Type, + "tuple", + "Tuple", + Type::Tuple(Box::new(Type::Int), Box::new(Type::Bytes)) + ); + input_to_ast_check!( Type, "identifier", @@ -2402,4 +2414,6 @@ mod tests { test_parsing!(local_vars); test_parsing!(cardano_witness); + + test_parsing!(tuple); } diff --git a/crates/tx3-lang/src/tx3.pest b/crates/tx3-lang/src/tx3.pest index fb94efd9..f92a85a5 100644 --- a/crates/tx3-lang/src/tx3.pest +++ b/crates/tx3-lang/src/tx3.pest @@ -24,10 +24,12 @@ primitive_type = { custom_type = { identifier } list_type = { "List<" ~ type ~ ">" } +tuple_type = { "Tuple<" ~ type ~ "," ~ type ~ ">"} type = { primitive_type | list_type | + tuple_type | custom_type } @@ -141,6 +143,7 @@ data_expr = { data_prefix* ~ data_primary ~ data_postfix* ~ (data_infix ~ data_p string | struct_constructor | list_constructor | + tuple_constructor | any_asset_constructor | static_asset_constructor | identifier | @@ -180,6 +183,10 @@ list_constructor = { "[" ~ (data_expr ~ ",")* ~ data_expr? ~ "]" } +tuple_constructor = { + "(" ~ data_expr ~ "," ~ data_expr ~ ")" +} + address_expr = { identifier | hex_string | diff --git a/examples/tuple.ast b/examples/tuple.ast new file mode 100644 index 00000000..ba81e32c --- /dev/null +++ b/examples/tuple.ast @@ -0,0 +1,408 @@ +{ + "env": null, + "txs": [ + { + "name": { + "value": "transfer", + "span": { + "dummy": false, + "start": 73, + "end": 81 + } + }, + "parameters": { + "parameters": [ + { + "name": { + "value": "quantity", + "span": { + "dummy": false, + "start": 87, + "end": 95 + } + }, + "type": "Int" + } + ], + "span": { + "dummy": false, + "start": 81, + "end": 102 + } + }, + "locals": null, + "references": [], + "inputs": [ + { + "name": "source", + "is_many": false, + "fields": [ + { + "From": { + "Identifier": { + "value": "Sender", + "span": { + "dummy": false, + "start": 138, + "end": 144 + } + } + } + }, + { + "MinAmount": { + "StaticAssetConstructor": { + "type": { + "value": "Ada", + "span": { + "dummy": false, + "start": 166, + "end": 169 + } + }, + "amount": { + "Identifier": { + "value": "quantity", + "span": { + "dummy": false, + "start": 170, + "end": 178 + } + } + }, + "span": { + "dummy": false, + "start": 166, + "end": 179 + } + } + } + } + ], + "span": { + "dummy": false, + "start": 109, + "end": 186 + } + } + ], + "outputs": [ + { + "name": null, + "fields": [ + { + "To": { + "Identifier": { + "value": "Receiver", + "span": { + "dummy": false, + "start": 217, + "end": 225 + } + } + } + }, + { + "Amount": { + "StaticAssetConstructor": { + "type": { + "value": "Ada", + "span": { + "dummy": false, + "start": 243, + "end": 246 + } + }, + "amount": { + "Identifier": { + "value": "quantity", + "span": { + "dummy": false, + "start": 247, + "end": 255 + } + } + }, + "span": { + "dummy": false, + "start": 243, + "end": 256 + } + } + } + } + ], + "span": { + "dummy": false, + "start": 196, + "end": 263 + } + }, + { + "name": null, + "fields": [ + { + "To": { + "Identifier": { + "value": "Sender", + "span": { + "dummy": false, + "start": 290, + "end": 296 + } + } + } + }, + { + "Amount": { + "SubOp": { + "lhs": { + "SubOp": { + "lhs": { + "Identifier": { + "value": "source", + "span": { + "dummy": false, + "start": 314, + "end": 320 + } + } + }, + "rhs": { + "StaticAssetConstructor": { + "type": { + "value": "Ada", + "span": { + "dummy": false, + "start": 323, + "end": 326 + } + }, + "amount": { + "Identifier": { + "value": "quantity", + "span": { + "dummy": false, + "start": 327, + "end": 335 + } + } + }, + "span": { + "dummy": false, + "start": 323, + "end": 336 + } + } + }, + "span": { + "dummy": false, + "start": 321, + "end": 322 + } + } + }, + "rhs": { + "Identifier": { + "value": "fees", + "span": { + "dummy": false, + "start": 339, + "end": 343 + } + } + }, + "span": { + "dummy": false, + "start": 337, + "end": 338 + } + } + } + }, + { + "Datum": { + "StructConstructor": { + "type": { + "value": "Datum", + "span": { + "dummy": false, + "start": 360, + "end": 365 + } + }, + "case": { + "name": { + "value": "Default", + "span": { + "dummy": true, + "start": 0, + "end": 0 + } + }, + "fields": [ + { + "name": { + "value": "A", + "span": { + "dummy": false, + "start": 378, + "end": 379 + } + }, + "value": { + "TupleConstructor": { + "fst": { + "Number": 1 + }, + "snd": { + "Number": 2 + }, + "span": { + "dummy": false, + "start": 381, + "end": 386 + } + } + }, + "span": { + "dummy": false, + "start": 378, + "end": 386 + } + } + ], + "spread": null, + "span": { + "dummy": false, + "start": 366, + "end": 397 + } + }, + "span": { + "dummy": false, + "start": 360, + "end": 397 + } + } + } + } + ], + "span": { + "dummy": false, + "start": 269, + "end": 404 + } + } + ], + "validity": null, + "burn": null, + "mints": [], + "signers": null, + "adhoc": [], + "span": { + "dummy": false, + "start": 70, + "end": 406 + }, + "collateral": [], + "metadata": null + } + ], + "types": [ + { + "name": { + "value": "Datum", + "span": { + "dummy": false, + "start": 5, + "end": 10 + } + }, + "cases": [ + { + "name": { + "value": "Default", + "span": { + "dummy": true, + "start": 0, + "end": 0 + } + }, + "fields": [ + { + "name": { + "value": "A", + "span": { + "dummy": false, + "start": 15, + "end": 16 + } + }, + "type": { + "Tuple": [ + "Int", + "Int" + ] + }, + "span": { + "dummy": false, + "start": 15, + "end": 33 + } + } + ], + "span": { + "dummy": false, + "start": 0, + "end": 36 + } + } + ], + "span": { + "dummy": false, + "start": 0, + "end": 36 + } + } + ], + "assets": [], + "parties": [ + { + "name": { + "value": "Sender", + "span": { + "dummy": false, + "start": 44, + "end": 50 + } + }, + "span": { + "dummy": false, + "start": 38, + "end": 51 + } + }, + { + "name": { + "value": "Receiver", + "span": { + "dummy": false, + "start": 59, + "end": 67 + } + }, + "span": { + "dummy": false, + "start": 53, + "end": 68 + } + } + ], + "policies": [], + "span": { + "dummy": false, + "start": 0, + "end": 406 + } +} \ No newline at end of file diff --git a/examples/tuple.transfer.tir b/examples/tuple.transfer.tir new file mode 100644 index 00000000..fe3598ad --- /dev/null +++ b/examples/tuple.transfer.tir @@ -0,0 +1,172 @@ +{ + "fees": { + "EvalParam": "ExpectFees" + }, + "references": [], + "inputs": [ + { + "name": "source", + "query": { + "address": { + "EvalParam": { + "ExpectValue": [ + "sender", + "Address" + ] + } + }, + "min_amount": { + "Assets": [ + { + "policy": "None", + "asset_name": "None", + "amount": { + "EvalParam": { + "ExpectValue": [ + "quantity", + "Int" + ] + } + } + } + ] + }, + "ref": "None" + }, + "refs": [], + "redeemer": null, + "policy": null + } + ], + "outputs": [ + { + "address": { + "EvalParam": { + "ExpectValue": [ + "receiver", + "Address" + ] + } + }, + "datum": "None", + "amount": { + "Assets": [ + { + "policy": "None", + "asset_name": "None", + "amount": { + "EvalParam": { + "ExpectValue": [ + "quantity", + "Int" + ] + } + } + } + ] + } + }, + { + "address": { + "EvalParam": { + "ExpectValue": [ + "sender", + "Address" + ] + } + }, + "datum": { + "Struct": { + "constructor": 0, + "fields": [ + { + "Tuple": [ + { + "Number": 1 + }, + { + "Number": 2 + } + ] + } + ] + } + }, + "amount": { + "EvalBuiltIn": { + "Sub": [ + { + "EvalBuiltIn": { + "Sub": [ + { + "EvalCoerce": { + "IntoAssets": { + "EvalParam": { + "ExpectInput": [ + "source", + { + "address": { + "EvalParam": { + "ExpectValue": [ + "sender", + "Address" + ] + } + }, + "min_amount": { + "Assets": [ + { + "policy": "None", + "asset_name": "None", + "amount": { + "EvalParam": { + "ExpectValue": [ + "quantity", + "Int" + ] + } + } + } + ] + }, + "ref": "None" + } + ] + } + } + } + }, + { + "Assets": [ + { + "policy": "None", + "asset_name": "None", + "amount": { + "EvalParam": { + "ExpectValue": [ + "quantity", + "Int" + ] + } + } + } + ] + } + ] + } + }, + { + "EvalParam": "ExpectFees" + } + ] + } + } + } + ], + "validity": null, + "mints": [], + "adhoc": [], + "collateral": [], + "signers": null, + "metadata": [] +} \ No newline at end of file diff --git a/examples/tuple.tx3 b/examples/tuple.tx3 new file mode 100644 index 00000000..79bf6718 --- /dev/null +++ b/examples/tuple.tx3 @@ -0,0 +1,29 @@ +type Datum { + A: Tuple, +} + +party Sender; + +party Receiver; + +tx transfer( + quantity: Int +) { + input source { + from: Sender, + min_amount: Ada(quantity), + } + + output { + to: Receiver, + amount: Ada(quantity), + } + + output { + to: Sender, + amount: source - Ada(quantity) - fees, + datum: Datum { + A: (1,2), + }, + } +} \ No newline at end of file From 4ca2fe6ce0097f4185226dfcedcdaecd7379acbb Mon Sep 17 00:00:00 2001 From: Benjamin Martinez Picech Date: Wed, 2 Jul 2025 17:02:40 -0300 Subject: [PATCH 6/9] adding particular case for list of tuples --- crates/tx3-cardano/src/compile/mod.rs | 1 + crates/tx3-cardano/src/compile/plutus_data.rs | 39 +++++++++++++++++-- crates/tx3-lang/src/applying.rs | 2 +- 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/crates/tx3-cardano/src/compile/mod.rs b/crates/tx3-cardano/src/compile/mod.rs index bfc57958..a2e17a4e 100644 --- a/crates/tx3-cardano/src/compile/mod.rs +++ b/crates/tx3-cardano/src/compile/mod.rs @@ -79,6 +79,7 @@ fn compile_data_expr(ir: &ir::Expression) -> Result Ok(x.as_str().as_data()), ir::Expression::Struct(x) => compile_struct(x), ir::Expression::Address(x) => Ok(x.as_data()), + ir::Expression::Tuple(x) => Ok(x.try_as_data()?), _ => Err(Error::CoerceError( format!("{:?}", ir), "DataExpr".to_string(), diff --git a/crates/tx3-cardano/src/compile/plutus_data.rs b/crates/tx3-cardano/src/compile/plutus_data.rs index e6ce3731..98c24b82 100644 --- a/crates/tx3-cardano/src/compile/plutus_data.rs +++ b/crates/tx3-cardano/src/compile/plutus_data.rs @@ -1,4 +1,5 @@ pub use pallas::codec::utils::Int; +use pallas::codec::utils::KeyValuePairs; pub use pallas::ledger::primitives::{BigInt, BoundedBytes, Constr, MaybeIndefArray, PlutusData}; use tx3_lang::ir; @@ -90,12 +91,41 @@ impl IntoData for i128 { impl TryIntoData for Vec { fn try_as_data(&self) -> Result { - let items = self + let all_tuples = self .iter() - .map(TryIntoData::try_as_data) - .collect::, _>>()?; + .all(|expr| matches!(expr, ir::Expression::Tuple(_))); + + if all_tuples && !self.is_empty() { + let key_value_pairs = self + .iter() + .map(|expr| -> Result<(PlutusData, PlutusData), super::Error> { + if let ir::Expression::Tuple(tuple) = expr { + Ok((tuple.0.try_as_data()?, tuple.1.try_as_data()?)) + } else { + unreachable!("Already checked all are tuples") + } + }) + .collect::, _>>()?; + + Ok(PlutusData::Map(KeyValuePairs::Def(key_value_pairs))) + } else { + let items = self + .iter() + .map(TryIntoData::try_as_data) + .collect::, _>>()?; + + Ok(PlutusData::Array(MaybeIndefArray::Def(items))) + } + } +} - Ok(PlutusData::Array(MaybeIndefArray::Def(items))) +impl TryIntoData for (ir::Expression, ir::Expression) { + fn try_as_data(&self) -> Result { + let (fst, snd) = self; + Ok(PlutusData::Map(KeyValuePairs::Def(vec![( + fst.try_as_data()?, + snd.try_as_data()?, + )]))) } } @@ -135,6 +165,7 @@ impl TryIntoData for ir::Expression { ir::Expression::Address(x) => Ok(x.as_data()), ir::Expression::Hash(x) => Ok(x.as_data()), ir::Expression::List(x) => x.try_as_data(), + ir::Expression::Tuple(x) => x.try_as_data(), x => Err(super::Error::CoerceError( format!("{:?}", x), "PlutusData".to_string(), diff --git a/crates/tx3-lang/src/applying.rs b/crates/tx3-lang/src/applying.rs index 12804d55..9f1904c3 100644 --- a/crates/tx3-lang/src/applying.rs +++ b/crates/tx3-lang/src/applying.rs @@ -521,7 +521,7 @@ impl Composite for ir::Coerce { Self::NoOp(x) => Ok(Self::NoOp(x)), Self::IntoAssets(x) => Ok(Self::NoOp(x.into_assets()?)), Self::IntoDatum(x) => Ok(Self::NoOp(x.into_datum()?)), - Self::IntoScript(x) => todo!(), + Self::IntoScript(_x) => todo!(), } } } From cbda1b12350114871d2ad632c66066bdbac14eb3 Mon Sep 17 00:00:00 2001 From: Benjamin Martinez Picech Date: Thu, 10 Jul 2025 16:45:34 -0300 Subject: [PATCH 7/9] fix on plutus data TryIntoData for Vec --- crates/tx3-cardano/src/compile/plutus_data.rs | 34 ++++--------------- 1 file changed, 7 insertions(+), 27 deletions(-) diff --git a/crates/tx3-cardano/src/compile/plutus_data.rs b/crates/tx3-cardano/src/compile/plutus_data.rs index 98c24b82..72a010ef 100644 --- a/crates/tx3-cardano/src/compile/plutus_data.rs +++ b/crates/tx3-cardano/src/compile/plutus_data.rs @@ -1,5 +1,4 @@ pub use pallas::codec::utils::Int; -use pallas::codec::utils::KeyValuePairs; pub use pallas::ledger::primitives::{BigInt, BoundedBytes, Constr, MaybeIndefArray, PlutusData}; use tx3_lang::ir; @@ -91,41 +90,22 @@ impl IntoData for i128 { impl TryIntoData for Vec { fn try_as_data(&self) -> Result { - let all_tuples = self + let items = self .iter() - .all(|expr| matches!(expr, ir::Expression::Tuple(_))); - - if all_tuples && !self.is_empty() { - let key_value_pairs = self - .iter() - .map(|expr| -> Result<(PlutusData, PlutusData), super::Error> { - if let ir::Expression::Tuple(tuple) = expr { - Ok((tuple.0.try_as_data()?, tuple.1.try_as_data()?)) - } else { - unreachable!("Already checked all are tuples") - } - }) - .collect::, _>>()?; - - Ok(PlutusData::Map(KeyValuePairs::Def(key_value_pairs))) - } else { - let items = self - .iter() - .map(TryIntoData::try_as_data) - .collect::, _>>()?; - - Ok(PlutusData::Array(MaybeIndefArray::Def(items))) - } + .map(TryIntoData::try_as_data) + .collect::, _>>()?; + + Ok(PlutusData::Array(MaybeIndefArray::Def(items))) } } impl TryIntoData for (ir::Expression, ir::Expression) { fn try_as_data(&self) -> Result { let (fst, snd) = self; - Ok(PlutusData::Map(KeyValuePairs::Def(vec![( + Ok(PlutusData::Array(MaybeIndefArray::Def(vec![ fst.try_as_data()?, snd.try_as_data()?, - )]))) + ]))) } } From dd9e8126d25866ac689fa3fba585a9373b741d7e Mon Sep 17 00:00:00 2001 From: Benjamin Martinez Picech Date: Thu, 10 Jul 2025 16:52:00 -0300 Subject: [PATCH 8/9] example using parameter of the transfer --- examples/tuple.ast | 32 +++++++++++++------ examples/tuple.transfer.tir | 63 +++++++++++++++++++++++-------------- examples/tuple.tx3 | 2 +- 3 files changed, 63 insertions(+), 34 deletions(-) diff --git a/examples/tuple.ast b/examples/tuple.ast index ba81e32c..7da03046 100644 --- a/examples/tuple.ast +++ b/examples/tuple.ast @@ -253,22 +253,36 @@ "value": { "TupleConstructor": { "fst": { - "Number": 1 + "Identifier": { + "value": "quantity", + "span": { + "dummy": false, + "start": 382, + "end": 390 + } + } }, "snd": { - "Number": 2 + "Identifier": { + "value": "quantity", + "span": { + "dummy": false, + "start": 391, + "end": 399 + } + } }, "span": { "dummy": false, "start": 381, - "end": 386 + "end": 400 } } }, "span": { "dummy": false, "start": 378, - "end": 386 + "end": 400 } } ], @@ -276,13 +290,13 @@ "span": { "dummy": false, "start": 366, - "end": 397 + "end": 411 } }, "span": { "dummy": false, "start": 360, - "end": 397 + "end": 411 } } } @@ -291,7 +305,7 @@ "span": { "dummy": false, "start": 269, - "end": 404 + "end": 418 } } ], @@ -303,7 +317,7 @@ "span": { "dummy": false, "start": 70, - "end": 406 + "end": 420 }, "collateral": [], "metadata": null @@ -403,6 +417,6 @@ "span": { "dummy": false, "start": 0, - "end": 406 + "end": 420 } } \ No newline at end of file diff --git a/examples/tuple.transfer.tir b/examples/tuple.transfer.tir index fe3598ad..cf614e44 100644 --- a/examples/tuple.transfer.tir +++ b/examples/tuple.transfer.tir @@ -6,36 +6,41 @@ "inputs": [ { "name": "source", - "query": { - "address": { - "EvalParam": { - "ExpectValue": [ - "sender", - "Address" - ] - } - }, - "min_amount": { - "Assets": [ + "utxos": { + "EvalParam": { + "ExpectInput": [ + "source", { - "policy": "None", - "asset_name": "None", - "amount": { + "address": { "EvalParam": { "ExpectValue": [ - "quantity", - "Int" + "sender", + "Address" ] } - } + }, + "min_amount": { + "Assets": [ + { + "policy": "None", + "asset_name": "None", + "amount": { + "EvalParam": { + "ExpectValue": [ + "quantity", + "Int" + ] + } + } + } + ] + }, + "ref": "None" } ] - }, - "ref": "None" + } }, - "refs": [], - "redeemer": null, - "policy": null + "redeemer": "None" } ], "outputs": [ @@ -82,10 +87,20 @@ { "Tuple": [ { - "Number": 1 + "EvalParam": { + "ExpectValue": [ + "quantity", + "Int" + ] + } }, { - "Number": 2 + "EvalParam": { + "ExpectValue": [ + "quantity", + "Int" + ] + } } ] } diff --git a/examples/tuple.tx3 b/examples/tuple.tx3 index 79bf6718..bc3dd3e1 100644 --- a/examples/tuple.tx3 +++ b/examples/tuple.tx3 @@ -23,7 +23,7 @@ tx transfer( to: Sender, amount: source - Ada(quantity) - fees, datum: Datum { - A: (1,2), + A: (quantity,quantity), }, } } \ No newline at end of file From 7a80525984a522a0554ca9f00cb1b187e3911a49 Mon Sep 17 00:00:00 2001 From: Benjamin Martinez Picech Date: Mon, 14 Jul 2025 17:38:43 -0300 Subject: [PATCH 9/9] tuples as list on ir --- crates/tx3-cardano/src/compile/mod.rs | 1 - crates/tx3-cardano/src/compile/plutus_data.rs | 11 ----------- crates/tx3-lang/src/lowering.rs | 2 +- examples/tuple.transfer.tir | 2 +- 4 files changed, 2 insertions(+), 14 deletions(-) diff --git a/crates/tx3-cardano/src/compile/mod.rs b/crates/tx3-cardano/src/compile/mod.rs index 7df012cd..a0a6bf01 100644 --- a/crates/tx3-cardano/src/compile/mod.rs +++ b/crates/tx3-cardano/src/compile/mod.rs @@ -79,7 +79,6 @@ fn compile_data_expr(ir: &ir::Expression) -> Result Ok(x.as_str().as_data()), ir::Expression::Struct(x) => compile_struct(x), ir::Expression::Address(x) => Ok(x.as_data()), - ir::Expression::Tuple(x) => Ok(x.try_as_data()?), _ => Err(Error::CoerceError( format!("{:?}", ir), "DataExpr".to_string(), diff --git a/crates/tx3-cardano/src/compile/plutus_data.rs b/crates/tx3-cardano/src/compile/plutus_data.rs index 72a010ef..e6ce3731 100644 --- a/crates/tx3-cardano/src/compile/plutus_data.rs +++ b/crates/tx3-cardano/src/compile/plutus_data.rs @@ -99,16 +99,6 @@ impl TryIntoData for Vec { } } -impl TryIntoData for (ir::Expression, ir::Expression) { - fn try_as_data(&self) -> Result { - let (fst, snd) = self; - Ok(PlutusData::Array(MaybeIndefArray::Def(vec![ - fst.try_as_data()?, - snd.try_as_data()?, - ]))) - } -} - impl TryIntoData for ir::StructExpr { fn try_as_data(&self) -> Result { let fields = self @@ -145,7 +135,6 @@ impl TryIntoData for ir::Expression { ir::Expression::Address(x) => Ok(x.as_data()), ir::Expression::Hash(x) => Ok(x.as_data()), ir::Expression::List(x) => x.try_as_data(), - ir::Expression::Tuple(x) => x.try_as_data(), x => Err(super::Error::CoerceError( format!("{:?}", x), "PlutusData".to_string(), diff --git a/crates/tx3-lang/src/lowering.rs b/crates/tx3-lang/src/lowering.rs index 9cb8eb93..83eabb9c 100644 --- a/crates/tx3-lang/src/lowering.rs +++ b/crates/tx3-lang/src/lowering.rs @@ -418,7 +418,7 @@ impl IntoLower for ast::TupleConstructor { let fst = self.fst.into_lower(ctx)?; let snd = self.snd.into_lower(ctx)?; - Ok(ir::Expression::Tuple(Box::new((fst, snd)))) + Ok(ir::Expression::List(vec![fst, snd])) } } diff --git a/examples/tuple.transfer.tir b/examples/tuple.transfer.tir index cf614e44..88460b82 100644 --- a/examples/tuple.transfer.tir +++ b/examples/tuple.transfer.tir @@ -85,7 +85,7 @@ "constructor": 0, "fields": [ { - "Tuple": [ + "List": [ { "EvalParam": { "ExpectValue": [