From 769d503172874e0458eb3f0bcb8d694a9547512d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ludue=C3=B1a?= Date: Mon, 19 Jan 2026 17:44:48 -0300 Subject: [PATCH 01/14] feat: parsing index kept --- crates/tx3-lang/src/ast.rs | 1 + crates/tx3-lang/src/cardano.rs | 10 +++++++++- crates/tx3-lang/src/lowering.rs | 1 + crates/tx3-lang/src/parsing.rs | 22 ++++++++++++++++++++-- crates/tx3-tir/src/model/v1beta0.rs | 2 ++ crates/tx3-tir/src/reduce/mod.rs | 1 + 6 files changed, 34 insertions(+), 3 deletions(-) diff --git a/crates/tx3-lang/src/ast.rs b/crates/tx3-lang/src/ast.rs index 54578928..ca294dd7 100644 --- a/crates/tx3-lang/src/ast.rs +++ b/crates/tx3-lang/src/ast.rs @@ -404,6 +404,7 @@ pub struct OutputBlock { pub optional: bool, pub fields: Vec, pub span: Span, + pub declared_index: Option, } impl OutputBlock { diff --git a/crates/tx3-lang/src/cardano.rs b/crates/tx3-lang/src/cardano.rs index 036ab79f..10311349 100644 --- a/crates/tx3-lang/src/cardano.rs +++ b/crates/tx3-lang/src/cardano.rs @@ -586,6 +586,7 @@ pub struct CardanoPublishBlock { pub name: Option, pub fields: Vec, pub span: Span, + pub declared_index: Option, } impl CardanoPublishBlock { @@ -663,7 +664,12 @@ impl AstNode for CardanoPublishBlock { .map(|x| CardanoPublishBlockField::parse(x)) .collect::, _>>()?; - Ok(CardanoPublishBlock { name, fields, span }) + Ok(CardanoPublishBlock { + name, + fields, + span, + declared_index: None, + }) } fn span(&self) -> &Span { @@ -938,6 +944,7 @@ mod tests { ))), ], span: Span::DUMMY, + declared_index: None, } ); @@ -967,6 +974,7 @@ mod tests { ))), ], span: Span::DUMMY, + declared_index: None, } ); diff --git a/crates/tx3-lang/src/lowering.rs b/crates/tx3-lang/src/lowering.rs index a49dfe79..2e460528 100644 --- a/crates/tx3-lang/src/lowering.rs +++ b/crates/tx3-lang/src/lowering.rs @@ -703,6 +703,7 @@ impl IntoLower for ast::OutputBlock { datum, amount, optional: self.optional, + declared_index: self.declared_index.map(|n| n as u32), }) } } diff --git a/crates/tx3-lang/src/parsing.rs b/crates/tx3-lang/src/parsing.rs index 2b34fc1c..a27bf966 100644 --- a/crates/tx3-lang/src/parsing.rs +++ b/crates/tx3-lang/src/parsing.rs @@ -208,16 +208,32 @@ impl AstNode for TxDef { let mut signers = None; let mut metadata = None; + let mut declared_index: usize = 0; + for item in inner { match item.as_rule() { Rule::locals_block => locals = Some(LocalsBlock::parse(item)?), Rule::reference_block => references.push(ReferenceBlock::parse(item)?), Rule::input_block => inputs.push(InputBlock::parse(item)?), - Rule::output_block => outputs.push(OutputBlock::parse(item)?), + Rule::output_block => { + let mut ob = OutputBlock::parse(item)?; + ob.declared_index = Some(declared_index); + declared_index += 1; + outputs.push(ob); + } Rule::validity_block => validity = Some(ValidityBlock::parse(item)?), Rule::mint_block => mints.push(MintBlock::parse(item)?), Rule::burn_block => burns.push(MintBlock::parse(item)?), - Rule::chain_specific_block => adhoc.push(ChainSpecificBlock::parse(item)?), + Rule::chain_specific_block => { + let mut csb = ChainSpecificBlock::parse(item)?; + let ChainSpecificBlock::Cardano(cardano_block) = &mut csb; + if let crate::cardano::CardanoBlock::Publish(pb) = cardano_block { + pb.declared_index = Some(declared_index); + declared_index += 1; + } + + adhoc.push(csb); + } Rule::collateral_block => collateral.push(CollateralBlock::parse(item)?), Rule::signers_block => signers = Some(SignersBlock::parse(item)?), Rule::metadata_block => metadata = Some(MetadataBlock::parse(item)?), @@ -617,6 +633,7 @@ impl AstNode for OutputBlock { optional, fields, span, + declared_index: None, }) } @@ -2464,6 +2481,7 @@ mod tests { }))), ], span: Span::DUMMY, + declared_index: None, } ); diff --git a/crates/tx3-tir/src/model/v1beta0.rs b/crates/tx3-tir/src/model/v1beta0.rs index 1d0ad338..61dd908f 100644 --- a/crates/tx3-tir/src/model/v1beta0.rs +++ b/crates/tx3-tir/src/model/v1beta0.rs @@ -305,6 +305,7 @@ pub struct Output { pub datum: Expression, pub amount: Expression, pub optional: bool, + pub declared_index: Option, } #[derive(Serialize, Deserialize, Debug, Clone)] @@ -522,6 +523,7 @@ impl Node for Output { datum: self.datum.apply(visitor)?, amount: self.amount.apply(visitor)?, optional: self.optional, + declared_index: self.declared_index, }; Ok(visited) diff --git a/crates/tx3-tir/src/reduce/mod.rs b/crates/tx3-tir/src/reduce/mod.rs index eb60cf4c..15617dca 100644 --- a/crates/tx3-tir/src/reduce/mod.rs +++ b/crates/tx3-tir/src/reduce/mod.rs @@ -1159,6 +1159,7 @@ impl Composite for Output { datum: f(self.datum)?, amount: f(self.amount)?, optional: self.optional, + declared_index: self.declared_index, }) } } From 967e0139dd27ce044b7f9bc0b817206b5e0b518f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ludue=C3=B1a?= Date: Mon, 19 Jan 2026 18:16:59 -0300 Subject: [PATCH 02/14] feat: keep declared index when lowering --- crates/tx3-lang/src/cardano.rs | 11 ++++++++++- crates/tx3-lang/src/lowering.rs | 2 +- crates/tx3-tir/src/model/v1beta0.rs | 2 +- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/crates/tx3-lang/src/cardano.rs b/crates/tx3-lang/src/cardano.rs index 10311349..8bc6a096 100644 --- a/crates/tx3-lang/src/cardano.rs +++ b/crates/tx3-lang/src/cardano.rs @@ -733,12 +733,21 @@ impl IntoLower for CardanoPublishBlock { &self, ctx: &crate::lowering::Context, ) -> Result { - let data = self + let mut data: HashMap = self .fields .iter() .map(|x| x.into_lower(ctx)) .collect::>()?; + data.insert( + "declared_index".into(), + ir::Expression::Number( + self.declared_index + .map(|x| x as i128) + .expect("Publish block must have a declaration index"), + ), + ); + Ok(ir::AdHocDirective { name: "cardano_publish".to_string(), data, diff --git a/crates/tx3-lang/src/lowering.rs b/crates/tx3-lang/src/lowering.rs index 2e460528..e4dd42da 100644 --- a/crates/tx3-lang/src/lowering.rs +++ b/crates/tx3-lang/src/lowering.rs @@ -703,7 +703,7 @@ impl IntoLower for ast::OutputBlock { datum, amount, optional: self.optional, - declared_index: self.declared_index.map(|n| n as u32), + declared_index: self.declared_index, }) } } diff --git a/crates/tx3-tir/src/model/v1beta0.rs b/crates/tx3-tir/src/model/v1beta0.rs index 61dd908f..5d8300a8 100644 --- a/crates/tx3-tir/src/model/v1beta0.rs +++ b/crates/tx3-tir/src/model/v1beta0.rs @@ -305,7 +305,7 @@ pub struct Output { pub datum: Expression, pub amount: Expression, pub optional: bool, - pub declared_index: Option, + pub declared_index: Option, } #[derive(Serialize, Deserialize, Debug, Clone)] From ce63297588cb7263f2347c43a41dc767634c99ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ludue=C3=B1a?= Date: Mon, 19 Jan 2026 19:30:02 -0300 Subject: [PATCH 03/14] feat: compile with index --- crates/tx3-cardano/src/compile/mod.rs | 34 +++++++++++++++++++-------- crates/tx3-lang/src/cardano.rs | 2 +- crates/tx3-lang/src/lowering.rs | 3 ++- crates/tx3-tir/src/model/v1beta0.rs | 2 +- 4 files changed, 28 insertions(+), 13 deletions(-) diff --git a/crates/tx3-cardano/src/compile/mod.rs b/crates/tx3-cardano/src/compile/mod.rs index 7b8a86e0..bc3a54a2 100644 --- a/crates/tx3-cardano/src/compile/mod.rs +++ b/crates/tx3-cardano/src/compile/mod.rs @@ -288,24 +288,38 @@ fn compile_outputs( tx: &tir::Tx, network: Network, ) -> Result>, Error> { - let mut resolved: Vec<_> = tx - .outputs - .iter() - .map(|out| (out.optional, compile_output_block(out, network))) - .filter(|(optional, output)| !optional || output_has_assets(output)) - .map(|(_, output)| output) - .collect::, _>>()?; + let outputs = tx.outputs.iter().filter_map(|out| { + let compiled = compile_output_block(out, network); + + if out.optional && !output_has_assets(&compiled) { + return None; + } + + let idx = out.declared_index.as_number().map(|n| n as usize); + Some(compiled.map(|o| (idx, o))) + }); let cardano_outputs = tx .adhoc .iter() .filter(|x| x.name.as_str() == "cardano_publish") - .map(|adhoc| compile_cardano_publish_directive(adhoc, network)) + .map(|adhoc| { + let idx = adhoc + .data + .get("declared_index") + .and_then(|expr| expr.as_number()) + .map(|n| n as usize); + + compile_cardano_publish_directive(adhoc, network).map(|o| (idx, o)) + }); + + let mut all_outputs: Vec<_> = outputs + .chain(cardano_outputs) .collect::, _>>()?; - resolved.extend(cardano_outputs); + all_outputs.sort_by_key(|(idx, _)| idx.unwrap_or(usize::MAX)); - Ok(resolved) + Ok(all_outputs.into_iter().map(|(_, out)| out).collect()) } pub fn compile_cardano_publish_directive( diff --git a/crates/tx3-lang/src/cardano.rs b/crates/tx3-lang/src/cardano.rs index 8bc6a096..486eee22 100644 --- a/crates/tx3-lang/src/cardano.rs +++ b/crates/tx3-lang/src/cardano.rs @@ -740,7 +740,7 @@ impl IntoLower for CardanoPublishBlock { .collect::>()?; data.insert( - "declared_index".into(), + "declared_index".to_string(), ir::Expression::Number( self.declared_index .map(|x| x as i128) diff --git a/crates/tx3-lang/src/lowering.rs b/crates/tx3-lang/src/lowering.rs index e4dd42da..b5c97d61 100644 --- a/crates/tx3-lang/src/lowering.rs +++ b/crates/tx3-lang/src/lowering.rs @@ -697,13 +697,14 @@ impl IntoLower for ast::OutputBlock { let address = self.find("to").into_lower(ctx)?.unwrap_or_default(); let datum = self.find("datum").into_lower(ctx)?.unwrap_or_default(); let amount = self.find("amount").into_lower(ctx)?.unwrap_or_default(); + let declared_ix = self.declared_index.unwrap(); Ok(ir::Output { address, datum, amount, optional: self.optional, - declared_index: self.declared_index, + declared_index: ir::Expression::Number(declared_ix as i128), }) } } diff --git a/crates/tx3-tir/src/model/v1beta0.rs b/crates/tx3-tir/src/model/v1beta0.rs index 5d8300a8..9ca650bd 100644 --- a/crates/tx3-tir/src/model/v1beta0.rs +++ b/crates/tx3-tir/src/model/v1beta0.rs @@ -305,7 +305,7 @@ pub struct Output { pub datum: Expression, pub amount: Expression, pub optional: bool, - pub declared_index: Option, + pub declared_index: Expression, } #[derive(Serialize, Deserialize, Debug, Clone)] From 17f92b5f96bd2c29c57c913e6fdfc5f8f351176d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ludue=C3=B1a?= Date: Mon, 19 Jan 2026 19:31:51 -0300 Subject: [PATCH 04/14] feat: tests include declared_index --- examples/asteria.ast | 6 ++-- examples/asteria.move_ship.tir | 10 +++++-- examples/buidler_fest_2026.ast | 9 ++++-- examples/buidler_fest_2026.buy_ticket.tir | 15 ++++++++-- examples/burn.ast | 3 +- examples/burn.burn_stuff.tir | 5 +++- examples/cardano_witness.ast | 6 ++-- ...ardano_witness.mint_from_native_script.tir | 5 +++- examples/cardano_witness.mint_from_plutus.tir | 5 +++- examples/disordered.ast | 6 ++-- examples/donation.ast | 3 +- examples/donation.mint_from_plutus.tir | 5 +++- examples/env_vars.ast | 3 +- examples/env_vars.mint_from_env.tir | 5 +++- examples/faucet.ast | 3 +- examples/faucet.claim_with_password.tir | 5 +++- examples/input_datum.ast | 3 +- examples/input_datum.increase_counter.tir | 5 +++- examples/lang_tour.ast | 3 +- examples/lang_tour.my_tx.tir | 23 ++++++++------- examples/list_concat.ast | 3 +- examples/list_concat.concat_list.tir | 5 +++- examples/local_vars.ast | 3 +- examples/local_vars.mint_from_local.tir | 5 +++- examples/map.ast | 6 ++-- examples/map.transfer.tir | 10 +++++-- examples/min_utxo.transfer_min.tir | 10 +++++-- examples/reference_script.ast | 12 +++++--- examples/reference_script.publish_native.tir | 26 ++++++++++------- examples/reference_script.publish_plutus.tir | 28 +++++++++++-------- examples/swap.ast | 6 ++-- examples/swap.swap.tir | 10 +++++-- examples/transfer.ast | 6 ++-- examples/transfer.transfer.tir | 10 +++++-- examples/vesting.ast | 9 ++++-- examples/vesting.lock.tir | 10 +++++-- examples/vesting.unlock.tir | 5 +++- examples/withdrawal.ast | 6 ++-- examples/withdrawal.transfer.tir | 10 +++++-- 39 files changed, 218 insertions(+), 90 deletions(-) diff --git a/examples/asteria.ast b/examples/asteria.ast index 4b556cb1..9cdda1d4 100644 --- a/examples/asteria.ast +++ b/examples/asteria.ast @@ -597,7 +597,8 @@ "dummy": false, "start": 1158, "end": 1379 - } + }, + "declared_index": 0 }, { "name": null, @@ -651,7 +652,8 @@ "dummy": false, "start": 1385, "end": 1449 - } + }, + "declared_index": 1 } ], "validity": null, diff --git a/examples/asteria.move_ship.tir b/examples/asteria.move_ship.tir index b3cf5322..11d1815e 100644 --- a/examples/asteria.move_ship.tir +++ b/examples/asteria.move_ship.tir @@ -1168,7 +1168,10 @@ ] } }, - "optional": false + "optional": false, + "declared_index": { + "Number": 0 + } }, { "address": { @@ -1262,7 +1265,10 @@ ] } }, - "optional": false + "optional": false, + "declared_index": { + "Number": 1 + } } ], "validity": null, diff --git a/examples/buidler_fest_2026.ast b/examples/buidler_fest_2026.ast index c0c0f570..53cf2bab 100644 --- a/examples/buidler_fest_2026.ast +++ b/examples/buidler_fest_2026.ast @@ -537,7 +537,8 @@ "dummy": false, "start": 958, "end": 1063 - } + }, + "declared_index": 0 }, { "name": { @@ -672,7 +673,8 @@ "dummy": false, "start": 1069, "end": 1246 - } + }, + "declared_index": 1 }, { "name": { @@ -733,7 +735,8 @@ "dummy": false, "start": 1252, "end": 1331 - } + }, + "declared_index": 2 } ], "validity": { diff --git a/examples/buidler_fest_2026.buy_ticket.tir b/examples/buidler_fest_2026.buy_ticket.tir index 28354443..e5133d3a 100644 --- a/examples/buidler_fest_2026.buy_ticket.tir +++ b/examples/buidler_fest_2026.buy_ticket.tir @@ -324,7 +324,10 @@ ] } }, - "optional": false + "optional": false, + "declared_index": { + "Number": 0 + } }, { "address": { @@ -458,7 +461,10 @@ } } }, - "optional": false + "optional": false, + "declared_index": { + "Number": 1 + } }, { "address": { @@ -486,7 +492,10 @@ } ] }, - "optional": false + "optional": false, + "declared_index": { + "Number": 2 + } } ], "validity": { diff --git a/examples/burn.ast b/examples/burn.ast index 6eafd087..b181ae69 100644 --- a/examples/burn.ast +++ b/examples/burn.ast @@ -222,7 +222,8 @@ "dummy": false, "start": 337, "end": 412 - } + }, + "declared_index": 0 } ], "validity": null, diff --git a/examples/burn.burn_stuff.tir b/examples/burn.burn_stuff.tir index 3a8a4e89..897dfd9e 100644 --- a/examples/burn.burn_stuff.tir +++ b/examples/burn.burn_stuff.tir @@ -244,7 +244,10 @@ ] } }, - "optional": false + "optional": false, + "declared_index": { + "Number": 0 + } } ], "validity": null, diff --git a/examples/cardano_witness.ast b/examples/cardano_witness.ast index 9136d18d..28f753d1 100644 --- a/examples/cardano_witness.ast +++ b/examples/cardano_witness.ast @@ -203,7 +203,8 @@ "dummy": false, "start": 404, "end": 481 - } + }, + "declared_index": 0 } ], "validity": null, @@ -525,7 +526,8 @@ "dummy": false, "start": 972, "end": 1049 - } + }, + "declared_index": 0 } ], "validity": null, diff --git a/examples/cardano_witness.mint_from_native_script.tir b/examples/cardano_witness.mint_from_native_script.tir index 95e1c83e..da5381cc 100644 --- a/examples/cardano_witness.mint_from_native_script.tir +++ b/examples/cardano_witness.mint_from_native_script.tir @@ -134,7 +134,10 @@ ] } }, - "optional": false + "optional": false, + "declared_index": { + "Number": 0 + } } ], "validity": null, diff --git a/examples/cardano_witness.mint_from_plutus.tir b/examples/cardano_witness.mint_from_plutus.tir index c0f4530f..b3a6bb4e 100644 --- a/examples/cardano_witness.mint_from_plutus.tir +++ b/examples/cardano_witness.mint_from_plutus.tir @@ -134,7 +134,10 @@ ] } }, - "optional": false + "optional": false, + "declared_index": { + "Number": 0 + } } ], "validity": null, diff --git a/examples/disordered.ast b/examples/disordered.ast index de9c48f3..42e4b4e1 100644 --- a/examples/disordered.ast +++ b/examples/disordered.ast @@ -160,7 +160,8 @@ "dummy": false, "start": 58, "end": 125 - } + }, + "declared_index": 0 }, { "name": null, @@ -252,7 +253,8 @@ "dummy": false, "start": 214, "end": 295 - } + }, + "declared_index": 1 } ], "validity": null, diff --git a/examples/donation.ast b/examples/donation.ast index f82e5c24..3e763f5c 100644 --- a/examples/donation.ast +++ b/examples/donation.ast @@ -267,7 +267,8 @@ "dummy": false, "start": 187, "end": 286 - } + }, + "declared_index": 0 } ], "validity": null, diff --git a/examples/donation.mint_from_plutus.tir b/examples/donation.mint_from_plutus.tir index 4aacfdf6..0305adae 100644 --- a/examples/donation.mint_from_plutus.tir +++ b/examples/donation.mint_from_plutus.tir @@ -198,7 +198,10 @@ ] } }, - "optional": false + "optional": false, + "declared_index": { + "Number": 0 + } } ], "validity": null, diff --git a/examples/env_vars.ast b/examples/env_vars.ast index cef2b474..24ad5171 100644 --- a/examples/env_vars.ast +++ b/examples/env_vars.ast @@ -170,7 +170,8 @@ "dummy": false, "start": 343, "end": 426 - } + }, + "declared_index": 0 } ], "validity": null, diff --git a/examples/env_vars.mint_from_env.tir b/examples/env_vars.mint_from_env.tir index b2b72adb..f1871695 100644 --- a/examples/env_vars.mint_from_env.tir +++ b/examples/env_vars.mint_from_env.tir @@ -72,7 +72,10 @@ } ] }, - "optional": false + "optional": false, + "declared_index": { + "Number": 0 + } } ], "validity": null, diff --git a/examples/faucet.ast b/examples/faucet.ast index e961c1d4..13a88f6d 100644 --- a/examples/faucet.ast +++ b/examples/faucet.ast @@ -171,7 +171,8 @@ "dummy": false, "start": 429, "end": 523 - } + }, + "declared_index": 0 } ], "validity": null, diff --git a/examples/faucet.claim_with_password.tir b/examples/faucet.claim_with_password.tir index d1f3874b..a669e3f6 100644 --- a/examples/faucet.claim_with_password.tir +++ b/examples/faucet.claim_with_password.tir @@ -134,7 +134,10 @@ ] } }, - "optional": false + "optional": false, + "declared_index": { + "Number": 0 + } } ], "validity": null, diff --git a/examples/input_datum.ast b/examples/input_datum.ast index c0cb2672..e1070141 100644 --- a/examples/input_datum.ast +++ b/examples/input_datum.ast @@ -259,7 +259,8 @@ "dummy": false, "start": 208, "end": 397 - } + }, + "declared_index": 0 } ], "validity": null, diff --git a/examples/input_datum.increase_counter.tir b/examples/input_datum.increase_counter.tir index f46c0176..f5668f6d 100644 --- a/examples/input_datum.increase_counter.tir +++ b/examples/input_datum.increase_counter.tir @@ -166,7 +166,10 @@ ] } }, - "optional": false + "optional": false, + "declared_index": { + "Number": 0 + } } ], "validity": null, diff --git a/examples/lang_tour.ast b/examples/lang_tour.ast index cfe0e4cd..b5b0597b 100644 --- a/examples/lang_tour.ast +++ b/examples/lang_tour.ast @@ -845,7 +845,8 @@ "dummy": false, "start": 1243, "end": 1622 - } + }, + "declared_index": 0 } ], "validity": { diff --git a/examples/lang_tour.my_tx.tir b/examples/lang_tour.my_tx.tir index 525dcb75..5e5ee84e 100644 --- a/examples/lang_tour.my_tx.tir +++ b/examples/lang_tour.my_tx.tir @@ -534,7 +534,10 @@ ] } }, - "optional": false + "optional": false, + "declared_index": { + "Number": 0 + } } ], "validity": { @@ -669,6 +672,12 @@ "amount": { "Number": 100 }, + "redeemer": { + "Struct": { + "constructor": 0, + "fields": [] + } + }, "credential": { "EvalParam": { "ExpectValue": [ @@ -676,18 +685,15 @@ "Address" ] } - }, - "redeemer": { - "Struct": { - "constructor": 0, - "fields": [] - } } } }, { "name": "plutus_witness", "data": { + "version": { + "Number": 2 + }, "script": { "Bytes": [ 171, @@ -696,9 +702,6 @@ 18, 52 ] - }, - "version": { - "Number": 2 } } }, diff --git a/examples/list_concat.ast b/examples/list_concat.ast index f8b6c17f..49c7f67d 100644 --- a/examples/list_concat.ast +++ b/examples/list_concat.ast @@ -214,7 +214,8 @@ "dummy": false, "start": 156, "end": 303 - } + }, + "declared_index": 0 } ], "validity": null, diff --git a/examples/list_concat.concat_list.tir b/examples/list_concat.concat_list.tir index 2c78d058..1bd9a505 100644 --- a/examples/list_concat.concat_list.tir +++ b/examples/list_concat.concat_list.tir @@ -128,7 +128,10 @@ ] } }, - "optional": false + "optional": false, + "declared_index": { + "Number": 0 + } } ], "validity": null, diff --git a/examples/local_vars.ast b/examples/local_vars.ast index 432bb4ff..aa1f690b 100644 --- a/examples/local_vars.ast +++ b/examples/local_vars.ast @@ -195,7 +195,8 @@ "dummy": false, "start": 302, "end": 366 - } + }, + "declared_index": 0 } ], "validity": null, diff --git a/examples/local_vars.mint_from_local.tir b/examples/local_vars.mint_from_local.tir index 03853938..ea7efbdc 100644 --- a/examples/local_vars.mint_from_local.tir +++ b/examples/local_vars.mint_from_local.tir @@ -77,7 +77,10 @@ ] } }, - "optional": false + "optional": false, + "declared_index": { + "Number": 0 + } } ], "validity": null, diff --git a/examples/map.ast b/examples/map.ast index 148694e0..525147e4 100644 --- a/examples/map.ast +++ b/examples/map.ast @@ -141,7 +141,8 @@ "dummy": false, "start": 190, "end": 257 - } + }, + "declared_index": 0 }, { "name": null, @@ -336,7 +337,8 @@ "dummy": false, "start": 263, "end": 418 - } + }, + "declared_index": 1 } ], "validity": null, diff --git a/examples/map.transfer.tir b/examples/map.transfer.tir index 99a17871..0fd8f133 100644 --- a/examples/map.transfer.tir +++ b/examples/map.transfer.tir @@ -72,7 +72,10 @@ } ] }, - "optional": false + "optional": false, + "declared_index": { + "Number": 0 + } }, { "address": { @@ -191,7 +194,10 @@ ] } }, - "optional": false + "optional": false, + "declared_index": { + "Number": 1 + } } ], "validity": null, diff --git a/examples/min_utxo.transfer_min.tir b/examples/min_utxo.transfer_min.tir index 8e5d4184..06f62871 100644 --- a/examples/min_utxo.transfer_min.tir +++ b/examples/min_utxo.transfer_min.tir @@ -76,7 +76,10 @@ } } }, - "optional": false + "optional": false, + "declared_index": { + "Number": 0 + } }, { "address": { @@ -163,7 +166,10 @@ ] } }, - "optional": false + "optional": false, + "declared_index": { + "Number": 1 + } } ], "validity": null, diff --git a/examples/reference_script.ast b/examples/reference_script.ast index ccb5f6db..61a70385 100644 --- a/examples/reference_script.ast +++ b/examples/reference_script.ast @@ -179,7 +179,8 @@ "dummy": false, "start": 323, "end": 404 - } + }, + "declared_index": 1 } ], "validity": null, @@ -257,7 +258,8 @@ "dummy": false, "start": 173, "end": 317 - } + }, + "declared_index": 0 } } } @@ -448,7 +450,8 @@ "dummy": false, "start": 675, "end": 756 - } + }, + "declared_index": 1 } ], "validity": null, @@ -526,7 +529,8 @@ "dummy": false, "start": 549, "end": 669 - } + }, + "declared_index": 0 } } } diff --git a/examples/reference_script.publish_native.tir b/examples/reference_script.publish_native.tir index 90fdd3b9..6438229c 100644 --- a/examples/reference_script.publish_native.tir +++ b/examples/reference_script.publish_native.tir @@ -127,7 +127,10 @@ ] } }, - "optional": false + "optional": false, + "declared_index": { + "Number": 1 + } } ], "validity": null, @@ -137,6 +140,17 @@ { "name": "cardano_publish", "data": { + "version": { + "Number": 0 + }, + "to": { + "EvalParam": { + "ExpectValue": [ + "receiver", + "Address" + ] + } + }, "script": { "Bytes": [ 130, @@ -163,15 +177,7 @@ } ] }, - "to": { - "EvalParam": { - "ExpectValue": [ - "receiver", - "Address" - ] - } - }, - "version": { + "declared_index": { "Number": 0 } } diff --git a/examples/reference_script.publish_plutus.tir b/examples/reference_script.publish_plutus.tir index 7506c0f5..2f757b60 100644 --- a/examples/reference_script.publish_plutus.tir +++ b/examples/reference_script.publish_plutus.tir @@ -127,7 +127,10 @@ ] } }, - "optional": false + "optional": false, + "declared_index": { + "Number": 1 + } } ], "validity": null, @@ -137,9 +140,6 @@ { "name": "cardano_publish", "data": { - "version": { - "Number": 3 - }, "amount": { "Assets": [ { @@ -156,6 +156,17 @@ } ] }, + "version": { + "Number": 3 + }, + "to": { + "EvalParam": { + "ExpectValue": [ + "receiver", + "Address" + ] + } + }, "script": { "Bytes": [ 81, @@ -178,13 +189,8 @@ 105 ] }, - "to": { - "EvalParam": { - "ExpectValue": [ - "receiver", - "Address" - ] - } + "declared_index": { + "Number": 0 } } } diff --git a/examples/swap.ast b/examples/swap.ast index 8d915d0f..57a28d15 100644 --- a/examples/swap.ast +++ b/examples/swap.ast @@ -465,7 +465,8 @@ "dummy": false, "start": 479, "end": 682 - } + }, + "declared_index": 0 }, { "name": null, @@ -557,7 +558,8 @@ "dummy": false, "start": 688, "end": 765 - } + }, + "declared_index": 1 } ], "validity": null, diff --git a/examples/swap.swap.tir b/examples/swap.swap.tir index c9a20a39..a4a33b99 100644 --- a/examples/swap.swap.tir +++ b/examples/swap.swap.tir @@ -249,7 +249,10 @@ } } }, - "optional": false + "optional": false, + "declared_index": { + "Number": 0 + } }, { "address": { @@ -339,7 +342,10 @@ ] } }, - "optional": false + "optional": false, + "declared_index": { + "Number": 1 + } } ], "validity": null, diff --git a/examples/transfer.ast b/examples/transfer.ast index a21a5413..4cf90a6a 100644 --- a/examples/transfer.ast +++ b/examples/transfer.ast @@ -141,7 +141,8 @@ "dummy": false, "start": 159, "end": 226 - } + }, + "declared_index": 0 }, { "name": null, @@ -233,7 +234,8 @@ "dummy": false, "start": 232, "end": 313 - } + }, + "declared_index": 1 } ], "validity": null, diff --git a/examples/transfer.transfer.tir b/examples/transfer.transfer.tir index d6750767..aaba3250 100644 --- a/examples/transfer.transfer.tir +++ b/examples/transfer.transfer.tir @@ -72,7 +72,10 @@ } ] }, - "optional": false + "optional": false, + "declared_index": { + "Number": 0 + } }, { "address": { @@ -155,7 +158,10 @@ ] } }, - "optional": false + "optional": false, + "declared_index": { + "Number": 1 + } } ], "validity": null, diff --git a/examples/vesting.ast b/examples/vesting.ast index dac2dfa9..b68cf240 100644 --- a/examples/vesting.ast +++ b/examples/vesting.ast @@ -271,7 +271,8 @@ "dummy": false, "start": 323, "end": 526 - } + }, + "declared_index": 0 }, { "name": null, @@ -363,7 +364,8 @@ "dummy": false, "start": 532, "end": 612 - } + }, + "declared_index": 1 } ], "validity": null, @@ -564,7 +566,8 @@ "dummy": false, "start": 911, "end": 994 - } + }, + "declared_index": 0 } ], "validity": null, diff --git a/examples/vesting.lock.tir b/examples/vesting.lock.tir index 5d9609db..b1824836 100644 --- a/examples/vesting.lock.tir +++ b/examples/vesting.lock.tir @@ -130,7 +130,10 @@ } ] }, - "optional": false + "optional": false, + "declared_index": { + "Number": 0 + } }, { "address": { @@ -213,7 +216,10 @@ ] } }, - "optional": false + "optional": false, + "declared_index": { + "Number": 1 + } } ], "validity": null, diff --git a/examples/vesting.unlock.tir b/examples/vesting.unlock.tir index 23f07fd2..0f8b20c4 100644 --- a/examples/vesting.unlock.tir +++ b/examples/vesting.unlock.tir @@ -210,7 +210,10 @@ ] } }, - "optional": false + "optional": false, + "declared_index": { + "Number": 0 + } } ], "validity": null, diff --git a/examples/withdrawal.ast b/examples/withdrawal.ast index 0f0e7382..f0a48934 100644 --- a/examples/withdrawal.ast +++ b/examples/withdrawal.ast @@ -141,7 +141,8 @@ "dummy": false, "start": 158, "end": 225 - } + }, + "declared_index": 0 }, { "name": null, @@ -233,7 +234,8 @@ "dummy": false, "start": 231, "end": 312 - } + }, + "declared_index": 1 } ], "validity": null, diff --git a/examples/withdrawal.transfer.tir b/examples/withdrawal.transfer.tir index facdbb3c..939505a7 100644 --- a/examples/withdrawal.transfer.tir +++ b/examples/withdrawal.transfer.tir @@ -72,7 +72,10 @@ } ] }, - "optional": false + "optional": false, + "declared_index": { + "Number": 0 + } }, { "address": { @@ -155,7 +158,10 @@ ] } }, - "optional": false + "optional": false, + "declared_index": { + "Number": 1 + } } ], "validity": null, From 3871ac715e47960f7cbcbcdf47e0d8942b2e0917 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ludue=C3=B1a?= Date: Tue, 27 Jan 2026 15:11:47 -0300 Subject: [PATCH 05/14] fix: deserialize with default declared index --- crates/tx3-tir/src/model/v1beta0.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/tx3-tir/src/model/v1beta0.rs b/crates/tx3-tir/src/model/v1beta0.rs index 9ca650bd..ad43dccd 100644 --- a/crates/tx3-tir/src/model/v1beta0.rs +++ b/crates/tx3-tir/src/model/v1beta0.rs @@ -305,6 +305,7 @@ pub struct Output { pub datum: Expression, pub amount: Expression, pub optional: bool, + #[serde(default)] pub declared_index: Expression, } From 557894b3a25a73ebad5ee8127b585e64fdaa484a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ludue=C3=B1a?= Date: Tue, 3 Mar 2026 13:05:23 -0300 Subject: [PATCH 06/14] feat: add explicit index to language --- crates/tx3-lang/src/tx3.pest | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/tx3-lang/src/tx3.pest b/crates/tx3-lang/src/tx3.pest index a01471ff..4ec470b3 100644 --- a/crates/tx3-lang/src/tx3.pest +++ b/crates/tx3-lang/src/tx3.pest @@ -247,11 +247,13 @@ reference_block = { output_block_to = { "to" ~ ":" ~ data_expr } output_block_amount = { "amount" ~ ":" ~ data_expr } output_block_datum = { "datum" ~ ":" ~ data_expr } +output_block_index = { "index" ~ ":" ~ data_expr } output_block_field = _{ output_block_to | output_block_amount | - output_block_datum + output_block_datum | + output_block_index } output_block = { @@ -374,6 +376,7 @@ cardano_publish_block_amount = { "amount" ~ ":" ~ data_expr } cardano_publish_block_datum = { "datum" ~ ":" ~ data_expr } cardano_publish_block_version = { "version" ~ ":" ~ data_expr } cardano_publish_block_script = { "script" ~ ":" ~ data_expr } +cardano_publish_block_index = { "index" ~ ":" ~ data_expr } cardano_publish_block_field = _{ @@ -381,7 +384,8 @@ cardano_publish_block_field = _{ cardano_publish_block_amount | cardano_publish_block_datum | cardano_publish_block_version | - cardano_publish_block_script + cardano_publish_block_script | + cardano_publish_block_index } cardano_publish_block = { From ca09b3f4fea6e32ef01590bad2151c3d2c8a54ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ludue=C3=B1a?= Date: Tue, 3 Mar 2026 13:05:48 -0300 Subject: [PATCH 07/14] refactor: tir with index instead of declared_index --- crates/tx3-tir/src/model/v1beta0.rs | 4 ++-- crates/tx3-tir/src/reduce/mod.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/tx3-tir/src/model/v1beta0.rs b/crates/tx3-tir/src/model/v1beta0.rs index ad43dccd..e3e4b78c 100644 --- a/crates/tx3-tir/src/model/v1beta0.rs +++ b/crates/tx3-tir/src/model/v1beta0.rs @@ -306,7 +306,7 @@ pub struct Output { pub amount: Expression, pub optional: bool, #[serde(default)] - pub declared_index: Expression, + pub index: Expression, } #[derive(Serialize, Deserialize, Debug, Clone)] @@ -524,7 +524,7 @@ impl Node for Output { datum: self.datum.apply(visitor)?, amount: self.amount.apply(visitor)?, optional: self.optional, - declared_index: self.declared_index, + index: self.index, }; Ok(visited) diff --git a/crates/tx3-tir/src/reduce/mod.rs b/crates/tx3-tir/src/reduce/mod.rs index 15617dca..4666a700 100644 --- a/crates/tx3-tir/src/reduce/mod.rs +++ b/crates/tx3-tir/src/reduce/mod.rs @@ -1159,7 +1159,7 @@ impl Composite for Output { datum: f(self.datum)?, amount: f(self.amount)?, optional: self.optional, - declared_index: self.declared_index, + index: self.index, }) } } From d33d5cfa76d98db31f12ce0be98a3a21640d9e78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ludue=C3=B1a?= Date: Thu, 5 Mar 2026 11:28:05 -0300 Subject: [PATCH 08/14] feat: explicit index parsing and lowering for output and publish blocks --- CLAUDE.md | 86 +++++++++++++++++++++++++++++++++ crates/tx3-lang/src/ast.rs | 3 +- crates/tx3-lang/src/cardano.rs | 33 ++++++------- crates/tx3-lang/src/lowering.rs | 5 +- crates/tx3-lang/src/parsing.rs | 28 +++-------- 5 files changed, 113 insertions(+), 42 deletions(-) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..be6ce5be --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,86 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What This Project Is + +Tx3 is a domain-specific language (DSL) for describing transaction templates on UTxO-based blockchains, primarily Cardano. It lets dapp authors define transaction patterns (inputs, outputs, datums, redeemers) in a purely functional language that compiles to concrete blockchain transactions. + +## Common Commands + +```bash +# Build everything +cargo build + +# Run all tests +cargo test --workspace + +# Test a specific crate +cargo test -p tx3-lang +cargo test -p tx3-cardano +cargo test -p tx3-resolver +cargo test -p tx3-tir + +# Run a specific test by name +cargo test -p tx3-lang -- parsing::tests::my_test_name + +# Build and run the CLI compiler +cargo run --bin tx3c -- build + +# Check without building +cargo check --workspace +``` + +## Architecture: Compiler Pipeline + +The project is a Rust workspace implementing a multi-stage compiler: + +``` +tx3c (CLI binary) + ├── tx3-lang → parse → analyze → lower → AST/TIR + ├── tx3-cardano → compile TIR to Cardano transactions + ├── tx3-tir → Transaction Intermediate Representation (shared IR) + └── tx3-resolver → UTxO selection and resolution at runtime +``` + +### Stage 1: Parsing (`crates/tx3-lang/src/parsing.rs`) +Uses a [pest](https://pest.rs/) PEG parser with grammar in `tx3.pest`. Produces an AST defined in `ast.rs`. Key types: `Program`, `Tx`, `Party`, `Input`, `Output`. + +### Stage 2: Analyzing (`crates/tx3-lang/src/analyzing.rs`) +Type checking and semantic analysis. Returns an `AnalyzeReport` with errors and warnings. + +### Stage 3: Lowering (`crates/tx3-lang/src/lowering.rs`) +Converts the AST to `tx3_tir::model::v1beta0::Tx` objects — simplifying and normalizing for the downstream compiler. Chain-specific lowering logic lives in `crates/tx3-lang/src/cardano/`. + +### Stage 4: Cardano Compilation (`crates/tx3-cardano/src/`) +Implements the `tx3_tir::compile::Compiler` trait. Sub-modules: +- `compile/mod.rs` — main compilation logic +- `asset_math.rs` — asset arithmetic +- `plutus_data.rs` — Plutus data serialization +- `coercion.rs` — type coercion to Cardano types + +### Stage 5: UTxO Resolution (`crates/tx3-resolver/src/`) +Runtime component. Takes a compiled transaction template and selects real UTxOs to fill inputs. Key sub-modules: +- `inputs/select/` — selection strategies (naive, vector-based) +- `inputs/narrow.rs` — UTxO filtering +- `trp/` — Transaction Resolution Protocol + +### High-Level API +`tx3-lang` exposes a `Workspace` facade (`crates/tx3-lang/src/facade.rs`) that orchestrates the full parse→analyze→lower pipeline. The `include_tx3_build!` macro supports build-time compilation of `.tx3` files. + +## Key Design Patterns + +- **Trait-based abstraction**: `tx3_tir::compile::Compiler` is the abstract compiler interface; `tx3_resolver::UtxoStore` abstracts UTxO storage (async). +- **Error handling**: `thiserror` for error types, `miette` for pretty diagnostics. +- **Serialization**: CBOR via `ciborium` for TIR wire format; `serde` + JSON for config. +- **Test fixtures**: `examples/` contains `.tx3` source files alongside `.ast` and `.tir` snapshots showing expected intermediate representations. + +## Tx3 Language Concepts + +The language's core constructs: +- `party` — a participant (wallet address or script) +- `policy` — an on-chain validation script (can also act as a party) +- `record` — a data structure for datums/redeemers +- `tx` — a transaction template (parameterized function returning a transaction) +- Inside `tx`: `input` blocks (UTxO selection criteria) and `output` blocks (new UTxOs to create) +- Special values: `fees`, `Ada(quantity)`, `source` (input reference for value arithmetic) diff --git a/crates/tx3-lang/src/ast.rs b/crates/tx3-lang/src/ast.rs index ca294dd7..53cc9a03 100644 --- a/crates/tx3-lang/src/ast.rs +++ b/crates/tx3-lang/src/ast.rs @@ -386,6 +386,7 @@ pub enum OutputBlockField { To(Box), Amount(Box), Datum(Box), + Index(Box), } impl OutputBlockField { @@ -394,6 +395,7 @@ impl OutputBlockField { OutputBlockField::To(_) => "to", OutputBlockField::Amount(_) => "amount", OutputBlockField::Datum(_) => "datum", + OutputBlockField::Index(_) => "index", } } } @@ -404,7 +406,6 @@ pub struct OutputBlock { pub optional: bool, pub fields: Vec, pub span: Span, - pub declared_index: Option, } impl OutputBlock { diff --git a/crates/tx3-lang/src/cardano.rs b/crates/tx3-lang/src/cardano.rs index 486eee22..bef5015f 100644 --- a/crates/tx3-lang/src/cardano.rs +++ b/crates/tx3-lang/src/cardano.rs @@ -567,6 +567,7 @@ pub enum CardanoPublishBlockField { Datum(Box), Version(Box), Script(Box), + Index(Box), } impl CardanoPublishBlockField { @@ -577,6 +578,7 @@ impl CardanoPublishBlockField { CardanoPublishBlockField::Datum(_) => "datum", CardanoPublishBlockField::Version(_) => "version", CardanoPublishBlockField::Script(_) => "script", + CardanoPublishBlockField::Index(_) => "index", } } } @@ -586,7 +588,6 @@ pub struct CardanoPublishBlock { pub name: Option, pub fields: Vec, pub span: Span, - pub declared_index: Option, } impl CardanoPublishBlock { @@ -628,6 +629,12 @@ impl AstNode for CardanoPublishBlockField { DataExpr::parse(pair)?.into(), )) } + Rule::cardano_publish_block_index => { + let pair = pair.into_inner().next().unwrap(); + Ok(CardanoPublishBlockField::Index( + DataExpr::parse(pair)?.into(), + )) + } x => unreachable!("Unexpected rule in cardano_publish_block_field: {:?}", x), } } @@ -639,6 +646,7 @@ impl AstNode for CardanoPublishBlockField { Self::Datum(x) => x.span(), Self::Version(x) => x.span(), Self::Script(x) => x.span(), + Self::Index(x) => x.span(), } } } @@ -664,12 +672,7 @@ impl AstNode for CardanoPublishBlock { .map(|x| CardanoPublishBlockField::parse(x)) .collect::, _>>()?; - Ok(CardanoPublishBlock { - name, - fields, - span, - declared_index: None, - }) + Ok(CardanoPublishBlock { name, fields, span }) } fn span(&self) -> &Span { @@ -685,6 +688,7 @@ impl Analyzable for CardanoPublishBlockField { CardanoPublishBlockField::Datum(x) => x.analyze(parent), CardanoPublishBlockField::Version(x) => x.analyze(parent), CardanoPublishBlockField::Script(x) => x.analyze(parent), + CardanoPublishBlockField::Index(x) => x.analyze(parent), } } @@ -695,6 +699,7 @@ impl Analyzable for CardanoPublishBlockField { CardanoPublishBlockField::Datum(x) => x.is_resolved(), CardanoPublishBlockField::Version(x) => x.is_resolved(), CardanoPublishBlockField::Script(x) => x.is_resolved(), + CardanoPublishBlockField::Index(x) => x.is_resolved(), } } } @@ -722,6 +727,7 @@ impl IntoLower for CardanoPublishBlockField { CardanoPublishBlockField::Datum(x) => Ok(("datum".to_string(), x.into_lower(ctx)?)), CardanoPublishBlockField::Version(x) => Ok(("version".to_string(), x.into_lower(ctx)?)), CardanoPublishBlockField::Script(x) => Ok(("script".to_string(), x.into_lower(ctx)?)), + CardanoPublishBlockField::Index(x) => Ok(("index".to_string(), x.into_lower(ctx)?)), } } } @@ -733,21 +739,12 @@ impl IntoLower for CardanoPublishBlock { &self, ctx: &crate::lowering::Context, ) -> Result { - let mut data: HashMap = self + let data: HashMap = self .fields .iter() .map(|x| x.into_lower(ctx)) .collect::>()?; - data.insert( - "declared_index".to_string(), - ir::Expression::Number( - self.declared_index - .map(|x| x as i128) - .expect("Publish block must have a declaration index"), - ), - ); - Ok(ir::AdHocDirective { name: "cardano_publish".to_string(), data, @@ -953,7 +950,6 @@ mod tests { ))), ], span: Span::DUMMY, - declared_index: None, } ); @@ -983,7 +979,6 @@ mod tests { ))), ], span: Span::DUMMY, - declared_index: None, } ); diff --git a/crates/tx3-lang/src/lowering.rs b/crates/tx3-lang/src/lowering.rs index b5c97d61..ff9e88fb 100644 --- a/crates/tx3-lang/src/lowering.rs +++ b/crates/tx3-lang/src/lowering.rs @@ -686,6 +686,7 @@ impl IntoLower for ast::OutputBlockField { let ctx = ctx.enter_datum_expr(); x.into_lower(&ctx) } + ast::OutputBlockField::Index(x) => x.into_lower(ctx), } } } @@ -697,14 +698,14 @@ impl IntoLower for ast::OutputBlock { let address = self.find("to").into_lower(ctx)?.unwrap_or_default(); let datum = self.find("datum").into_lower(ctx)?.unwrap_or_default(); let amount = self.find("amount").into_lower(ctx)?.unwrap_or_default(); - let declared_ix = self.declared_index.unwrap(); + let index = self.find("index").into_lower(ctx)?.unwrap_or_default(); Ok(ir::Output { address, datum, amount, optional: self.optional, - declared_index: ir::Expression::Number(declared_ix as i128), + index, }) } } diff --git a/crates/tx3-lang/src/parsing.rs b/crates/tx3-lang/src/parsing.rs index a27bf966..9a62ba87 100644 --- a/crates/tx3-lang/src/parsing.rs +++ b/crates/tx3-lang/src/parsing.rs @@ -208,32 +208,16 @@ impl AstNode for TxDef { let mut signers = None; let mut metadata = None; - let mut declared_index: usize = 0; - for item in inner { match item.as_rule() { Rule::locals_block => locals = Some(LocalsBlock::parse(item)?), Rule::reference_block => references.push(ReferenceBlock::parse(item)?), Rule::input_block => inputs.push(InputBlock::parse(item)?), - Rule::output_block => { - let mut ob = OutputBlock::parse(item)?; - ob.declared_index = Some(declared_index); - declared_index += 1; - outputs.push(ob); - } + Rule::output_block => outputs.push(OutputBlock::parse(item)?), Rule::validity_block => validity = Some(ValidityBlock::parse(item)?), Rule::mint_block => mints.push(MintBlock::parse(item)?), Rule::burn_block => burns.push(MintBlock::parse(item)?), - Rule::chain_specific_block => { - let mut csb = ChainSpecificBlock::parse(item)?; - let ChainSpecificBlock::Cardano(cardano_block) = &mut csb; - if let crate::cardano::CardanoBlock::Publish(pb) = cardano_block { - pb.declared_index = Some(declared_index); - declared_index += 1; - } - - adhoc.push(csb); - } + Rule::chain_specific_block => adhoc.push(ChainSpecificBlock::parse(item)?), Rule::collateral_block => collateral.push(CollateralBlock::parse(item)?), Rule::signers_block => signers = Some(SignersBlock::parse(item)?), Rule::metadata_block => metadata = Some(MetadataBlock::parse(item)?), @@ -585,6 +569,11 @@ impl AstNode for OutputBlockField { let x = OutputBlockField::Datum(DataExpr::parse(pair)?.into()); Ok(x) } + Rule::output_block_index => { + let pair = pair.into_inner().next().unwrap(); + let x = OutputBlockField::Index(DataExpr::parse(pair)?.into()); + Ok(x) + } x => unreachable!("Unexpected rule in output_block_field: {:?}", x), } } @@ -594,6 +583,7 @@ impl AstNode for OutputBlockField { Self::To(x) => x.span(), Self::Amount(x) => x.span(), Self::Datum(x) => x.span(), + Self::Index(x) => x.span(), } } } @@ -633,7 +623,6 @@ impl AstNode for OutputBlock { optional, fields, span, - declared_index: None, }) } @@ -2481,7 +2470,6 @@ mod tests { }))), ], span: Span::DUMMY, - declared_index: None, } ); From e4f8bedcb351d26ca4ae60bb30a5ef925a67aa0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ludue=C3=B1a?= Date: Thu, 5 Mar 2026 11:32:51 -0300 Subject: [PATCH 09/14] feat: analyze phase working --- crates/tx3-lang/src/analyzing.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/tx3-lang/src/analyzing.rs b/crates/tx3-lang/src/analyzing.rs index 55e50b02..48387ac3 100644 --- a/crates/tx3-lang/src/analyzing.rs +++ b/crates/tx3-lang/src/analyzing.rs @@ -1019,6 +1019,7 @@ impl Analyzable for OutputBlockField { OutputBlockField::To(x) => x.analyze(parent), OutputBlockField::Amount(x) => x.analyze(parent), OutputBlockField::Datum(x) => x.analyze(parent), + OutputBlockField::Index(x) => x.analyze(parent), } } @@ -1027,6 +1028,7 @@ impl Analyzable for OutputBlockField { OutputBlockField::To(x) => x.is_resolved(), OutputBlockField::Amount(x) => x.is_resolved(), OutputBlockField::Datum(x) => x.is_resolved(), + OutputBlockField::Index(x) => x.is_resolved(), } } } From ffe65c9592f344975774140751179844bbfe9028 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ludue=C3=B1a?= Date: Thu, 5 Mar 2026 11:34:12 -0300 Subject: [PATCH 10/14] feat: place outputs with explicit indices & fill empty spaces with the rest --- crates/tx3-cardano/src/compile/mod.rs | 45 ++++++++++++++++++++++----- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/crates/tx3-cardano/src/compile/mod.rs b/crates/tx3-cardano/src/compile/mod.rs index bc3a54a2..aa8724b6 100644 --- a/crates/tx3-cardano/src/compile/mod.rs +++ b/crates/tx3-cardano/src/compile/mod.rs @@ -288,38 +288,67 @@ fn compile_outputs( tx: &tir::Tx, network: Network, ) -> Result>, Error> { - let outputs = tx.outputs.iter().filter_map(|out| { + let regular_outputs = tx.outputs.iter().filter_map(|out| { let compiled = compile_output_block(out, network); if out.optional && !output_has_assets(&compiled) { return None; } - let idx = out.declared_index.as_number().map(|n| n as usize); + let idx = out.index.as_number().map(|n| n as usize); Some(compiled.map(|o| (idx, o))) }); - let cardano_outputs = tx + let publish_outputs = tx .adhoc .iter() .filter(|x| x.name.as_str() == "cardano_publish") .map(|adhoc| { let idx = adhoc .data - .get("declared_index") + .get("index") .and_then(|expr| expr.as_number()) .map(|n| n as usize); compile_cardano_publish_directive(adhoc, network).map(|o| (idx, o)) }); - let mut all_outputs: Vec<_> = outputs - .chain(cardano_outputs) + let all: Vec<_> = publish_outputs + .chain(regular_outputs) .collect::, _>>()?; - all_outputs.sort_by_key(|(idx, _)| idx.unwrap_or(usize::MAX)); + order_by_index(all) +} + +fn order_by_index( + outputs: Vec<(Option, primitives::TransactionOutput<'static>)>, +) -> Result>, Error> { + let total = outputs.len(); + let mut slots: Vec>> = vec![None; total]; + let mut non_indexed = Vec::new(); + + for (idx, compiled) in outputs { + match idx { + Some(pos) => { + if pos >= total { + return Err(Error::ConsistencyError(format!( + "output index {pos} is out of range (total outputs: {total})" + ))); + } + slots[pos] = Some(compiled); + } + None => non_indexed.push(compiled), + } + } + + let mut filler = non_indexed.into_iter(); + for slot in &mut slots { + if slot.is_none() { + *slot = filler.next(); + } + } - Ok(all_outputs.into_iter().map(|(_, out)| out).collect()) + Ok(slots.into_iter().flatten().collect()) } pub fn compile_cardano_publish_directive( From 0070efd7173be4aca47f3bef042c6a415e595786 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ludue=C3=B1a?= Date: Thu, 5 Mar 2026 11:53:38 -0300 Subject: [PATCH 11/14] chore: update tests --- examples/asteria.move_ship.tir | 8 +--- examples/buidler_fest_2026.buy_ticket.tir | 12 ++---- examples/burn.burn_stuff.tir | 4 +- ...ardano_witness.mint_from_native_script.tir | 4 +- examples/cardano_witness.mint_from_plutus.tir | 4 +- examples/donation.mint_from_plutus.tir | 4 +- examples/env_vars.mint_from_env.tir | 4 +- examples/faucet.claim_with_password.tir | 4 +- examples/input_datum.increase_counter.tir | 4 +- examples/lang_tour.my_tx.tir | 10 ++--- examples/list_concat.concat_list.tir | 4 +- examples/local_vars.mint_from_local.tir | 4 +- examples/map.transfer.tir | 8 +--- examples/min_utxo.transfer_min.tir | 8 +--- examples/reference_script.publish_native.tir | 25 +++++------ examples/reference_script.publish_plutus.tir | 43 ++++++++----------- examples/swap.swap.tir | 8 +--- examples/transfer.transfer.tir | 8 +--- examples/vesting.lock.tir | 8 +--- examples/vesting.unlock.tir | 4 +- examples/withdrawal.transfer.tir | 20 ++++----- 21 files changed, 66 insertions(+), 132 deletions(-) diff --git a/examples/asteria.move_ship.tir b/examples/asteria.move_ship.tir index 11d1815e..450cccf4 100644 --- a/examples/asteria.move_ship.tir +++ b/examples/asteria.move_ship.tir @@ -1169,9 +1169,7 @@ } }, "optional": false, - "declared_index": { - "Number": 0 - } + "index": "None" }, { "address": { @@ -1266,9 +1264,7 @@ } }, "optional": false, - "declared_index": { - "Number": 1 - } + "index": "None" } ], "validity": null, diff --git a/examples/buidler_fest_2026.buy_ticket.tir b/examples/buidler_fest_2026.buy_ticket.tir index e5133d3a..40a89297 100644 --- a/examples/buidler_fest_2026.buy_ticket.tir +++ b/examples/buidler_fest_2026.buy_ticket.tir @@ -325,9 +325,7 @@ } }, "optional": false, - "declared_index": { - "Number": 0 - } + "index": "None" }, { "address": { @@ -462,9 +460,7 @@ } }, "optional": false, - "declared_index": { - "Number": 1 - } + "index": "None" }, { "address": { @@ -493,9 +489,7 @@ ] }, "optional": false, - "declared_index": { - "Number": 2 - } + "index": "None" } ], "validity": { diff --git a/examples/burn.burn_stuff.tir b/examples/burn.burn_stuff.tir index 897dfd9e..658b00d0 100644 --- a/examples/burn.burn_stuff.tir +++ b/examples/burn.burn_stuff.tir @@ -245,9 +245,7 @@ } }, "optional": false, - "declared_index": { - "Number": 0 - } + "index": "None" } ], "validity": null, diff --git a/examples/cardano_witness.mint_from_native_script.tir b/examples/cardano_witness.mint_from_native_script.tir index da5381cc..49e37c57 100644 --- a/examples/cardano_witness.mint_from_native_script.tir +++ b/examples/cardano_witness.mint_from_native_script.tir @@ -135,9 +135,7 @@ } }, "optional": false, - "declared_index": { - "Number": 0 - } + "index": "None" } ], "validity": null, diff --git a/examples/cardano_witness.mint_from_plutus.tir b/examples/cardano_witness.mint_from_plutus.tir index b3a6bb4e..17659c35 100644 --- a/examples/cardano_witness.mint_from_plutus.tir +++ b/examples/cardano_witness.mint_from_plutus.tir @@ -135,9 +135,7 @@ } }, "optional": false, - "declared_index": { - "Number": 0 - } + "index": "None" } ], "validity": null, diff --git a/examples/donation.mint_from_plutus.tir b/examples/donation.mint_from_plutus.tir index 0305adae..265a84b0 100644 --- a/examples/donation.mint_from_plutus.tir +++ b/examples/donation.mint_from_plutus.tir @@ -199,9 +199,7 @@ } }, "optional": false, - "declared_index": { - "Number": 0 - } + "index": "None" } ], "validity": null, diff --git a/examples/env_vars.mint_from_env.tir b/examples/env_vars.mint_from_env.tir index f1871695..a096c753 100644 --- a/examples/env_vars.mint_from_env.tir +++ b/examples/env_vars.mint_from_env.tir @@ -73,9 +73,7 @@ ] }, "optional": false, - "declared_index": { - "Number": 0 - } + "index": "None" } ], "validity": null, diff --git a/examples/faucet.claim_with_password.tir b/examples/faucet.claim_with_password.tir index a669e3f6..06cc1000 100644 --- a/examples/faucet.claim_with_password.tir +++ b/examples/faucet.claim_with_password.tir @@ -135,9 +135,7 @@ } }, "optional": false, - "declared_index": { - "Number": 0 - } + "index": "None" } ], "validity": null, diff --git a/examples/input_datum.increase_counter.tir b/examples/input_datum.increase_counter.tir index f5668f6d..47ab847d 100644 --- a/examples/input_datum.increase_counter.tir +++ b/examples/input_datum.increase_counter.tir @@ -167,9 +167,7 @@ } }, "optional": false, - "declared_index": { - "Number": 0 - } + "index": "None" } ], "validity": null, diff --git a/examples/lang_tour.my_tx.tir b/examples/lang_tour.my_tx.tir index 5e5ee84e..13d4c2c0 100644 --- a/examples/lang_tour.my_tx.tir +++ b/examples/lang_tour.my_tx.tir @@ -535,9 +535,7 @@ } }, "optional": false, - "declared_index": { - "Number": 0 - } + "index": "None" } ], "validity": { @@ -669,9 +667,6 @@ { "name": "withdrawal", "data": { - "amount": { - "Number": 100 - }, "redeemer": { "Struct": { "constructor": 0, @@ -685,6 +680,9 @@ "Address" ] } + }, + "amount": { + "Number": 100 } } }, diff --git a/examples/list_concat.concat_list.tir b/examples/list_concat.concat_list.tir index 1bd9a505..672e7130 100644 --- a/examples/list_concat.concat_list.tir +++ b/examples/list_concat.concat_list.tir @@ -129,9 +129,7 @@ } }, "optional": false, - "declared_index": { - "Number": 0 - } + "index": "None" } ], "validity": null, diff --git a/examples/local_vars.mint_from_local.tir b/examples/local_vars.mint_from_local.tir index ea7efbdc..81358783 100644 --- a/examples/local_vars.mint_from_local.tir +++ b/examples/local_vars.mint_from_local.tir @@ -78,9 +78,7 @@ } }, "optional": false, - "declared_index": { - "Number": 0 - } + "index": "None" } ], "validity": null, diff --git a/examples/map.transfer.tir b/examples/map.transfer.tir index 0fd8f133..241be073 100644 --- a/examples/map.transfer.tir +++ b/examples/map.transfer.tir @@ -73,9 +73,7 @@ ] }, "optional": false, - "declared_index": { - "Number": 0 - } + "index": "None" }, { "address": { @@ -195,9 +193,7 @@ } }, "optional": false, - "declared_index": { - "Number": 1 - } + "index": "None" } ], "validity": null, diff --git a/examples/min_utxo.transfer_min.tir b/examples/min_utxo.transfer_min.tir index 06f62871..c6c10d38 100644 --- a/examples/min_utxo.transfer_min.tir +++ b/examples/min_utxo.transfer_min.tir @@ -77,9 +77,7 @@ } }, "optional": false, - "declared_index": { - "Number": 0 - } + "index": "None" }, { "address": { @@ -167,9 +165,7 @@ } }, "optional": false, - "declared_index": { - "Number": 1 - } + "index": "None" } ], "validity": null, diff --git a/examples/reference_script.publish_native.tir b/examples/reference_script.publish_native.tir index 6438229c..264dcc77 100644 --- a/examples/reference_script.publish_native.tir +++ b/examples/reference_script.publish_native.tir @@ -128,9 +128,7 @@ } }, "optional": false, - "declared_index": { - "Number": 1 - } + "index": "None" } ], "validity": null, @@ -140,17 +138,6 @@ { "name": "cardano_publish", "data": { - "version": { - "Number": 0 - }, - "to": { - "EvalParam": { - "ExpectValue": [ - "receiver", - "Address" - ] - } - }, "script": { "Bytes": [ 130, @@ -161,6 +148,14 @@ 0 ] }, + "to": { + "EvalParam": { + "ExpectValue": [ + "receiver", + "Address" + ] + } + }, "amount": { "Assets": [ { @@ -177,7 +172,7 @@ } ] }, - "declared_index": { + "version": { "Number": 0 } } diff --git a/examples/reference_script.publish_plutus.tir b/examples/reference_script.publish_plutus.tir index 2f757b60..774b7d17 100644 --- a/examples/reference_script.publish_plutus.tir +++ b/examples/reference_script.publish_plutus.tir @@ -128,9 +128,7 @@ } }, "optional": false, - "declared_index": { - "Number": 1 - } + "index": "None" } ], "validity": null, @@ -140,25 +138,6 @@ { "name": "cardano_publish", "data": { - "amount": { - "Assets": [ - { - "policy": "None", - "asset_name": "None", - "amount": { - "EvalParam": { - "ExpectValue": [ - "quantity", - "Int" - ] - } - } - } - ] - }, - "version": { - "Number": 3 - }, "to": { "EvalParam": { "ExpectValue": [ @@ -189,8 +168,24 @@ 105 ] }, - "declared_index": { - "Number": 0 + "version": { + "Number": 3 + }, + "amount": { + "Assets": [ + { + "policy": "None", + "asset_name": "None", + "amount": { + "EvalParam": { + "ExpectValue": [ + "quantity", + "Int" + ] + } + } + } + ] } } } diff --git a/examples/swap.swap.tir b/examples/swap.swap.tir index a4a33b99..323372b4 100644 --- a/examples/swap.swap.tir +++ b/examples/swap.swap.tir @@ -250,9 +250,7 @@ } }, "optional": false, - "declared_index": { - "Number": 0 - } + "index": "None" }, { "address": { @@ -343,9 +341,7 @@ } }, "optional": false, - "declared_index": { - "Number": 1 - } + "index": "None" } ], "validity": null, diff --git a/examples/transfer.transfer.tir b/examples/transfer.transfer.tir index aaba3250..31f82603 100644 --- a/examples/transfer.transfer.tir +++ b/examples/transfer.transfer.tir @@ -73,9 +73,7 @@ ] }, "optional": false, - "declared_index": { - "Number": 0 - } + "index": "None" }, { "address": { @@ -159,9 +157,7 @@ } }, "optional": false, - "declared_index": { - "Number": 1 - } + "index": "None" } ], "validity": null, diff --git a/examples/vesting.lock.tir b/examples/vesting.lock.tir index b1824836..166c7cf8 100644 --- a/examples/vesting.lock.tir +++ b/examples/vesting.lock.tir @@ -131,9 +131,7 @@ ] }, "optional": false, - "declared_index": { - "Number": 0 - } + "index": "None" }, { "address": { @@ -217,9 +215,7 @@ } }, "optional": false, - "declared_index": { - "Number": 1 - } + "index": "None" } ], "validity": null, diff --git a/examples/vesting.unlock.tir b/examples/vesting.unlock.tir index 0f8b20c4..4c41d0f8 100644 --- a/examples/vesting.unlock.tir +++ b/examples/vesting.unlock.tir @@ -211,9 +211,7 @@ } }, "optional": false, - "declared_index": { - "Number": 0 - } + "index": "None" } ], "validity": null, diff --git a/examples/withdrawal.transfer.tir b/examples/withdrawal.transfer.tir index 939505a7..e5b95ce0 100644 --- a/examples/withdrawal.transfer.tir +++ b/examples/withdrawal.transfer.tir @@ -73,9 +73,7 @@ ] }, "optional": false, - "declared_index": { - "Number": 0 - } + "index": "None" }, { "address": { @@ -159,9 +157,7 @@ } }, "optional": false, - "declared_index": { - "Number": 1 - } + "index": "None" } ], "validity": null, @@ -171,6 +167,12 @@ { "name": "withdrawal", "data": { + "redeemer": { + "Struct": { + "constructor": 0, + "fields": [] + } + }, "credential": { "EvalParam": { "ExpectValue": [ @@ -181,12 +183,6 @@ }, "amount": { "Number": 0 - }, - "redeemer": { - "Struct": { - "constructor": 0, - "fields": [] - } } } } From 91d36221fc0704aeba64fc2d9decf0cb5628ddd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ludue=C3=B1a?= Date: Tue, 10 Mar 2026 15:05:10 -0300 Subject: [PATCH 12/14] remove CLAUDE.md file --- CLAUDE.md | 86 ------------------------------------------------------- 1 file changed, 86 deletions(-) delete mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index be6ce5be..00000000 --- a/CLAUDE.md +++ /dev/null @@ -1,86 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## What This Project Is - -Tx3 is a domain-specific language (DSL) for describing transaction templates on UTxO-based blockchains, primarily Cardano. It lets dapp authors define transaction patterns (inputs, outputs, datums, redeemers) in a purely functional language that compiles to concrete blockchain transactions. - -## Common Commands - -```bash -# Build everything -cargo build - -# Run all tests -cargo test --workspace - -# Test a specific crate -cargo test -p tx3-lang -cargo test -p tx3-cardano -cargo test -p tx3-resolver -cargo test -p tx3-tir - -# Run a specific test by name -cargo test -p tx3-lang -- parsing::tests::my_test_name - -# Build and run the CLI compiler -cargo run --bin tx3c -- build - -# Check without building -cargo check --workspace -``` - -## Architecture: Compiler Pipeline - -The project is a Rust workspace implementing a multi-stage compiler: - -``` -tx3c (CLI binary) - ├── tx3-lang → parse → analyze → lower → AST/TIR - ├── tx3-cardano → compile TIR to Cardano transactions - ├── tx3-tir → Transaction Intermediate Representation (shared IR) - └── tx3-resolver → UTxO selection and resolution at runtime -``` - -### Stage 1: Parsing (`crates/tx3-lang/src/parsing.rs`) -Uses a [pest](https://pest.rs/) PEG parser with grammar in `tx3.pest`. Produces an AST defined in `ast.rs`. Key types: `Program`, `Tx`, `Party`, `Input`, `Output`. - -### Stage 2: Analyzing (`crates/tx3-lang/src/analyzing.rs`) -Type checking and semantic analysis. Returns an `AnalyzeReport` with errors and warnings. - -### Stage 3: Lowering (`crates/tx3-lang/src/lowering.rs`) -Converts the AST to `tx3_tir::model::v1beta0::Tx` objects — simplifying and normalizing for the downstream compiler. Chain-specific lowering logic lives in `crates/tx3-lang/src/cardano/`. - -### Stage 4: Cardano Compilation (`crates/tx3-cardano/src/`) -Implements the `tx3_tir::compile::Compiler` trait. Sub-modules: -- `compile/mod.rs` — main compilation logic -- `asset_math.rs` — asset arithmetic -- `plutus_data.rs` — Plutus data serialization -- `coercion.rs` — type coercion to Cardano types - -### Stage 5: UTxO Resolution (`crates/tx3-resolver/src/`) -Runtime component. Takes a compiled transaction template and selects real UTxOs to fill inputs. Key sub-modules: -- `inputs/select/` — selection strategies (naive, vector-based) -- `inputs/narrow.rs` — UTxO filtering -- `trp/` — Transaction Resolution Protocol - -### High-Level API -`tx3-lang` exposes a `Workspace` facade (`crates/tx3-lang/src/facade.rs`) that orchestrates the full parse→analyze→lower pipeline. The `include_tx3_build!` macro supports build-time compilation of `.tx3` files. - -## Key Design Patterns - -- **Trait-based abstraction**: `tx3_tir::compile::Compiler` is the abstract compiler interface; `tx3_resolver::UtxoStore` abstracts UTxO storage (async). -- **Error handling**: `thiserror` for error types, `miette` for pretty diagnostics. -- **Serialization**: CBOR via `ciborium` for TIR wire format; `serde` + JSON for config. -- **Test fixtures**: `examples/` contains `.tx3` source files alongside `.ast` and `.tir` snapshots showing expected intermediate representations. - -## Tx3 Language Concepts - -The language's core constructs: -- `party` — a participant (wallet address or script) -- `policy` — an on-chain validation script (can also act as a party) -- `record` — a data structure for datums/redeemers -- `tx` — a transaction template (parameterized function returning a transaction) -- Inside `tx`: `input` blocks (UTxO selection criteria) and `output` blocks (new UTxOs to create) -- Special values: `fees`, `Ada(quantity)`, `source` (input reference for value arithmetic) From 06a445b8d510980e0d8990f4c08cfc0ee9e05639 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ludue=C3=B1a?= Date: Thu, 12 Mar 2026 10:27:09 -0300 Subject: [PATCH 13/14] fix: tests compatible with new schema --- examples/asteria.ast | 6 +-- examples/buidler_fest_2026.ast | 9 ++-- examples/burn.ast | 3 +- examples/cardano_witness.ast | 6 +-- examples/cardano_witness.mint_from_plutus.tir | 6 +-- examples/disordered.ast | 6 +-- examples/donation.ast | 3 +- examples/env_vars.ast | 3 +- examples/faucet.ast | 3 +- examples/input_datum.ast | 3 +- examples/lang_tour.ast | 3 +- examples/lang_tour.my_tx.tir | 6 +-- examples/list_concat.ast | 3 +- examples/local_vars.ast | 3 +- examples/map.ast | 6 +-- examples/reference_script.ast | 12 ++--- examples/reference_script.publish_native.tir | 6 +-- examples/reference_script.publish_plutus.tir | 44 +++++++++---------- examples/swap.ast | 6 +-- examples/transfer.ast | 6 +-- examples/vesting.ast | 9 ++-- examples/withdrawal.ast | 6 +-- examples/withdrawal.transfer.tir | 12 ++--- 23 files changed, 69 insertions(+), 101 deletions(-) diff --git a/examples/asteria.ast b/examples/asteria.ast index 9cdda1d4..4b556cb1 100644 --- a/examples/asteria.ast +++ b/examples/asteria.ast @@ -597,8 +597,7 @@ "dummy": false, "start": 1158, "end": 1379 - }, - "declared_index": 0 + } }, { "name": null, @@ -652,8 +651,7 @@ "dummy": false, "start": 1385, "end": 1449 - }, - "declared_index": 1 + } } ], "validity": null, diff --git a/examples/buidler_fest_2026.ast b/examples/buidler_fest_2026.ast index 53cf2bab..c0c0f570 100644 --- a/examples/buidler_fest_2026.ast +++ b/examples/buidler_fest_2026.ast @@ -537,8 +537,7 @@ "dummy": false, "start": 958, "end": 1063 - }, - "declared_index": 0 + } }, { "name": { @@ -673,8 +672,7 @@ "dummy": false, "start": 1069, "end": 1246 - }, - "declared_index": 1 + } }, { "name": { @@ -735,8 +733,7 @@ "dummy": false, "start": 1252, "end": 1331 - }, - "declared_index": 2 + } } ], "validity": { diff --git a/examples/burn.ast b/examples/burn.ast index b181ae69..6eafd087 100644 --- a/examples/burn.ast +++ b/examples/burn.ast @@ -222,8 +222,7 @@ "dummy": false, "start": 337, "end": 412 - }, - "declared_index": 0 + } } ], "validity": null, diff --git a/examples/cardano_witness.ast b/examples/cardano_witness.ast index 28f753d1..9136d18d 100644 --- a/examples/cardano_witness.ast +++ b/examples/cardano_witness.ast @@ -203,8 +203,7 @@ "dummy": false, "start": 404, "end": 481 - }, - "declared_index": 0 + } } ], "validity": null, @@ -526,8 +525,7 @@ "dummy": false, "start": 972, "end": 1049 - }, - "declared_index": 0 + } } ], "validity": null, diff --git a/examples/cardano_witness.mint_from_plutus.tir b/examples/cardano_witness.mint_from_plutus.tir index 17659c35..272b799d 100644 --- a/examples/cardano_witness.mint_from_plutus.tir +++ b/examples/cardano_witness.mint_from_plutus.tir @@ -203,9 +203,6 @@ { "name": "plutus_witness", "data": { - "version": { - "Number": 3 - }, "script": { "Bytes": [ 81, @@ -227,6 +224,9 @@ 174, 105 ] + }, + "version": { + "Number": 3 } } } diff --git a/examples/disordered.ast b/examples/disordered.ast index 42e4b4e1..de9c48f3 100644 --- a/examples/disordered.ast +++ b/examples/disordered.ast @@ -160,8 +160,7 @@ "dummy": false, "start": 58, "end": 125 - }, - "declared_index": 0 + } }, { "name": null, @@ -253,8 +252,7 @@ "dummy": false, "start": 214, "end": 295 - }, - "declared_index": 1 + } } ], "validity": null, diff --git a/examples/donation.ast b/examples/donation.ast index 3e763f5c..f82e5c24 100644 --- a/examples/donation.ast +++ b/examples/donation.ast @@ -267,8 +267,7 @@ "dummy": false, "start": 187, "end": 286 - }, - "declared_index": 0 + } } ], "validity": null, diff --git a/examples/env_vars.ast b/examples/env_vars.ast index 24ad5171..cef2b474 100644 --- a/examples/env_vars.ast +++ b/examples/env_vars.ast @@ -170,8 +170,7 @@ "dummy": false, "start": 343, "end": 426 - }, - "declared_index": 0 + } } ], "validity": null, diff --git a/examples/faucet.ast b/examples/faucet.ast index 13a88f6d..e961c1d4 100644 --- a/examples/faucet.ast +++ b/examples/faucet.ast @@ -171,8 +171,7 @@ "dummy": false, "start": 429, "end": 523 - }, - "declared_index": 0 + } } ], "validity": null, diff --git a/examples/input_datum.ast b/examples/input_datum.ast index e1070141..c0cb2672 100644 --- a/examples/input_datum.ast +++ b/examples/input_datum.ast @@ -259,8 +259,7 @@ "dummy": false, "start": 208, "end": 397 - }, - "declared_index": 0 + } } ], "validity": null, diff --git a/examples/lang_tour.ast b/examples/lang_tour.ast index b5b0597b..cfe0e4cd 100644 --- a/examples/lang_tour.ast +++ b/examples/lang_tour.ast @@ -845,8 +845,7 @@ "dummy": false, "start": 1243, "end": 1622 - }, - "declared_index": 0 + } } ], "validity": { diff --git a/examples/lang_tour.my_tx.tir b/examples/lang_tour.my_tx.tir index 13d4c2c0..044b0e84 100644 --- a/examples/lang_tour.my_tx.tir +++ b/examples/lang_tour.my_tx.tir @@ -673,6 +673,9 @@ "fields": [] } }, + "amount": { + "Number": 100 + }, "credential": { "EvalParam": { "ExpectValue": [ @@ -680,9 +683,6 @@ "Address" ] } - }, - "amount": { - "Number": 100 } } }, diff --git a/examples/list_concat.ast b/examples/list_concat.ast index 49c7f67d..f8b6c17f 100644 --- a/examples/list_concat.ast +++ b/examples/list_concat.ast @@ -214,8 +214,7 @@ "dummy": false, "start": 156, "end": 303 - }, - "declared_index": 0 + } } ], "validity": null, diff --git a/examples/local_vars.ast b/examples/local_vars.ast index aa1f690b..432bb4ff 100644 --- a/examples/local_vars.ast +++ b/examples/local_vars.ast @@ -195,8 +195,7 @@ "dummy": false, "start": 302, "end": 366 - }, - "declared_index": 0 + } } ], "validity": null, diff --git a/examples/map.ast b/examples/map.ast index 525147e4..148694e0 100644 --- a/examples/map.ast +++ b/examples/map.ast @@ -141,8 +141,7 @@ "dummy": false, "start": 190, "end": 257 - }, - "declared_index": 0 + } }, { "name": null, @@ -337,8 +336,7 @@ "dummy": false, "start": 263, "end": 418 - }, - "declared_index": 1 + } } ], "validity": null, diff --git a/examples/reference_script.ast b/examples/reference_script.ast index 61a70385..ccb5f6db 100644 --- a/examples/reference_script.ast +++ b/examples/reference_script.ast @@ -179,8 +179,7 @@ "dummy": false, "start": 323, "end": 404 - }, - "declared_index": 1 + } } ], "validity": null, @@ -258,8 +257,7 @@ "dummy": false, "start": 173, "end": 317 - }, - "declared_index": 0 + } } } } @@ -450,8 +448,7 @@ "dummy": false, "start": 675, "end": 756 - }, - "declared_index": 1 + } } ], "validity": null, @@ -529,8 +526,7 @@ "dummy": false, "start": 549, "end": 669 - }, - "declared_index": 0 + } } } } diff --git a/examples/reference_script.publish_native.tir b/examples/reference_script.publish_native.tir index 264dcc77..30f6c7a9 100644 --- a/examples/reference_script.publish_native.tir +++ b/examples/reference_script.publish_native.tir @@ -148,6 +148,9 @@ 0 ] }, + "version": { + "Number": 0 + }, "to": { "EvalParam": { "ExpectValue": [ @@ -171,9 +174,6 @@ } } ] - }, - "version": { - "Number": 0 } } } diff --git a/examples/reference_script.publish_plutus.tir b/examples/reference_script.publish_plutus.tir index 774b7d17..e9ea1d62 100644 --- a/examples/reference_script.publish_plutus.tir +++ b/examples/reference_script.publish_plutus.tir @@ -138,13 +138,21 @@ { "name": "cardano_publish", "data": { - "to": { - "EvalParam": { - "ExpectValue": [ - "receiver", - "Address" - ] - } + "amount": { + "Assets": [ + { + "policy": "None", + "asset_name": "None", + "amount": { + "EvalParam": { + "ExpectValue": [ + "quantity", + "Int" + ] + } + } + } + ] }, "script": { "Bytes": [ @@ -171,21 +179,13 @@ "version": { "Number": 3 }, - "amount": { - "Assets": [ - { - "policy": "None", - "asset_name": "None", - "amount": { - "EvalParam": { - "ExpectValue": [ - "quantity", - "Int" - ] - } - } - } - ] + "to": { + "EvalParam": { + "ExpectValue": [ + "receiver", + "Address" + ] + } } } } diff --git a/examples/swap.ast b/examples/swap.ast index 57a28d15..8d915d0f 100644 --- a/examples/swap.ast +++ b/examples/swap.ast @@ -465,8 +465,7 @@ "dummy": false, "start": 479, "end": 682 - }, - "declared_index": 0 + } }, { "name": null, @@ -558,8 +557,7 @@ "dummy": false, "start": 688, "end": 765 - }, - "declared_index": 1 + } } ], "validity": null, diff --git a/examples/transfer.ast b/examples/transfer.ast index 4cf90a6a..a21a5413 100644 --- a/examples/transfer.ast +++ b/examples/transfer.ast @@ -141,8 +141,7 @@ "dummy": false, "start": 159, "end": 226 - }, - "declared_index": 0 + } }, { "name": null, @@ -234,8 +233,7 @@ "dummy": false, "start": 232, "end": 313 - }, - "declared_index": 1 + } } ], "validity": null, diff --git a/examples/vesting.ast b/examples/vesting.ast index b68cf240..dac2dfa9 100644 --- a/examples/vesting.ast +++ b/examples/vesting.ast @@ -271,8 +271,7 @@ "dummy": false, "start": 323, "end": 526 - }, - "declared_index": 0 + } }, { "name": null, @@ -364,8 +363,7 @@ "dummy": false, "start": 532, "end": 612 - }, - "declared_index": 1 + } } ], "validity": null, @@ -566,8 +564,7 @@ "dummy": false, "start": 911, "end": 994 - }, - "declared_index": 0 + } } ], "validity": null, diff --git a/examples/withdrawal.ast b/examples/withdrawal.ast index f0a48934..0f0e7382 100644 --- a/examples/withdrawal.ast +++ b/examples/withdrawal.ast @@ -141,8 +141,7 @@ "dummy": false, "start": 158, "end": 225 - }, - "declared_index": 0 + } }, { "name": null, @@ -234,8 +233,7 @@ "dummy": false, "start": 231, "end": 312 - }, - "declared_index": 1 + } } ], "validity": null, diff --git a/examples/withdrawal.transfer.tir b/examples/withdrawal.transfer.tir index e5b95ce0..411954ab 100644 --- a/examples/withdrawal.transfer.tir +++ b/examples/withdrawal.transfer.tir @@ -167,12 +167,6 @@ { "name": "withdrawal", "data": { - "redeemer": { - "Struct": { - "constructor": 0, - "fields": [] - } - }, "credential": { "EvalParam": { "ExpectValue": [ @@ -181,6 +175,12 @@ ] } }, + "redeemer": { + "Struct": { + "constructor": 0, + "fields": [] + } + }, "amount": { "Number": 0 } From 16457b4b5d9c634ac833b282e8c54c2e77d76246 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ludue=C3=B1a?= Date: Thu, 12 Mar 2026 10:32:26 -0300 Subject: [PATCH 14/14] feat: include new test case for sorted publish --- crates/tx3-lang/src/parsing.rs | 2 + examples/index_outputs.ast | 488 +++++++++++++++++++++++++++++++++ examples/index_outputs.tx3 | 31 +++ 3 files changed, 521 insertions(+) create mode 100644 examples/index_outputs.ast create mode 100644 examples/index_outputs.tx3 diff --git a/crates/tx3-lang/src/parsing.rs b/crates/tx3-lang/src/parsing.rs index 9a62ba87..473f76a1 100644 --- a/crates/tx3-lang/src/parsing.rs +++ b/crates/tx3-lang/src/parsing.rs @@ -2823,4 +2823,6 @@ mod tests { test_parsing!(list_concat); test_parsing!(buidler_fest_2026); + + test_parsing!(index_outputs); } diff --git a/examples/index_outputs.ast b/examples/index_outputs.ast new file mode 100644 index 00000000..ce79630f --- /dev/null +++ b/examples/index_outputs.ast @@ -0,0 +1,488 @@ +{ + "env": null, + "txs": [ + { + "name": { + "value": "sorted_publish_plutus", + "span": { + "dummy": false, + "start": 35, + "end": 56 + } + }, + "parameters": { + "parameters": [ + { + "name": { + "value": "quantity", + "span": { + "dummy": false, + "start": 62, + "end": 70 + } + }, + "type": "Int" + } + ], + "span": { + "dummy": false, + "start": 56, + "end": 77 + } + }, + "locals": null, + "references": [], + "inputs": [ + { + "name": "source", + "many": false, + "fields": [ + { + "From": { + "Identifier": { + "value": "Sender", + "span": { + "dummy": false, + "start": 113, + "end": 119 + } + } + } + }, + { + "MinAmount": { + "AddOp": { + "lhs": { + "AddOp": { + "lhs": { + "AddOp": { + "lhs": { + "FnCall": { + "callee": { + "value": "Ada", + "span": { + "dummy": false, + "start": 141, + "end": 144 + } + }, + "args": [ + { + "Identifier": { + "value": "quantity", + "span": { + "dummy": false, + "start": 145, + "end": 153 + } + } + } + ], + "span": { + "dummy": false, + "start": 141, + "end": 154 + } + } + }, + "rhs": { + "FnCall": { + "callee": { + "value": "min_utxo", + "span": { + "dummy": false, + "start": 157, + "end": 165 + } + }, + "args": [ + { + "Identifier": { + "value": "funds", + "span": { + "dummy": false, + "start": 166, + "end": 171 + } + } + } + ], + "span": { + "dummy": false, + "start": 157, + "end": 172 + } + } + }, + "span": { + "dummy": false, + "start": 155, + "end": 156 + } + } + }, + "rhs": { + "FnCall": { + "callee": { + "value": "min_utxo", + "span": { + "dummy": false, + "start": 175, + "end": 183 + } + }, + "args": [ + { + "Identifier": { + "value": "funds_rep", + "span": { + "dummy": false, + "start": 184, + "end": 193 + } + } + } + ], + "span": { + "dummy": false, + "start": 175, + "end": 194 + } + } + }, + "span": { + "dummy": false, + "start": 173, + "end": 174 + } + } + }, + "rhs": { + "Identifier": { + "value": "fees", + "span": { + "dummy": false, + "start": 197, + "end": 201 + } + } + }, + "span": { + "dummy": false, + "start": 195, + "end": 196 + } + } + } + } + ], + "span": { + "dummy": false, + "start": 84, + "end": 208 + } + } + ], + "outputs": [ + { + "name": { + "value": "funds", + "span": { + "dummy": false, + "start": 402, + "end": 407 + } + }, + "optional": false, + "fields": [ + { + "To": { + "Identifier": { + "value": "Sender", + "span": { + "dummy": false, + "start": 422, + "end": 428 + } + } + } + }, + { + "Amount": { + "FnCall": { + "callee": { + "value": "min_utxo", + "span": { + "dummy": false, + "start": 446, + "end": 454 + } + }, + "args": [ + { + "Identifier": { + "value": "funds", + "span": { + "dummy": false, + "start": 455, + "end": 460 + } + } + } + ], + "span": { + "dummy": false, + "start": 446, + "end": 461 + } + } + } + }, + { + "Index": { + "Number": 0 + } + } + ], + "span": { + "dummy": false, + "start": 395, + "end": 486 + } + }, + { + "name": { + "value": "funds_rep", + "span": { + "dummy": false, + "start": 499, + "end": 508 + } + }, + "optional": false, + "fields": [ + { + "To": { + "Identifier": { + "value": "Sender", + "span": { + "dummy": false, + "start": 523, + "end": 529 + } + } + } + }, + { + "Amount": { + "SubOp": { + "lhs": { + "SubOp": { + "lhs": { + "Identifier": { + "value": "source", + "span": { + "dummy": false, + "start": 547, + "end": 553 + } + } + }, + "rhs": { + "FnCall": { + "callee": { + "value": "Ada", + "span": { + "dummy": false, + "start": 556, + "end": 559 + } + }, + "args": [ + { + "Identifier": { + "value": "quantity", + "span": { + "dummy": false, + "start": 560, + "end": 568 + } + } + } + ], + "span": { + "dummy": false, + "start": 556, + "end": 569 + } + } + }, + "span": { + "dummy": false, + "start": 554, + "end": 555 + } + } + }, + "rhs": { + "Identifier": { + "value": "fees", + "span": { + "dummy": false, + "start": 572, + "end": 576 + } + } + }, + "span": { + "dummy": false, + "start": 570, + "end": 571 + } + } + } + } + ], + "span": { + "dummy": false, + "start": 492, + "end": 583 + } + } + ], + "validity": null, + "mints": [], + "burns": [], + "signers": null, + "adhoc": [ + { + "Cardano": { + "Publish": { + "name": null, + "fields": [ + { + "To": { + "Identifier": { + "value": "Receiver", + "span": { + "dummy": false, + "start": 249, + "end": 257 + } + } + } + }, + { + "Amount": { + "FnCall": { + "callee": { + "value": "Ada", + "span": { + "dummy": false, + "start": 275, + "end": 278 + } + }, + "args": [ + { + "Identifier": { + "value": "quantity", + "span": { + "dummy": false, + "start": 279, + "end": 287 + } + } + } + ], + "span": { + "dummy": false, + "start": 275, + "end": 288 + } + } + } + }, + { + "Version": { + "Number": 3 + } + }, + { + "Script": { + "HexString": { + "value": "5101010023259800a518a4d136564004ae69", + "span": { + "dummy": false, + "start": 326, + "end": 364 + } + } + } + }, + { + "Index": { + "Number": 1 + } + } + ], + "span": { + "dummy": false, + "start": 227, + "end": 389 + } + } + } + } + ], + "span": { + "dummy": false, + "start": 32, + "end": 585 + }, + "collateral": [], + "metadata": null + } + ], + "types": [], + "aliases": [], + "assets": [], + "parties": [ + { + "name": { + "value": "Sender", + "span": { + "dummy": false, + "start": 6, + "end": 12 + } + }, + "span": { + "dummy": false, + "start": 0, + "end": 13 + } + }, + { + "name": { + "value": "Receiver", + "span": { + "dummy": false, + "start": 21, + "end": 29 + } + }, + "span": { + "dummy": false, + "start": 15, + "end": 30 + } + } + ], + "policies": [], + "span": { + "dummy": false, + "start": 0, + "end": 586 + } +} \ No newline at end of file diff --git a/examples/index_outputs.tx3 b/examples/index_outputs.tx3 new file mode 100644 index 00000000..6fe97597 --- /dev/null +++ b/examples/index_outputs.tx3 @@ -0,0 +1,31 @@ +party Sender; + +party Receiver; + +tx sorted_publish_plutus( + quantity: Int +) { + input source { + from: Sender, + min_amount: Ada(quantity) + min_utxo(funds) + min_utxo(funds_rep) + fees, + } + + cardano::publish { + to: Receiver, + amount: Ada(quantity), + version: 3, + script: 0x5101010023259800a518a4d136564004ae69, + index: 1, + } + + output funds { + to: Sender, + amount: min_utxo(funds), + index: 0, + } + + output funds_rep { + to: Sender, + amount: source - Ada(quantity) - fees, + } +}