diff --git a/crates/tx3-lang/src/analyzing.rs b/crates/tx3-lang/src/analyzing.rs index aa0ddbe5..18e5f2d5 100644 --- a/crates/tx3-lang/src/analyzing.rs +++ b/crates/tx3-lang/src/analyzing.rs @@ -534,11 +534,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), @@ -670,6 +681,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(), } } @@ -678,6 +690,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/applying.rs b/crates/tx3-lang/src/applying.rs index 0f238ff4..26eac31b 100644 --- a/crates/tx3-lang/src/applying.rs +++ b/crates/tx3-lang/src/applying.rs @@ -577,7 +577,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!(), } } } diff --git a/crates/tx3-lang/src/ast.rs b/crates/tx3-lang/src/ast.rs index 8b4cc045..832a8984 100644 --- a/crates/tx3-lang/src/ast.rs +++ b/crates/tx3-lang/src/ast.rs @@ -552,6 +552,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, @@ -694,6 +714,7 @@ pub enum DataExpr { HexString(HexStringLiteral), StructConstructor(StructConstructor), ListConstructor(ListConstructor), + TupleConstructor(TupleConstructor), StaticAssetConstructor(StaticAssetConstructor), AnyAssetConstructor(AnyAssetConstructor), Identifier(Identifier), @@ -724,6 +745,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::ConcatOp(x) => x.target_type(), @@ -765,6 +787,7 @@ pub enum Type { AnyAsset, List(Box), Custom(Identifier), + Tuple(Box, Box), } impl std::fmt::Display for Type { @@ -781,6 +804,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 2696cb1a..70a45a0b 100644 --- a/crates/tx3-lang/src/ir.rs +++ b/crates/tx3-lang/src/ir.rs @@ -181,6 +181,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 1b3e7097..3df432ac 100644 --- a/crates/tx3-lang/src/lowering.rs +++ b/crates/tx3-lang/src/lowering.rs @@ -330,6 +330,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), } } } @@ -423,6 +424,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::List(vec![fst, snd])) + } +} + impl IntoLower for ast::DataExpr { type Output = ir::Expression; @@ -435,6 +447,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()), @@ -896,6 +909,7 @@ mod tests { test_lowering!(local_vars); test_lowering!(cardano_witness); + test_lowering!(tuple); test_lowering!(burn); } diff --git a/crates/tx3-lang/src/parsing.rs b/crates/tx3-lang/src/parsing.rs index ca28adc5..db67b151 100644 --- a/crates/tx3-lang/src/parsing.rs +++ b/crates/tx3-lang/src/parsing.rs @@ -1048,6 +1048,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())) @@ -1150,6 +1172,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), @@ -1185,6 +1210,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(), @@ -1218,6 +1244,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), } @@ -1460,6 +1492,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", @@ -2385,5 +2424,6 @@ mod tests { test_parsing!(cardano_witness); + test_parsing!(tuple); test_parsing!(burn); } diff --git a/crates/tx3-lang/src/tx3.pest b/crates/tx3-lang/src/tx3.pest index bcb18e1e..b242940a 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 } @@ -145,6 +147,7 @@ data_expr = { data_prefix* ~ data_primary ~ data_postfix* ~ (data_infix ~ data_p string | struct_constructor | list_constructor | + tuple_constructor | concat_constructor | any_asset_constructor | static_asset_constructor | @@ -185,6 +188,10 @@ list_constructor = { "[" ~ (data_expr ~ ",")* ~ data_expr? ~ "]" } +tuple_constructor = { + "(" ~ data_expr ~ "," ~ data_expr ~ ")" +} + // input block input_many = { "*" } diff --git a/examples/tuple.ast b/examples/tuple.ast new file mode 100644 index 00000000..3b8dbfa8 --- /dev/null +++ b/examples/tuple.ast @@ -0,0 +1,422 @@ +{ + "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", + "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": { + "Identifier": { + "value": "quantity", + "span": { + "dummy": false, + "start": 382, + "end": 390 + } + } + }, + "snd": { + "Identifier": { + "value": "quantity", + "span": { + "dummy": false, + "start": 391, + "end": 399 + } + } + }, + "span": { + "dummy": false, + "start": 381, + "end": 400 + } + } + }, + "span": { + "dummy": false, + "start": 378, + "end": 400 + } + } + ], + "spread": null, + "span": { + "dummy": false, + "start": 366, + "end": 411 + } + }, + "span": { + "dummy": false, + "start": 360, + "end": 411 + } + } + } + } + ], + "span": { + "dummy": false, + "start": 269, + "end": 418 + } + } + ], + "validity": null, + "mints": [], + "burns": [], + "signers": null, + "adhoc": [], + "span": { + "dummy": false, + "start": 70, + "end": 420 + }, + "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": 420 + } +} \ No newline at end of file diff --git a/examples/tuple.transfer.tir b/examples/tuple.transfer.tir new file mode 100644 index 00000000..35db8f6a --- /dev/null +++ b/examples/tuple.transfer.tir @@ -0,0 +1,192 @@ +{ + "fees": { + "EvalParam": "ExpectFees" + }, + "references": [], + "inputs": [ + { + "name": "source", + "utxos": { + "EvalParam": { + "ExpectInput": [ + "source", + { + "address": { + "EvalParam": { + "ExpectValue": [ + "sender", + "Address" + ] + } + }, + "min_amount": { + "Assets": [ + { + "policy": "None", + "asset_name": "None", + "amount": { + "EvalParam": { + "ExpectValue": [ + "quantity", + "Int" + ] + } + } + } + ] + }, + "ref": "None", + "many": false, + "collateral": false + } + ] + } + }, + "redeemer": "None" + } + ], + "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": [ + { + "List": [ + { + "EvalParam": { + "ExpectValue": [ + "quantity", + "Int" + ] + } + }, + { + "EvalParam": { + "ExpectValue": [ + "quantity", + "Int" + ] + } + } + ] + } + ] + } + }, + "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", + "many": false, + "collateral": false + } + ] + } + } + } + }, + { + "Assets": [ + { + "policy": "None", + "asset_name": "None", + "amount": { + "EvalParam": { + "ExpectValue": [ + "quantity", + "Int" + ] + } + } + } + ] + } + ] + } + }, + { + "EvalParam": "ExpectFees" + } + ] + } + } + } + ], + "validity": null, + "mints": [], + "burns": [], + "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..bc3dd3e1 --- /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: (quantity,quantity), + }, + } +} \ No newline at end of file