From b0e8e5ea860dfc6fca7fe3d873a76812590f3ced Mon Sep 17 00:00:00 2001 From: sofia-bobbiesi Date: Mon, 15 Dec 2025 15:17:15 -0300 Subject: [PATCH 1/4] feat: add scope inspection command to inspect types in tx3 file --- src/commands/inspect/mod.rs | 4 ++ src/commands/inspect/scope.rs | 99 +++++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 src/commands/inspect/scope.rs diff --git a/src/commands/inspect/mod.rs b/src/commands/inspect/mod.rs index 936ba12..1176a7a 100644 --- a/src/commands/inspect/mod.rs +++ b/src/commands/inspect/mod.rs @@ -1,10 +1,13 @@ use crate::config::Config; use clap::{Args as ClapArgs, Subcommand}; +mod scope; mod tir; #[derive(Subcommand)] pub enum Command { + /// Inspect the global scope and available symbols in a tx3 file + Scope(scope::Args), /// Inspect the intermediate representation of a transaction Tir(tir::Args), } @@ -17,6 +20,7 @@ pub struct Args { pub fn run(args: Args, config: &Config) -> miette::Result<()> { match args.command { + Command::Scope(_) => scope::run(config), Command::Tir(args) => tir::run(args, config), } } diff --git a/src/commands/inspect/scope.rs b/src/commands/inspect/scope.rs new file mode 100644 index 0000000..ee5b809 --- /dev/null +++ b/src/commands/inspect/scope.rs @@ -0,0 +1,99 @@ +use clap::Args as ClapArgs; +use miette::IntoDiagnostic as _; +use serde::Serialize; + +use crate::config::Config; +use tx3_lang::ast::Symbol; + +#[derive(ClapArgs)] +pub struct Args { + #[arg(short, long)] + pretty: bool, +} + +#[derive(Serialize, Debug)] +struct SymbolInfo { + name: String, + r#type: String, + #[serde(skip_serializing_if = "Option::is_none")] + cases: Option, +} + +pub fn run(config: &Config) -> miette::Result<()> { + let main_path = config.protocol.main.clone(); + + let content = std::fs::read_to_string(main_path).into_diagnostic()?; + + let mut ast = tx3_lang::parsing::parse_string(&content)?; + + tx3_lang::analyzing::analyze(&mut ast).ok()?; + + // Extract symbols from the global scope + let symbols = if let Some(scope) = ast.scope() { + let mut symbol_list = Vec::new(); + + for (name, symbol) in scope.symbols() { + let r#type = match symbol { + Symbol::EnvVar(_, ty) => format!("EnvVar({})", ty), + Symbol::ParamVar(_, ty) => format!("ParamVar({})", ty), + Symbol::LocalExpr(_) => "LocalExpr".to_string(), + Symbol::Output(idx) => format!("Output({})", idx), + Symbol::Input(_) => "Input".to_string(), + Symbol::PartyDef(_) => "PartyDef".to_string(), + Symbol::PolicyDef(_) => "PolicyDef".to_string(), + Symbol::AssetDef(_) => "AssetDef".to_string(), + Symbol::TypeDef(_) => "TypeDef".to_string(), + Symbol::AliasDef(_) => "AliasDef".to_string(), + Symbol::RecordField(_) => "RecordField".to_string(), + Symbol::VariantCase(_) => "VariantCase".to_string(), + Symbol::Fees => "Fees".to_string(), + }; + + let cases = match symbol { + Symbol::EnvVar(env_name, ty) => Some(format!("env: {}, type: {}", env_name, ty)), + Symbol::ParamVar(param_name, ty) => { + Some(format!("param: {}, type: {}", param_name, ty)) + } + Symbol::TypeDef(typedef) => { + let cases: Vec = typedef + .cases + .iter() + .map(|c| { + if c.fields.is_empty() { + c.name.value.clone() + } else { + let fields: Vec = c + .fields + .iter() + .map(|f| format!("{}: {}", f.name.value, f.r#type)) + .collect(); + format!("{} {{ {} }}", c.name.value, fields.join(", ")) + } + }) + .collect(); + Some(format!("{}", cases.join(", "))) + } + Symbol::AliasDef(aliasdef) => Some(format!("alias: {}", aliasdef.alias_type)), + _ => None, + }; + + symbol_list.push(SymbolInfo { + name: name.clone(), + r#type, + cases, + }); + } + + symbol_list.sort_by(|a, b| a.r#type.cmp(&b.r#type)); + symbol_list + } else { + vec![] + }; + + println!( + "{}", + serde_json::to_string_pretty(&symbols).into_diagnostic()? + ); + + Ok(()) +} From c9bd8e7427ba53b0b0a610f5b3ab6a2c434bb885 Mon Sep 17 00:00:00 2001 From: sofia-bobbiesi Date: Tue, 16 Dec 2025 16:08:35 -0300 Subject: [PATCH 2/4] fix: correct argument passing in scope inspection command --- src/commands/inspect/mod.rs | 2 +- src/commands/inspect/scope.rs | 196 +++++++++++++++++++++------------- 2 files changed, 123 insertions(+), 75 deletions(-) diff --git a/src/commands/inspect/mod.rs b/src/commands/inspect/mod.rs index 1176a7a..591f7ae 100644 --- a/src/commands/inspect/mod.rs +++ b/src/commands/inspect/mod.rs @@ -20,7 +20,7 @@ pub struct Args { pub fn run(args: Args, config: &Config) -> miette::Result<()> { match args.command { - Command::Scope(_) => scope::run(config), + Command::Scope(args) => scope::run(args, config), Command::Tir(args) => tir::run(args, config), } } diff --git a/src/commands/inspect/scope.rs b/src/commands/inspect/scope.rs index ee5b809..381bea8 100644 --- a/src/commands/inspect/scope.rs +++ b/src/commands/inspect/scope.rs @@ -1,99 +1,147 @@ +use crate::config::Config; use clap::Args as ClapArgs; use miette::IntoDiagnostic as _; -use serde::Serialize; - -use crate::config::Config; +use std::collections::HashSet; use tx3_lang::ast::Symbol; #[derive(ClapArgs)] pub struct Args { - #[arg(short, long)] - pretty: bool, + #[arg( + short = 't', + long = "type", + value_name = "TYPE", + num_args = 1.., + action = clap::ArgAction::Append, + value_delimiter = ' ', + help = "Filter by symbol type (e.g. typedef, assetdef, envvar, paramvar, localexpr, output, input, + partydef, policydef, aliasdef, recordfield, variantcase, fees). + Accepts multiple values." + )] + types: Vec, } -#[derive(Serialize, Debug)] +#[derive(Debug)] struct SymbolInfo { name: String, r#type: String, - #[serde(skip_serializing_if = "Option::is_none")] cases: Option, } -pub fn run(config: &Config) -> miette::Result<()> { - let main_path = config.protocol.main.clone(); +fn build_symbol_info(name: &str, symbol: &Symbol) -> SymbolInfo { + let r#type = match symbol { + Symbol::EnvVar(_, ty) => format!("EnvVar({})", ty), + Symbol::ParamVar(_, ty) => format!("ParamVar({})", ty), + Symbol::LocalExpr(_) => "LocalExpr".to_string(), + Symbol::Output(idx) => format!("Output({})", idx), + Symbol::Input(_) => "Input".to_string(), + Symbol::PartyDef(_) => "PartyDef".to_string(), + Symbol::PolicyDef(_) => "PolicyDef".to_string(), + Symbol::AssetDef(_) => "AssetDef".to_string(), + Symbol::TypeDef(_) => "TypeDef".to_string(), + Symbol::AliasDef(_) => "AliasDef".to_string(), + Symbol::RecordField(_) => "RecordField".to_string(), + Symbol::VariantCase(_) => "VariantCase".to_string(), + Symbol::Fees => "Fees".to_string(), + }; - let content = std::fs::read_to_string(main_path).into_diagnostic()?; + let cases = match symbol { + Symbol::EnvVar(env, ty) => Some(format!("env: {}, type: {}", env, ty)), + Symbol::ParamVar(param, ty) => Some(format!("param: {}, type: {}", param, ty)), + Symbol::AliasDef(alias) => Some(format!("alias: {}", alias.alias_type)), + Symbol::TypeDef(typedef) => { + let cases = typedef + .cases + .iter() + .map(|case| { + if case.fields.is_empty() { + format!("- {}", case.name.value) + } else { + let fields = case + .fields + .iter() + .map(|f| format!(" + {}: {}", f.name.value, f.r#type)) + .collect::>() + .join("\n"); - let mut ast = tx3_lang::parsing::parse_string(&content)?; + format!("- {}\n{}", case.name.value, fields) + } + }) + .collect::>() + .join("\n"); + + Some(cases) + } + _ => None, + }; + + SymbolInfo { + name: name.to_string(), + r#type, + cases, + } +} + +pub fn run(args: Args, config: &Config) -> miette::Result<()> { + let content = std::fs::read_to_string(&config.protocol.main).into_diagnostic()?; + let mut ast = tx3_lang::parsing::parse_string(&content)?; tx3_lang::analyzing::analyze(&mut ast).ok()?; - // Extract symbols from the global scope - let symbols = if let Some(scope) = ast.scope() { - let mut symbol_list = Vec::new(); - - for (name, symbol) in scope.symbols() { - let r#type = match symbol { - Symbol::EnvVar(_, ty) => format!("EnvVar({})", ty), - Symbol::ParamVar(_, ty) => format!("ParamVar({})", ty), - Symbol::LocalExpr(_) => "LocalExpr".to_string(), - Symbol::Output(idx) => format!("Output({})", idx), - Symbol::Input(_) => "Input".to_string(), - Symbol::PartyDef(_) => "PartyDef".to_string(), - Symbol::PolicyDef(_) => "PolicyDef".to_string(), - Symbol::AssetDef(_) => "AssetDef".to_string(), - Symbol::TypeDef(_) => "TypeDef".to_string(), - Symbol::AliasDef(_) => "AliasDef".to_string(), - Symbol::RecordField(_) => "RecordField".to_string(), - Symbol::VariantCase(_) => "VariantCase".to_string(), - Symbol::Fees => "Fees".to_string(), - }; - - let cases = match symbol { - Symbol::EnvVar(env_name, ty) => Some(format!("env: {}, type: {}", env_name, ty)), - Symbol::ParamVar(param_name, ty) => { - Some(format!("param: {}, type: {}", param_name, ty)) - } - Symbol::TypeDef(typedef) => { - let cases: Vec = typedef - .cases - .iter() - .map(|c| { - if c.fields.is_empty() { - c.name.value.clone() - } else { - let fields: Vec = c - .fields - .iter() - .map(|f| format!("{}: {}", f.name.value, f.r#type)) - .collect(); - format!("{} {{ {} }}", c.name.value, fields.join(", ")) - } - }) - .collect(); - Some(format!("{}", cases.join(", "))) - } - Symbol::AliasDef(aliasdef) => Some(format!("alias: {}", aliasdef.alias_type)), - _ => None, - }; - - symbol_list.push(SymbolInfo { - name: name.clone(), - r#type, - cases, - }); + let mut symbols = match ast.scope() { + Some(scope) => scope + .symbols() + .into_iter() + .map(|(name, symbol)| build_symbol_info(name, symbol)) + .collect::>(), + None => Vec::new(), + }; + + symbols.sort_by(|a, b| a.r#type.cmp(&b.r#type).then(a.name.cmp(&b.name))); + + if !args.types.is_empty() { + let filters: HashSet = args.types.iter().map(|t| t.to_ascii_lowercase()).collect(); + + symbols.retain(|s| { + s.r#type + .split('(') + .next() + .map(|k| filters.contains(&k.to_ascii_lowercase())) + .unwrap_or(false) + }); + } + + if symbols.is_empty() { + if args.types.is_empty() { + println!("No symbols found in scope."); + } else { + println!("No symbols found for types: {}", args.types.join(", ")); } + return Ok(()); + } - symbol_list.sort_by(|a, b| a.r#type.cmp(&b.r#type)); - symbol_list - } else { - vec![] - }; + let mut last_type: Option<&str> = None; + + for sym in &symbols { + if last_type != Some(sym.r#type.as_str()) { + println!("\n\x1b[1m{}\x1b[0m", sym.r#type); + last_type = Some(&sym.r#type); + } - println!( - "{}", - serde_json::to_string_pretty(&symbols).into_diagnostic()? - ); + match &sym.cases { + Some(cases) if cases.contains('\n') => { + println!(" {}", sym.name); + for line in cases.lines() { + println!(" {}", line); + } + } + Some(cases) => { + println!(" {} — {}", sym.name, cases); + } + None => { + println!(" {}", sym.name); + } + } + } Ok(()) } From 255e3f16c3d3f618e83cf89e5ff5140ac023c15c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ludue=C3=B1a?= Date: Thu, 18 Dec 2025 12:30:46 -0300 Subject: [PATCH 3/4] fix use parents for looking up scope symbols as well --- src/commands/bindgen.rs | 4 ++-- src/commands/inspect/scope.rs | 25 ++++++++++++------------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/commands/bindgen.rs b/src/commands/bindgen.rs index d48ba98..d81929e 100644 --- a/src/commands/bindgen.rs +++ b/src/commands/bindgen.rs @@ -474,8 +474,8 @@ pub async fn run(_args: Args, config: &Config, profile: &ProfileConfig) -> miett let trp_config = profile .trp - .as_ref() - .ok_or_else(|| miette::miette!("TRP config not found"))?; + .clone() + .unwrap_or_else(|| TrpConfig::from(profile.chain.clone())); let job = Job { name: config.protocol.name.clone(), diff --git a/src/commands/inspect/scope.rs b/src/commands/inspect/scope.rs index 381bea8..16c9934 100644 --- a/src/commands/inspect/scope.rs +++ b/src/commands/inspect/scope.rs @@ -82,19 +82,18 @@ fn build_symbol_info(name: &str, symbol: &Symbol) -> SymbolInfo { } pub fn run(args: Args, config: &Config) -> miette::Result<()> { - let content = std::fs::read_to_string(&config.protocol.main).into_diagnostic()?; - - let mut ast = tx3_lang::parsing::parse_string(&content)?; - tx3_lang::analyzing::analyze(&mut ast).ok()?; - - let mut symbols = match ast.scope() { - Some(scope) => scope - .symbols() - .into_iter() - .map(|(name, symbol)| build_symbol_info(name, symbol)) - .collect::>(), - None => Vec::new(), - }; + let loader = tx3_lang::loading::ProtocolLoader::from_file(&config.protocol.main); + let protocol = loader.load()?; + + let mut symbols = Vec::new(); + let mut current_scope = protocol.ast().scope(); + + while let Some(scope) = current_scope { + for (name, symbol) in scope.symbols().iter() { + symbols.push(build_symbol_info(name, symbol)); + } + current_scope = scope.parent(); + } symbols.sort_by(|a, b| a.r#type.cmp(&b.r#type).then(a.name.cmp(&b.name))); From 4aa2d1a445e5fc00057288c9161f83268dead770 Mon Sep 17 00:00:00 2001 From: sofia-bobbiesi Date: Thu, 18 Dec 2025 14:16:47 -0300 Subject: [PATCH 4/4] fix: adjust formatting --- src/commands/inspect/scope.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/commands/inspect/scope.rs b/src/commands/inspect/scope.rs index 16c9934..380daa7 100644 --- a/src/commands/inspect/scope.rs +++ b/src/commands/inspect/scope.rs @@ -1,6 +1,5 @@ use crate::config::Config; use clap::Args as ClapArgs; -use miette::IntoDiagnostic as _; use std::collections::HashSet; use tx3_lang::ast::Symbol; @@ -54,7 +53,7 @@ fn build_symbol_info(name: &str, symbol: &Symbol) -> SymbolInfo { .iter() .map(|case| { if case.fields.is_empty() { - format!("- {}", case.name.value) + format!("{}", case.name.value) } else { let fields = case .fields