Skip to content
Merged
110 changes: 110 additions & 0 deletions crates/common/fmt/src/console.rs
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,40 @@ fn format_spec<'a>(
}
}

pub fn console_table_format(
keys: Option<&[&dyn ConsoleFmt]>,
values: &[&dyn ConsoleFmt],
) -> String {
let keys_header = "(index)";
let values_header = "Values";

let keys_strings: Vec<String> = match keys {
Some(keys) => keys.iter().map(|k| k.fmt(FormatSpec::String)).collect(),
None => (0..values.len()).map(|i| i.to_string()).collect(),
};
let values_strings: Vec<String> = values.iter().map(|v| v.fmt(FormatSpec::String)).collect();

let key_width = keys_strings.iter().map(|s| s.len()).max().unwrap_or(0).max(keys_header.len());
let value_width =
values_strings.iter().map(|s| s.len()).max().unwrap_or(0).max(values_header.len());

let border = |l: char, m: char, r: char| {
format!("{l}{}{m}{}{r}", "─".repeat(key_width + 2), "─".repeat(value_width + 2))
};

let mut out = String::new();
writeln!(out, "{}", border('┌', '┬', '┐')).unwrap();
writeln!(out, "│ {keys_header:<key_width$} │ {values_header:<value_width$} │").unwrap();
writeln!(out, "{}", border('├', '┼', '┤')).unwrap();
for i in 0..keys_strings.len().max(values_strings.len()) {
let key = keys_strings.get(i).map(String::as_str).unwrap_or("");
let value = values_strings.get(i).map(String::as_str).unwrap_or("");
writeln!(out, "│ {key:<key_width$} │ {value:<value_width$} │").unwrap();
}
write!(out, "{}", border('└', '┴', '┘')).unwrap();
out
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -610,4 +644,80 @@ mod tests {
let call = Logs::Log1(log1);
assert_eq!(call.fmt(Default::default()), "foo 42 bar");
}

#[test]
fn test_console_table_format() {
// auto-indexed, uint256 values
let values: &[&dyn ConsoleFmt] = &[&U256::from(100), &U256::from(200), &U256::from(300)];
assert_eq!(
console_table_format(None, values),
"┌─────────┬────────┐\n\
│ (index) │ Values │\n\
├─────────┼────────┤\n\
│ 0 │ 100 │\n\
│ 1 │ 200 │\n\
│ 2 │ 300 │\n\
└─────────┴────────┘"
);

// string keys, uint256 values
// key col expands to fit "charlie123" and value col expands to fit "20000000000000000"
let keys: &[&dyn ConsoleFmt] =
&[&String::from("alice"), &String::from("bob"), &String::from("charlie123")];
let values: &[&dyn ConsoleFmt] = &[
&U256::from(1),
&U256::from_str("20000000000000000").unwrap(),
&U256::from_str("30000000000").unwrap(),
];
assert_eq!(
console_table_format(Some(keys), values),
"┌────────────┬───────────────────┐\n\
│ (index) │ Values │\n\
├────────────┼───────────────────┤\n\
│ alice │ 1 │\n\
│ bob │ 20000000000000000 │\n\
│ charlie123 │ 30000000000 │\n\
└────────────┴───────────────────┘"
);

// empty table
assert_eq!(
console_table_format(None, &[]),
"┌─────────┬────────┐\n\
│ (index) │ Values │\n\
├─────────┼────────┤\n\
└─────────┴────────┘"
);

// more keys than values
let keys: &[&dyn ConsoleFmt] =
&[&String::from("alice"), &String::from("bob"), &String::from("charlie")];
let values: &[&dyn ConsoleFmt] = &[&U256::from(1), &U256::from(2)];
assert_eq!(
console_table_format(Some(keys), values),
"┌─────────┬────────┐\n\
│ (index) │ Values │\n\
├─────────┼────────┤\n\
│ alice │ 1 │\n\
│ bob │ 2 │\n\
│ charlie │ │\n\
└─────────┴────────┘"
);

// more values than keys
let keys: &[&dyn ConsoleFmt] = &[&String::from("alice"), &String::from("bob")];
let values: &[&dyn ConsoleFmt] =
&[&U256::from(1), &U256::from(2), &U256::from(3), &U256::from(4)];
assert_eq!(
console_table_format(Some(keys), values),
"┌─────────┬────────┐\n\
│ (index) │ Values │\n\
├─────────┼────────┤\n\
│ alice │ 1 │\n\
│ bob │ 2 │\n\
│ │ 3 │\n\
│ │ 4 │\n\
└─────────┴────────┘"
);
}
}
2 changes: 1 addition & 1 deletion crates/common/fmt/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
#![cfg_attr(not(test), warn(unused_crate_dependencies))]

mod console;
pub use console::{ConsoleFmt, FormatSpec, console_format};
pub use console::{ConsoleFmt, FormatSpec, console_format, console_table_format};

mod dynamic;
pub use dynamic::{
Expand Down
2 changes: 1 addition & 1 deletion crates/evm/abi/src/Console.json

Large diffs are not rendered by default.

8 changes: 6 additions & 2 deletions crates/evm/abi/src/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,15 @@ def main():
# Parse signatures from `console.sol`'s string literals
console_sol = open(console_file).read()
sig_strings = re.findall(
r'"(log.*?)"',
r'"((?:log|table).*?)"',
console_sol,
)
raw_sigs = [s.strip().strip('"') for s in sig_strings]
sigs = [
s.replace("string", "string memory").replace("bytes)", "bytes memory)")
re.sub(r"(\w+\[\])", r"\1 memory", s)
.replace("string,", "string memory,")
.replace("string)", "string memory)")
.replace("bytes)", "bytes memory)")
for s in raw_sigs
]
sigs = list(set(sigs))
Expand All @@ -38,6 +41,7 @@ def main():
)
combined = json.loads(r.stdout.strip())
abi = combined["contracts"]["<stdin>:HardhatConsole"]["abi"]

open(abi_file, "w").write(json.dumps(abi, separators=(",", ":"), indent=None))


Expand Down
4 changes: 3 additions & 1 deletion crates/evm/evm/src/inspectors/logs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ impl LogCollector {

fn hardhat_log(&mut self, data: &[u8]) -> alloy_sol_types::Result<()> {
let decoded = console::hh::ConsoleCalls::abi_decode(data)?;
self.push_msg(&decoded.fmt(Default::default()));
for line in decoded.fmt(Default::default()).lines() {
self.push_msg(line);
}
Ok(())
}

Expand Down
38 changes: 33 additions & 5 deletions crates/macros/src/console_fmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use syn::{
pub fn console_fmt(input: &DeriveInput) -> TokenStream {
let name = &input.ident;
let tokens = match &input.data {
Data::Struct(s) => derive_struct(s),
Data::Struct(s) => derive_struct(s, name),
Data::Enum(e) => derive_enum(e),
Data::Union(_) => return quote!(compile_error!("Unions are unsupported");),
};
Expand All @@ -18,16 +18,16 @@ pub fn console_fmt(input: &DeriveInput) -> TokenStream {
}
}

fn derive_struct(s: &DataStruct) -> TokenStream {
let imp = impl_struct(s).unwrap_or_else(|| quote!(String::new()));
fn derive_struct(s: &DataStruct, name: &Ident) -> TokenStream {
let imp = impl_struct(s, name).unwrap_or_else(|| quote!(String::new()));
quote! {
fn fmt(&self, _spec: FormatSpec) -> String {
#imp
}
}
}

fn impl_struct(s: &DataStruct) -> Option<TokenStream> {
fn impl_struct(s: &DataStruct, name: &Ident) -> Option<TokenStream> {
if s.fields.is_empty() {
return None;
}
Expand All @@ -36,13 +36,41 @@ fn impl_struct(s: &DataStruct) -> Option<TokenStream> {
return None;
}

let members = s.fields.members().collect::<Vec<_>>();

// TODO: name-based dispatches may be fragile; find a better way to detect table structs
if name.to_string().starts_with("table") {
Comment thread
mablr marked this conversation as resolved.
Outdated
let member_ref = |m: &Member| match m {
Member::Named(ident) => quote!(&self.#ident),
Member::Unnamed(idx) => quote!(&self.#idx),
};
let imp = if members.len() == 1 {
let vals = member_ref(&members[0]);
quote! {
let values: ::std::vec::Vec<&dyn ConsoleFmt> =
(#vals).iter().map(|v| v as &dyn ConsoleFmt).collect();
console_table_format(None, &values)
}
} else {
let keys = member_ref(&members[0]);
let vals = member_ref(&members[1]);
quote! {
let keys: ::std::vec::Vec<&dyn ConsoleFmt> =
(#keys).iter().map(|v| v as &dyn ConsoleFmt).collect();
let values: ::std::vec::Vec<&dyn ConsoleFmt> =
(#vals).iter().map(|v| v as &dyn ConsoleFmt).collect();
console_table_format(Some(&keys), &values)
}
};
return Some(imp);
}

let fields = s.fields.iter().collect::<Vec<_>>();
let first_ty = match &fields.first().unwrap().ty {
Type::Path(path) => path.path.segments.last().unwrap().ident.to_string(),
_ => String::new(),
};

let members = s.fields.members().collect::<Vec<_>>();
let args: Punctuated<TokenStream, Token![,]> = members
.into_iter()
.map(|member| match member {
Expand Down
Loading